Auth, teams and admin
Auth
Convex apps use Better Auth wired to Convex: email + password sign-up, sessions, and a hosted sign-in panel reachable from the marketing page's Get started button. Identity is ALWAYS server-derived — every backend handler calls requireUserId(ctx) first; the client never passes a user id. Two deployment env vars make auth work: BETTER_AUTH_SECRET (generated automatically on first prod deploy and preserved after) and SITE_URL (the trusted origin, synced to the real frontend URL by pnpm deploy).
Supabase apps use Supabase Auth with the same email + password UX; RLS policies scope every row to auth.uid(), so authorization lives in the database.
Workspaces
The shell ships a workspace switcher in the chrome; per-user data is indexed by_user (Convex convention: always index("by_user", ["userId"]) — and never name an index by_id, it is reserved).
team
The team feature adds members and invites: invite by email, accept, and list the roster. On Convex it is marker-only (no owned files — the logic lives in shared shell tables); on Supabase it owns src/features/team.tsx.
admin
The admin feature adds a role-gated area with a users list and an audit log. On Convex it owns convex/admin.ts; on Supabase, src/features/admin.tsx. Admin access is a role on the user record — promote a user server-side (Convex dashboard or SQL) rather than exposing a client mutation.
Roles in practice
- Regular users see the features they can use; admin pages simply do not render for them
- Server handlers re-check the role — nav hiding is UX, not security
- On Supabase, role checks are RLS policies: the API surface IS the policy set