+ Lets them sign in with a short username instead of their email. 2–30 lowercase
+ letters, digits, dot, underscore, or hyphen. Leave blank to sign in by email
+ only.
+
+
+ )}
+
0) {
+ throw new ConflictError('That username is already taken.');
+ }
+ username = candidate;
+ }
+
// Two onboarding modes:
// - setup-email (default when no password is supplied): provision the
// account with a throwaway random password the admin never sees, then
@@ -179,6 +205,7 @@ export async function createUser(portId: string, data: CreateUserInput, meta: Au
await db.insert(userProfiles).values({
userId: newUserId,
displayName: data.displayName,
+ username,
firstName: data.firstName ?? null,
lastName: data.lastName ?? null,
phone: data.phone ?? null,
diff --git a/src/lib/validators/users.ts b/src/lib/validators/users.ts
index 27a2fe60..059af9d1 100644
--- a/src/lib/validators/users.ts
+++ b/src/lib/validators/users.ts
@@ -13,6 +13,9 @@ export const createUserSchema = z
* password. When false, `password` must be supplied inline. */
sendSetupEmail: z.boolean().optional(),
displayName: z.string().min(1).max(200),
+ /** Optional sign-in username. Shape/uniqueness/reserved checks run in the
+ * service (mirrors the self-service /api/v1/me path). Omit for email-only. */
+ username: z.string().optional(),
firstName: z.string().min(1).max(200).nullable().optional(),
lastName: z.string().min(1).max(200).nullable().optional(),
phone: z.string().optional(),