First-run setup
A fresh deployment has no schema, no users, and no organisations. First-run
setup bridges that gap — automatically for the database, then interactively
for the first admin account — via scripts/supa-first-run.sh, the /setup
route, and three server functions in apps/app/src/server/functions/setup.ts.
Database bootstrap (automatic)
Section titled “Database bootstrap (automatic)”scripts/supa-first-run.sh runs on every pnpm dev start (root predev
hook) and whenever scripts/supa <runner> is invoked on a protected branch.
It’s a no-op once migrations exist:
- Calls the Supabase Management API to check whether any migrations have
been applied to
SUPABASE_PROJECT_REF. - If none have, applies every local migration with
SUPABASE_FIRST_RUN=1 scripts/supa db push(the one exception to the protected-branch migration guard — see Migration workflow). - Runs
scripts/supa-configure-project.shto set the PostgREST exposed-schema list and the Auth redirect allow-list — project-level settings a migration can’t reach, without which everyapp/networkquery would 42501 even on a freshly-migrated schema.
Both steps are idempotent and safe to run on every pnpm dev start; they exit
immediately once done.
Application bootstrap (interactive)
Section titled “Application bootstrap (interactive)”Once the schema exists but no app.system_admin row exists yet, the app is
“uninitialised”. The root route’s beforeLoad (apps/app/src/routes/__root.tsx)
calls getAppStatus() on every request and redirects any route outside
SETUP_EXEMPT (/setup, /auth/confirm, /auth/callback) to /setup.
getAppStatus() uses the service-role client — it must be able to answer
“is this app initialised?” before any admin user (and therefore before any
RLS-satisfying session) exists. It also reports noSchema: true if the query
itself fails, so /setup can distinguish “not initialised” from “not
migrated”.
The /setup route is a two-step wizard:
signUpAdmin({ email, password })— callssupabase.auth.signUp(). Three outcomes:- Email confirmation is disabled (or Supabase signs the user straight in): proceed immediately to step 2.
- A genuinely new signup: Supabase sends a confirmation email;
the UI shows a “check your inbox” state. Confirming
(
/auth/confirm) redirects to/dashboard, which redirects back to/setupfor step 2 since the app still isn’t initialised. - Already-registered-email recovery: Supabase’s anti-enumeration
behaviour means
signUp()never errors and never sends an email for an address that already has an account — it silently returnsidentities: [].signUpAdmindetects this and tries signing in with the password just supplied, so a setup that was interrupted after the auth user was created (but beforeinitializeAppran) can be resumed without creating a duplicate account or leaking whether the email exists.
initializeApp({ orgName })— one service-role write that creates the caller’sapp.profile, the firstapp.organisation, theapp.system_adminrow, and anorg_adminmembership row, all in sequence. Re-verifies no system admin exists first (defence-in-depth —/setup’sbeforeLoadalso checks) and requires a valid signed-in session (getClaims()), so it’s safe to retry if a step fails partway.
After initializeApp succeeds, getAppStatus() reports initialized: true
and the app behaves normally for every subsequent request.