Better Auth rollout
This runbook configures Better Auth as EXEPERT's identity authority while
keeping Supabase project vsfajebxloenaertaten as the database, RLS, Realtime,
RPC, and Edge Function data plane. The production origin is
https://app.exepert.com; the Google Cloud project is exepert-507611.
The repository implementation does not itself change any live dashboard. Do not enable the new runtime until every preflight and rollback item below is complete.
For local end-to-end authentication, start the Vite app and serverless API
together with pnpm dev:auth. The ordinary pnpm dev command intentionally
serves only the Vite client and therefore returns 404 for /api/auth/*.
Runtime contract
app_private.principal.id is the stable owner UUID carried in JWT sub.
Better Auth's authn.user.id can change during an account merge without
changing application ownership. The private schemas are never exposed through
the Data API.
1. Preflight and database migration
Confirm that the target URL is exactly
https://vsfajebxloenaertaten.supabase.co, take a database backup, and record
row counts for auth.users, projects, prompt_versions, arcade_episodes,
and chat_saved_prompts. Stop if the target or schema differs from the checked-
in migration.
Apply
supabase/migrations/20260904121150_better_auth_principal_bridge.sql through
the normal reviewed Supabase migration workflow. It:
- creates private
authnandapp_privateschemas; - retains every existing Supabase user UUID as a claimable principal;
- seeds verified email identities without copying password hashes;
- replaces the four ownership foreign keys with principal foreign keys;
- installs locked, single-use guest merge and legacy claim procedures; and
- pre-registers the
exepert-desktoppublic OAuth client and thehttps://app.exepert.com/apiresource.
Afterward, verify that principal count is at least the preflight auth.users
count, all four replacement constraints are validated, the private schemas
have no grants for anon or authenticated, and the original application row
counts are unchanged. Existing email users establish a Better Auth password
through password reset; hashes are intentionally not migrated.
The Vercel database credential must be a server-only PostgreSQL URL whose role
is a member of exepert_auth_backend. On PostgreSQL 16 and later, grant the
membership with INHERIT TRUE; role-level INHERIT alone is not sufficient
when the membership's inheritance option is disabled. Keep the pool small; the
implementation uses one connection per function instance and sequences
identity lookups to avoid exhausting Supabase's transaction pooler.
2. Supabase ES256 signing key
Generate a P-256 key with supabase gen signing-key --algorithm ES256. Store
the resulting private JWK in the secret manager before importing the same JWK
as a standby signing key in the target Supabase project.
Record its kid, wait until the public key appears at
/auth/v1/.well-known/jwks.json, then rotate it to current. Do not revoke the
previous key during the migration window: existing Supabase sessions remain
the rollback path. Set the full one-line private JWK only as
SUPABASE_JWT_PRIVATE_JWK in Vercel and its exact id as
SUPABASE_JWT_KID; neither value belongs in client code.
The bridge issues ES256 tokens with a five-minute expiry, audience
authenticated, role authenticated, and the stable principal UUID as
sub. The eight migrated Edge Functions explicitly validate either this token
or a temporary legacy Supabase session before passing the bearer to PostgREST.
3. Google Cloud OAuth
In GCP project exepert-507611, configure the OAuth branding/consent screen
for EXEPERT and create one Web application client.
- Authorized JavaScript origin:
https://app.exepert.com - Authorized redirect URI:
https://app.exepert.com/api/auth/callback/google - Local development origin:
http://localhost:9001 - Local development redirect URI:
http://localhost:9001/api/auth/callback/google
The redirect URI must match exactly, including scheme, host, path, and port.
Only the standard identity scopes (openid, email, and profile) are
needed. Put the client id and secret in Vercel as GOOGLE_CLIENT_ID and
GOOGLE_CLIENT_SECRET; no Google API key or Supabase anon key is created in
GCP.
If the consent screen remains in Testing, add the intended test accounts. Do not request paid Google APIs or billing for this sign-in flow.
4. Email and abuse protection
Verify auth.exepert.com in Resend and publish its required SPF/DKIM records in
the authoritative Vercel DNS zone for exepert.com. Store RESEND_API_KEY
only in Vercel and set RESEND_FROM to a verified sender, for example
EXEPERT <no-reply@auth.exepert.com>.
Create a Cloudflare Turnstile widget for app.exepert.com and the intended
local test hosts. Store the site key as TURNSTILE_SITE_KEY and the secret as
TURNSTILE_SECRET_KEY; both are consumed by the server configuration endpoint
and Better Auth respectively. Set TURNSTILE_ALLOWED_HOSTNAMES to an explicit
comma-separated allowlist. The Turnstile secret must never use a VITE_
prefix.
exepert.com currently delegates to ns1.vercel-dns.com and
ns2.vercel-dns.com; Cloudflare hosts other EXEPERT zones but is not
authoritative for this domain. VPSMalaysia remains the registrar. Do not
duplicate records in Cloudflare or the registrar's non-authoritative DNS
panels.
5. Vercel variables
Set these server-only production variables on the exepert project:
BETTER_AUTH_URL=https://app.exepert.com
BETTER_AUTH_DATABASE_URL=<private PostgreSQL URL>
BETTER_AUTH_SECRET=<at least 32 random characters>
AUTH_RUNTIME_MODE=legacy
SUPABASE_URL=https://vsfajebxloenaertaten.supabase.co
SUPABASE_PUBLISHABLE_KEY=<project publishable key>
SUPABASE_JWT_PRIVATE_JWK=<one-line private ES256 JWK>
SUPABASE_JWT_KID=<matching imported key id>
GOOGLE_CLIENT_ID=<GCP web client id>
GOOGLE_CLIENT_SECRET=<GCP web client secret>
RESEND_API_KEY=<server-only key>
RESEND_FROM=EXEPERT <no-reply@auth.exepert.com>
TURNSTILE_SITE_KEY=<widget site key>
TURNSTILE_SECRET_KEY=<widget secret>
TURNSTILE_ALLOWED_HOSTNAMES=app.exepert.com
EXEPERT_DESKTOP_CLIENT_ID=exepert-desktop
EXEPERT_AUTH_RESOURCE=https://app.exepert.com/api
Set VITE_AUTH_RUNTIME_MODE=legacy for the first deployment. The server's
/api/auth/config value is authoritative; the Vite flag is only a fail-safe
when that endpoint is unavailable. Existing client-safe
VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY values remain required for the
Supabase data plane.
6. Guarded activation
- Apply and verify the database migration while legacy auth remains active.
- Deploy the API, web UI, dual-token Edge Functions, and desktop-compatible
OAuth metadata with both runtime flags still set to
legacy. - Verify
/api/auth/health, the authorization-server metadata, protected- resource metadata, and Supabase JWKS without printing secrets. - Import/rotate the ES256 key and verify a server-minted token against an RLS- protected read plus one authenticated Edge Function.
- Change
AUTH_RUNTIME_MODEandVITE_AUTH_RUNTIME_MODEtobetter, deploy, then test Google, verified email, reset, guest creation, guest upgrade, legacy claim, Realtime, and restart persistence. - Release the native app only after its loopback state/issuer checks and Windows Credential Manager persistence pass on a real Windows session.
Watch authentication error rate, bridge issuance, merge failures, and RLS denials during the guarded window. Never log cookies, authorization headers, refresh tokens, database URLs, or JWKs.
Production activation record (2026-09-05)
The guarded browser/API cutover is active on app.exepert.com. Vercel
deployment dpl_7KZi1GPpq5Wujd25Ay8Jwca9kSy5 reports better from both
/api/auth/health and /api/auth/config. OAuth/OIDC discovery, the
standards-compatible well-known aliases, the imported Supabase ES256 key,
Turnstile, the verified auth.exepert.com Resend domain, the restricted
database role, and all eight dual-token Edge Functions are deployed.
Post-activation probes passed for rejected missing sessions/origins/CAPTCHA, Google authorization initiation and callback selection, anonymous Better Auth creation, principal synchronization, authenticated model discovery through the Supabase bridge, and session persistence across a page reload. The production smoke test intentionally created one anonymous Better Auth account.
Do not mark the rollout or native release fully complete until a person finishes the Google account-consent/guest-upgrade flow and the team verifies email delivery/reset, legacy claim, Realtime, and the native Windows loopback plus Credential Manager flow on real clients.
Rollback
Set both runtime flags back to legacy and redeploy the browser/API. Keep the
new tables, principal rows, imported signing key, replacement constraints, and
dual verifier intact; they are backward-compatible and preserve claim data.
Do not drop schemas, revert ownership foreign keys, delete Better Auth users,
or revoke the prior Supabase key during an incident. Native users can sign in
again after service recovery; their local draft remains available.