Authentication — NextAuth → Better Auth migration plan
Why we ship with NextAuth today, why we're moving to Better Auth, and the migration path that won't lock you in.
Today: NextAuth v5 (Auth.js)
The boilerplate ships with next-auth@5.0.0-beta.30 and the Drizzle adapter
(@auth/drizzle-adapter). It's the most common auth choice for Next.js and the
ecosystem documentation is the deepest, which makes the boilerplate immediately
familiar to most buyers.
What's wired
- Email + password — credentials provider with bcrypt hashing
(
src/actions/register-user.ts). - OAuth — GitHub + Google providers, gated on env vars. Without keys, the buttons don't render.
- Sessions — JWT strategy, 30-day expiry, rolling refresh.
- Email verification + password reset — both flows live, both use the
shared
Emaillibrary so the dev no-op behavior (stdout instead of Resend) works for local dev. - Account deletion — 30-day grace period, daily cron hard-delete.
Where NextAuth shows its age
- The session callback is the only place you can attach custom claims, and it runs on every request — easy to over-fetch.
- The OAuth flow is opaque. Debugging a misconfigured provider means reading Auth.js source.
- Multi-tenant ergonomics are bolted on. The boilerplate's
org_idswitch lives ingetCurrentOrg()rather than in the session itself. - Edge runtime support requires careful adapter selection.
Tomorrow: Better Auth
Better Auth is already a dependency
(better-auth@^1.6.9) — pre-installed but not wired. The migration plan:
Phase 1 — Coexistence (now → next 2 sprints)
- Add Better Auth alongside NextAuth, mounted at
/api/auth/v2/*. - Mirror the user / session schema (Better Auth's models are Drizzle-friendly and ship with a code generator).
- Switch new sign-ups to Better Auth behind a feature flag
(
auth.use_better_auth). Existing users keep using NextAuth until they next sign in.
Phase 2 — Migration (sprints 3-4)
- On every NextAuth sign-in, dual-write the session to Better Auth.
- Backfill existing user records into the Better Auth
accounttable. - Migrate password hashes (bcrypt → argon2id) on next successful sign-in (graceful, no forced reset email).
Phase 3 — Cutover (sprint 5)
- Flip the feature flag to 100%.
- Drop the NextAuth route handlers, leaving the dual-write code as a fallback for one more sprint.
- Remove
next-authand@auth/drizzle-adapterfrompackage.json. Migration commit is structural-only under the Tidy First rule (S commit, no behavior change).
Why bother
- Tenant-native — Better Auth treats organizations as first-class, with
built-in invite/role/permission primitives. The boilerplate's
organization_invitesflow becomes a thin call-through. - Edge-first — runs everywhere Next.js runs, with no separate adapter.
- Plugin model — 2FA, magic links, passkeys, and OIDC providers ship as composable plugins instead of a session-callback maze.
- Better DX — typed session, typed user, no
as anyceremony.
Rollback
Drop the auth.use_better_auth flag and the dual-write code. The NextAuth
path is the source of truth until Phase 3 cutover, so rollback before then is
a single commit revert.
What stays the same
- The
Emaillibrary — both auth systems callEmail.sendVerifyEmail()and friends. - The
audit_chainemitters —account.deletion_scheduled,invite.accepted, etc., are unchanged. - The
<TierGate>component andrequireTier()helper — they read fromgetCurrentOrg()which is auth-agnostic.
The whole point of the migration plan is that you can swap the auth provider without rewriting the rest of the app.