Skip to content

First-run setup

A new Supabase project has no schema, no users and no organisations. Setup happens in two parts: the database, then the first admin account.

scripts/supa-first-run.sh runs before pnpm dev. It does something only when you’re on main, master, dev or develop and the project has no migrations yet:

  1. It asks the Supabase Management API whether SUPABASE_PROJECT_REF has any migrations.
  2. If not, it applies them all with scripts/supa db push.
  3. It then runs scripts/supa-configure-project.sh. That sets the project-level settings a migration can’t reach: the schemas PostgREST exposes, the Auth site_url, and the Auth redirect allow-list (see Deployment).

On any other branch, or once migrations exist, it exits straight away. On a feature branch, run ./scripts/supa db push yourself.

While the schema exists but no app.system_admin row does, the app is uninitialised. The root route (routes/__root.tsx) checks getAppStatus() and redirects every page to /setup. A few routes are exempt: /setup, /auth/confirm and /auth/callback. Server routes such as /api/health don’t go through the root route, so they always work.

getAppStatus() uses the service-role client, because nobody can be signed in yet. It reports noSchema: true when the query fails, so /setup can tell “not migrated” from “not initialised”. The result is cached for an hour, so a new deployment can take a moment to notice it’s been set up.

/setup is a two-step wizard, backed by server/functions/setup.ts:

  1. signUpAdmin({ email, password }) creates the account. Depending on the project’s settings, either:

    • the user is signed in straight away, and goes to step 2; or
    • Supabase sends a confirmation email. The confirmation link signs them in, and they return to /setup for step 2.

    If the email already has an account (an interrupted earlier attempt), Supabase returns no error and sends nothing. signUpAdmin detects this and signs in with the password given, so setup can resume.

  2. initializeApp({ orgName }) needs a signed-in user. Using the service role, it creates their app.profile, the first app.organisation, the app.system_admin row and an org_admin membership. It checks again that no system admin exists, so it’s safe to retry.

After that, the app behaves normally. Everyone else joins by invitation.

No screen grants the role. Add a row by hand on the right project, using the user’s auth.users id:

insert into app.system_admin (user_id) values ('<user id>') on conflict do nothing;

A system admin can create organisations from the organisation menu, and can see every organisation and its member list. They can’t see an organisation’s maps unless they’re also a member: the network policies only check membership.