nextjsboilerplate Docs
Architecture

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 Email library 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_id switch lives in getCurrentOrg() 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)

  1. Add Better Auth alongside NextAuth, mounted at /api/auth/v2/*.
  2. Mirror the user / session schema (Better Auth's models are Drizzle-friendly and ship with a code generator).
  3. 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)

  1. On every NextAuth sign-in, dual-write the session to Better Auth.
  2. Backfill existing user records into the Better Auth account table.
  3. Migrate password hashes (bcrypt → argon2id) on next successful sign-in (graceful, no forced reset email).

Phase 3 — Cutover (sprint 5)

  1. Flip the feature flag to 100%.
  2. Drop the NextAuth route handlers, leaving the dual-write code as a fallback for one more sprint.
  3. Remove next-auth and @auth/drizzle-adapter from package.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_invites flow 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 any ceremony.

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 Email library — both auth systems call Email.sendVerifyEmail() and friends.
  • The audit_chain emitters — account.deletion_scheduled, invite.accepted, etc., are unchanged.
  • The <TierGate> component and requireTier() helper — they read from getCurrentOrg() 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.

On this page