Authentication

Authentication overview

The dashboard UI is protected by Better Auth, mounted in-process at /api/auth/** by @onmax/nuxt-better-auth from apps/dashboard/server/auth.config.ts.

The dashboard UI is protected by Better Auth, mounted in-process at /api/auth/** by @onmax/nuxt-better-auth from apps/dashboard/server/auth.config.ts.

The boundary

Authentication follows the adapter rule at the package level:

  • packages/auth holds the policy: authBetterAuthOptions builds the database, policy, and provider options Better Auth needs. It has no HTTP server and no runtime imports, and its ./config subpath stays free of database dependencies so the login page can read feature flags without bundling one.
  • packages/database owns the store: the Drizzle schema, the connection factory, and the checked-in migrations. It knows nothing about sign-in and must never import packages/auth — see Database.
  • apps/dashboard/server/auth.config.ts composes that policy into the running instance. It is the only route in the app that resolves the connection string (through packages/database) and the signing secret, and therefore the only process that opens a connection to Postgres.

authBetterAuthOptions deliberately omits secret, baseURL, and trustedOrigins — the Nuxt module resolves those itself and constructs the actual instance, so the two cannot diverge. createAuth, which does build a full standalone instance, remains for callers that own their own secret and origin, such as the Better Auth CLI's schema-generation entry point.

Secrets

VariableRule
NUXT_BETTER_AUTH_SECRETRequired under this name in production; generate with openssl rand -base64 32
BETTER_AUTH_SECRETDevelopment-only fallback
DATABASE_URLPostgres connection string, resolved only by server/auth.config.ts (the pre-split AUTH_DATABASE_URL is still read when unset)

Closed by default

Registration and GitHub OAuth are off until you turn them on, so a fresh deployment cannot be signed up for by a stranger:

  • AUTH_ENABLE_SIGNUP=true enables self-registration at /signup (default false); with it off, that page shows a closed state and /login hides the link to it;
  • GitHub OAuth requires both GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET — see GitHub OAuth.
Rebuild after policy changesThe methods the auth pages offer are derived at build time from the same policy variables the server reads at runtime, and no runtime override can change them. Whenever you change those variables, rebuild the app — the server still enforces its own policy either way, but /login and /signup will keep advertising the old capabilities until rebuilt.

SSR and sessions

The dashboard renders with SSR. The session cookie is scoped to the app's own origin, so the server resolves it directly from the incoming request before the first paint — a signed-out visitor never flashes protected content before redirecting.

Sessions are not control-plane authority

The Better Auth session protects the dashboard UI. The control-plane API (/rpc/**, /api/v1/**) uses an independent bearer-token scheme — see Protect endpoints.

Copyright © 2026