CertaOS
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_SECRET
  • BETTER_AUTH_BASE_URL
  • BETTER_AUTH_URL
  • NEXT_PUBLIC_BETTER_AUTH_URL
  • ALLOW_PUBLIC_SIGNUP (optional; if true, 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 (like users).
  • Migrations are generated via Drizzle Kit and live in src/db/migrations/.
  • Domain authorization uses users (UUID) and links to Better Auth via user_identities (see docs/decisions/0002-identity-bridge.md).
  • Domain emails are normalized to lowercase for consistent matching (users.email, user_invitations.email).
  • session.updated_at and account.updated_at default to now() 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/account shows 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 for provider_admin/counselor and attorney.
  • 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>