Summary

Flow runs a closed beta: registration is open, but using the app requires explicit approval. A new account is gated until an admin approves it, or until the user redeems an invite code. Applies to both consumer users and B2B partners.

Files Changed

Backend (flow-platform)

  • supabase/migrations/20260622120000_beta_access_gate.sql — gate columns on profiles, invite_codes table, redeem_invite_code() RPC, backfill.
  • web/lib/admin-auth.ts — shared getAdminUserId() for the new routes.
  • web/lib/beta-access.ts — invite-code generation, magic-link builder, Resend approval/invite emails.
  • web/app/api/admin/access/profiles/route.ts + [id]/route.ts — list / approve / reject accounts.
  • web/app/api/admin/access/leads/route.ts — triage waitlist + partner_leads, invite on approve.
  • web/app/api/admin/access/invites/route.ts + [id]/route.ts — mint / list / revoke codes.
  • web/app/admin/(dashboard)/access/page.tsx + components/dashboard/AdminSidebar.tsx — “Accesso Beta” dashboard.

Key Decisions

  • Dedicated access_status column, not a reuse of profiles.status. status is the moderation lifecycle (active/suspended/banned); access_status (pending/approved/rejected) is the onboarding lifecycle. The app gate is access_status='approved' AND status='active'. Keeps the two concerns auditable and independent.
  • No change to handle_new_user() — new profiles inherit the column DEFAULT pending. Existing accounts were backfilled to approved in the same migration so the gate does not lock out current users on deploy.
  • Both unlock mechanisms (per product requirement): a magic link emailed on approval and a redeemable invite code. Both end at access_status='approved'.
  • Codes are admin-only at the table level; redemption goes through a SECURITY DEFINER RPC so a pending user (who can’t read invite_codes nor flip their own gate under RLS) can still self-approve with a valid code.
  • Admin actions via Next.js API routes, not an Edge Function — consistent with the existing web/app/api/admin/* pattern (service_role client + admin_actions_log + Resend), see overview.

Architecture Notes

Two converging paths, both ending at access_status='approved':

  1. Direct signup → profile born pending → admin approves from the dashboard → approval email (magic link + personal code).
  2. Whitelist lead (waitlist / partner_leads) → admin approves the lead → invite_codes row minted, invite email sent → user registers (born pending) and redeems the code → approved. The partner’s role flows into the code’s intended_role. On redemption the originating lead is advanced to its converted state (waitlist → joined, partner_leads → won) by redeem_invite_code() — migration 20260628120000.

Mobile gate implemented

Client-side enforcement now lives in flow-mobile (feat/beta-access-gate): GoRouter redirect + lock screen + invite-code redemption + Realtime unlock. See feat-beta-access-gate.

Required env

NEXT_PUBLIC_APP_INVITE_URL (deep link base), plus the existing RESEND_API_KEY / WAITLIST_FROM_EMAIL used by the marketing waitlist flow.

Admin API surface (web/app/api/admin/access/)

RouteMethodPurpose
profilesGETList accounts by access_status (paginated, search)
profiles/[id]POST{action:'approve'|'reject'} — flip gate, mint code, email
leadsGET / POSTTriage waitlist/partner leads; approve → invite + email
invitesGET / POSTList / mint codes (optional email binding, role, max_uses)
invites/[id]DELETERevoke (soft → status='revoked')

All gated by getAdminUserId() and logged to admin_actions_log.