Core
Authentication
Better Auth setup, environment variables, invites, and dashboard gating
Selection
CertaOS uses Better Auth for application authentication and session management.
Decision record: docs/decisions/0001-auth-better-auth.md
Runtime split
- Better Auth handles app identity and sessions.
- Supabase RLS remains the data authorization boundary.
Current implementation
- Server config:
src/lib/auth/server.ts - Client helper:
src/lib/auth/client.ts - API handler:
src/app/api/auth/[...all]/route.ts - Drizzle auth tables:
src/db/schema/better-auth.ts
The auth route runs on the Node.js runtime (required for pg/Drizzle).
Required environment variables
BETTER_AUTH_SECRETBETTER_AUTH_BASE_URLBETTER_AUTH_URLNEXT_PUBLIC_BETTER_AUTH_URLALLOW_PUBLIC_SIGNUP(optional; iftrue, production signup is public; otherwise it is invite-only)
Legacy fallback:
AUTH_SECRET(temporary)
Database notes
- Better Auth uses its own tables (
user,session,account,verification) alongside CertaOS domain tables (likeusers). - Migrations are generated via Drizzle Kit and live in
src/db/migrations/. - Domain authorization uses
users(UUID) and links to Better Auth viauser_identities(seedocs/decisions/0002-identity-bridge.md). - Domain emails are normalized to lowercase for consistent matching (
users.email,user_invitations.email). session.updated_atandaccount.updated_atdefault tonow()to keep Better Auth inserts working in serverless environments.
Current status
- Minimal sign-up/sign-in flows exist.
/dashboard/*is session-gated and role-gated server-side.
Dashboard gating
/dashboard/*requires an authenticated session.- Role checks are enforced server-side on dashboard pages (for now).
/dashboard/accountshows your session + domain user mapping for debugging.
Invites (Bootstrap)
Invite-only enforcement is implemented at the auth endpoint layer (see docs/decisions/0003-invite-only-signup.md).
Invites are redeemed when a user first creates/links their domain user record (typically by signing in and visiting /dashboard). The invite's role (and optional provider_id / firm_id) are applied to the domain users row at that time.
Manage invites via:
- Admin UI:
/dashboard/admin(platform_admin only). Provider/Firm fields scope onboarding forprovider_admin/counselorandattorney. - CLI helpers:
npm run user:invite -- --email you@example.com --role provider_admin --provider-id <uuid>npm run user:set-role -- --email you@example.com --role provider_admin --provider-id <uuid|null>