Skip to main content

Clerk to BetterAuth Migration — SO1 Console

Date: 2026-03-16 Repos: so1-io/so1-console References: sparki-tools/web-app, smo1-io/meow-web

What Changed

Migrated so1-console (Rover + BFF) from Clerk to BetterAuth, unifying the auth stack across the platform. Key changes:
  • Console: Replaced @clerk/nextjs with better-auth. Custom login/signup pages at /auth/login and /auth/signup. Cookie-based session (so1.session_token) instead of Clerk JWT.
  • BFF: Replaced @clerk/backend JWT verification with shared BFF_AUTH_SECRET + trusted internal headers pattern.
  • E2E: Full auth cycle Playwright tests: signup, login, logout, session persistence, auth gate protection.
  • CI: Separate rover-e2e-auth job for auth cycle testing alongside existing smoke tests.

Pattern: Internal BFF Auth (Trusted Headers)

When the BFF sits behind a same-origin Next.js proxy, the proxy validates the BetterAuth session and forwards:
Authorization: Bearer <BFF_AUTH_SECRET>
X-Auth-User-Id: <user_id>
X-Auth-Org-Id: <org_id>
X-Auth-Email: <email>
This avoids the BFF needing the BetterAuth library. Security relies on:
  1. BFF only accepting the shared secret
  2. BFF only being accessible internally (not exposed to the internet)

Learnings

  1. BetterAuth is not JWT-based: Uses opaque session tokens + database lookup. Simpler than Clerk’s JWT/JWKS flow but means downstream services can’t decode tokens independently.
  2. Cookie prefix matters: The session cookie name is {cookiePrefix}.session_token. Must match in middleware and auth config.
  3. No ClerkProvider replacement needed: BetterAuth client hooks (useSession, signIn, etc.) work without a provider wrapper — they use the base URL for API calls.
  4. Build-time compatibility: BetterAuth config references process.env.DATABASE_URL which is only available at runtime. Build succeeds with placeholder values in CI.
  5. E2E auth is simpler: No two-step Clerk flow (identifier → password). BetterAuth login is a single form with email + password fields.