Who may create an account in this instance, and how the very first one is created. Three env switches (SETUP_TOKEN_REQUIRED, SIGNUP_ENABLED, ALLOWED_SIGNUP_DOMAINS/ADMIN_SIGNUP_DOMAINS) and one invariant that holds under concurrency.
Invariant: the first account is always created via /setup (as ADMIN). Every other registration channel (password signup, first-time Google Sign-In) requires setup to be complete and SIGNUP_ENABLED=true. Login and account linking for existing users are never gated by any of these.
- First-run setup (
/setup). When the database has no users, the frontend (SetupGateinsrc/client/components/SetupGate.tsx) redirects anonymous traffic to/setup, where the operator creates the initial admin viaPOST /api/auth/setup. That endpoint first callsrefreshSetupState()(insetup.service.ts) to self-heal a stale in-memory flag from the DB, then creates the user asADMIN(withlastLoginAtstamped, since it auto-logs-in), bypassingALLOWED_SIGNUP_DOMAINS(the operator is trusted), inside a transaction with a Postgres advisory lock + count re-check (createInitialAdmininauth.service.ts) so the /setup-vs-/setup bootstrap can only ever happen once even under concurrency. On success it sets the auth cookie andcompleteSetup()flips the in-memory flag. State lives insrc/api/features/auth/setup.service.ts;initSetupState()runs once at boot fromsrc/index.ts(notapp.onStart, which would re-run underbun --hot).- Scope of the advisory lock. It serializes
/setupposts against each other (across replicas sharing the DB). It does not serialize against thebun set-adminCLI, which writes outside this lock; runningbun set-adminand/setupconcurrently can produce a regular user and a /setup-created admin. The invariant we actually care about ("/setup produces at most one admin") still holds; flagging this in case future code grows other admin-creation paths. - Operational caveats. The setup token and the
setupCompleteflag are per-process in memory. (1) Multi-instance: each replica logs its own token, so the browser must reach the instance that generated it (single instance or sticky sessions for the first run); the DB advisory lock still guarantees a single admin. A stale replica self-heals on its nextPOST /auth/setupbecauserefreshSetupState()queries the DB before the token check and flips the local flag if a user already exists, so the operator gets a clean 409 (which the frontend converts into a redirect to/loginviaAuthContext.refresh()) instead of a permanent 401 from local-token mismatch. (2) Re-opening setup: if every user is deleted while the app runs, the flag stays "complete" until the next restart, wheninitSetupState()re-detects the empty table and reopens/setup. (3) Boot-time DB outage:initSetupState()errors are logged as warnings and the flag staysfalse;refreshSetupState()on the first/setupPOST self-heals once the DB recovers, so this no longer produces a permanent dead-end.
- Scope of the advisory lock. It serializes
SETUP_TOKEN_REQUIRED(defaulttrue) protects the setup window. At boot, when no users exist, the server logs a one-time token and a ready-to-use${PUBLIC_URL}/setup?token=...URL; the setup form requires that token (verified timing-safe). TheSetupPagepre-fills it from the query string then strips it from the URL. Set tofalseonly on trusted networks to allow/setupwithout a token. The token is held in memory (regenerated on each restart while setup is pending) and cleared on completion./auth/meexposessetupRequiredandsetupTokenRequiredfor the frontend.SIGNUP_ENABLED(defaultfalse) is the master switch for public registration. Whenfalse, bothPOST /api/auth/signupand first-time Google Sign-In that would create a new account return403(errors.signupDisabled); existing users keep logging in./auth/meexposessignupEnabledso the UI hides the signup link and/signupredirects to/login. To add users without opening public signup: enable Google (existing-account linking still works), flipSIGNUP_ENABLED=true, or usebun set-admin.ALLOWED_SIGNUP_DOMAINS(comma-separated, default empty = allow any) gatesPOST /api/auth/signupand first-time Google Sign-In viaPOST /api/auth/google(only relevant whenSIGNUP_ENABLED=true). Existing users are never affected: login and account linking skip the check so pre-existing accounts keep working even if their domain is later removed from the allowlist. The first-run/setupadmin bypasses this allowlist.ADMIN_SIGNUP_DOMAINS(comma-separated, default empty = no auto-promotion) auto-promotes new accounts from listed domains toADMINon signup. Only fires for first-time Google Sign-In withemail_verified: true. Password signups viaPOST /api/auth/signupare always created asUSER(no proof of address ownership); usebun set-admin <email>to promote them. IfALLOWED_SIGNUP_DOMAINSis set, admin domains must be in the allowlist as well.
Threat model for ADMIN_SIGNUP_DOMAINS. "The user controls this email address" is not the same as "the user is entitled to ADMIN in this application". Anyone who can obtain a Google ID token with email_verified: true for a listed domain becomes ADMIN on first sign-in: any Workspace admin of the domain, any insider with an account, anyone who compromises the Workspace credentials or DNS, anyone who exploits a Google provisioning bug. Only enable this when the group that operates the Google Workspace is the same group that should have admin control over the application. Otherwise leave it empty and promote admins explicitly with bun set-admin.
To narrow the takeover window for bun set-admin-created accounts, the Google linking flow refuses to attach a Google identity to an ADMIN row that has never logged in. The pre-created admin must complete a password login at least once before Google linking becomes available.