Auth pattern — Supabase Auth + RLS

Why Supabase Auth (not Clerk / NextAuth / Auth0)

  • Same vendor as DB → JWT subauth.users.id join is native (no email-lookup roundtrip)
  • RLS lives next to the data — auth and authz are one table read away
  • Magic link + Google + GitHub out of the box
  • Free up to 50k MAU; matches our pre-revenue runway

RLS policies (per role)

sql
-- Tenant isolation: every table has user_id or organization_id CREATE POLICY "own rows only" ON pages FOR SELECT USING ( organization_id IN ( SELECT organization_id FROM organization_members WHERE user_id = auth.uid() ) );

Editors get write access. Viewers get read-only. Admin role bypasses via service-role key on the server only — never in the browser.

Session handling

  • Server components → createServerClient with cookie store
  • Client components → @supabase/ssr browser client
  • API routes → verify the JWT with verifyAuthToken (see lib/verify-auth.ts)
  • Anonymous edits → cookie-grouped anonymous_id until first sign-in

Magic link tips

  • Email template lives in Supabase dashboard, not in code (annoying for PRs)
  • 24h link TTL; user pasting into wrong browser → friction. Use OTP fallback when telemetry shows >5% magic-link bounces.

Open questions

  • Org invitations: do we want self-serve invite links or just email invitations?
  • Should we move to passkeys before launch or after?