Skip to main content

Self-registration provisions no tenant, membership, or role

Register.tsx calls only supabase.auth.signUp({ email, password, data: { full_name } }). It creates no company, no company_memberships row, and no role.

Membership/role are provisioned out of band, through paths that are invisible from the signup screen:

  • a Postgres trigger handle_new_user() creates a profiles row (name copied from auth metadata) — but not a membership;
  • the setup-company edge function creates a company + a super_admin membership, but only via the onboarding flow, and it rejects users who already have any membership (one-company-per-user at signup);
  • the invite-user edge function adds a user to an existing company with a role.

So a user who simply signs up authenticates successfully but has no primaryCompanyId — and the whole app keys off that. AdminLayout / Dashboard / OrgAuditLog compute companyId as null and the user is stuck in empty / "select a company" states forever, with no on-screen hint that a backend step is missing.

Related silent dependencies: audit actor names are read back from profiles.full_name (OrgAuditLog), so if the handle_new_user trigger isn't installed, audit logs show raw UUIDs. Email confirmation is assumed enabled in Supabase (Register unconditionally tells the user to check their email); password strength is a client-only length < 6 check.

The tacit assumption: the Supabase project has specific triggers and auth settings configured in the console, none of which live in the repo.

What breaks: provisioning the database from the repo alone yields an app where signup appears to work but produces unusable accounts.

Rebuild implication

Make provisioning explicit and server-owned: signup → create/join tenant → assign role, as one auditable flow. Don't rely on console-configured triggers that aren't in source control (see also phantom deliverables table).