page.tsx197 lines · main
| 1 | import Link from 'next/link'; |
| 2 | |
| 3 | import { DocsShell } from '../../../components/shell'; |
| 4 | |
| 5 | export const metadata = { title: 'migration · nextauth → briven' }; |
| 6 | |
| 7 | export default function NextAuthMigrationPage() { |
| 8 | return ( |
| 9 | <DocsShell> |
| 10 | <p className="font-mono text-xs text-[var(--color-text-muted)]"> |
| 11 | <Link href="/migration" className="hover:text-[var(--color-text)]"> |
| 12 | ← migration |
| 13 | </Link> |
| 14 | </p> |
| 15 | <h1 className="mt-2 font-mono text-2xl tracking-tight">nextauth → briven</h1> |
| 16 | <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]"> |
| 17 | moving from NextAuth (or Auth.js) to briven — which uses Better Auth under the hood. |
| 18 | the protocols line up cleanly; the work is at the integration seam (your prisma adapter |
| 19 | vs briven's schema, your getServerSession vs briven's client SDK). plan a |
| 20 | 2-3 day window for a single-app cutover. |
| 21 | </p> |
| 22 | |
| 23 | <div className="mt-6 rounded-md border border-[var(--color-text-error)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]"> |
| 24 | <strong>read this first:</strong> the typical pitfall is forgetting that briven also |
| 25 | owns the database. nextauth lives next to your db; briven IS the db boundary. you can |
| 26 | keep your existing postgres, but the schema for users/accounts/sessions/verifications |
| 27 | now lives in briven's control plane, not in your app. step 2 of the playbook is |
| 28 | where this lands. |
| 29 | </div> |
| 30 | |
| 31 | <Section title="account preservation"> |
| 32 | <p> |
| 33 | two cutover shapes — pick one before you start: |
| 34 | </p> |
| 35 | <ul className="list-disc pl-5"> |
| 36 | <li> |
| 37 | <strong>preserve user ids</strong> (recommended): import your nextauth users into |
| 38 | briven's <code>users</code> table with the existing ids. all your foreign |
| 39 | keys keep working (<code>posts.userId</code> still resolves). users sign in fresh |
| 40 | once after cutover. covered in step 3 of the playbook. |
| 41 | </li> |
| 42 | <li> |
| 43 | <strong>preserve sessions</strong>: bridge nextauth sessions to briven for a |
| 44 | window via a custom middleware. only worth it if you can't stomach a forced |
| 45 | re-auth (consumer app with millions of active sessions). file a ticket — this path |
| 46 | is supported but not self-service. |
| 47 | </li> |
| 48 | </ul> |
| 49 | </Section> |
| 50 | |
| 51 | <Section title="provider port"> |
| 52 | <p> |
| 53 | nextauth providers map to briven providers 1:1 — same OAuth endpoints, same scopes, |
| 54 | same redirect URIs. you just register them on the briven side instead of in your |
| 55 | nextauth config. supported on briven today: |
| 56 | </p> |
| 57 | <ul className="list-disc pl-5"> |
| 58 | <li> |
| 59 | <strong>magic link via email</strong> — drop-in for nextauth's{' '} |
| 60 | <code>EmailProvider</code>. briven uses mittera.eu (configurable);{' '} |
| 61 | <code>sendMagicLink</code> already wired. |
| 62 | </li> |
| 63 | <li> |
| 64 | <strong>email + password</strong> — drop-in for nextauth's{' '} |
| 65 | <code>CredentialsProvider</code> when you used password hashing. briven uses |
| 66 | argon2id (better-auth default). |
| 67 | </li> |
| 68 | <li> |
| 69 | <strong>Google OAuth</strong> — register a new client at console.cloud.google.com |
| 70 | with redirect URI <code>https://api.briven.tech/v1/auth/callback/google</code>. |
| 71 | </li> |
| 72 | <li> |
| 73 | <strong>GitHub OAuth</strong> — same shape, redirect URI{' '} |
| 74 | <code>https://api.briven.tech/v1/auth/callback/github</code>. |
| 75 | </li> |
| 76 | </ul> |
| 77 | <p> |
| 78 | not supported yet (file a ticket if you're blocked on one): Apple, Twitter, |
| 79 | Facebook, Discord. the underlying Better Auth library covers all of these — they |
| 80 | just need a small wire-up patch. |
| 81 | </p> |
| 82 | </Section> |
| 83 | |
| 84 | <Section title="schema port"> |
| 85 | <p> |
| 86 | nextauth's <code>users</code> / <code>accounts</code> / <code>sessions</code> /{' '} |
| 87 | <code>verification_tokens</code> tables map 1:1 to briven's{' '} |
| 88 | <code>users</code> / <code>accounts</code> / <code>sessions</code> /{' '} |
| 89 | <code>verifications</code>. column names line up exactly because both libraries |
| 90 | target the same Better Auth schema shape. import script: |
| 91 | </p> |
| 92 | <Snippet>{`-- inside the briven control-plane meta-db, after a fresh briven install |
| 93 | INSERT INTO users (id, email, email_verified, name, image, created_at) |
| 94 | SELECT id, email, email_verified IS NOT NULL, name, image, created_at |
| 95 | FROM nextauth_dump.users; |
| 96 | |
| 97 | INSERT INTO accounts (id, user_id, provider_id, account_id, access_token, |
| 98 | refresh_token, scope, id_token, password) |
| 99 | SELECT id, user_id, provider, "providerAccountId", access_token, |
| 100 | refresh_token, scope, id_token, NULL |
| 101 | FROM nextauth_dump.accounts; |
| 102 | |
| 103 | -- sessions intentionally NOT imported; users sign in fresh after cutover.`}</Snippet> |
| 104 | </Section> |
| 105 | |
| 106 | <Section title="api shape"> |
| 107 | <p> |
| 108 | nextauth's <code>getServerSession(authOptions)</code> becomes briven's |
| 109 | server-side session helper. nextauth's <code>useSession()</code> becomes |
| 110 | briven's <code>useSession()</code> hook from <code>@briven/react</code>. |
| 111 | </p> |
| 112 | <Snippet>{`// before — nextauth on next.js |
| 113 | import { getServerSession } from 'next-auth'; |
| 114 | |
| 115 | export default async function Page() { |
| 116 | const session = await getServerSession(authOptions); |
| 117 | if (!session) return <SignInPrompt />; |
| 118 | return <Dashboard user={session.user} />; |
| 119 | } |
| 120 | |
| 121 | // after — briven |
| 122 | import { brivenServer } from '@briven/react/server'; |
| 123 | |
| 124 | export default async function Page() { |
| 125 | const session = await brivenServer.session(); |
| 126 | if (!session) return <SignInPrompt />; |
| 127 | return <Dashboard user={session.user} />; |
| 128 | }`}</Snippet> |
| 129 | </Section> |
| 130 | |
| 131 | <Section title="callback / event hooks"> |
| 132 | <p> |
| 133 | nextauth's <code>callbacks.session</code>, <code>callbacks.jwt</code>, and{' '} |
| 134 | <code>events.signIn</code> become briven server functions called from the auth |
| 135 | lifecycle. instead of mutating the session object in a callback, write a briven |
| 136 | function that returns whatever shape your app needs and call it from your |
| 137 | dashboard: |
| 138 | </p> |
| 139 | <Snippet>{`// before — nextauth callback |
| 140 | callbacks: { |
| 141 | async session({ session, user }) { |
| 142 | session.user.role = await getRoleFromDb(user.id); |
| 143 | return session; |
| 144 | }, |
| 145 | } |
| 146 | |
| 147 | // after — briven function the client calls after sign-in |
| 148 | // briven/functions/getCurrentUser.ts |
| 149 | import { query } from '@briven/cli/server'; |
| 150 | |
| 151 | export const getCurrentUser = query({ |
| 152 | args: {}, |
| 153 | handler: async (ctx) => { |
| 154 | if (!ctx.user) return null; |
| 155 | const role = await ctx.db('user_roles') |
| 156 | .select(['role']) |
| 157 | .where({ user_id: ctx.user.id }) |
| 158 | .first(); |
| 159 | return { ...ctx.user, role: role?.role ?? 'free' }; |
| 160 | }, |
| 161 | });`}</Snippet> |
| 162 | </Section> |
| 163 | |
| 164 | <Section title="cutover checklist"> |
| 165 | <ul className="list-disc pl-5"> |
| 166 | <li>users + accounts imported, row counts match nextauth source</li> |
| 167 | <li>every nextauth provider re-registered with briven callback URLs</li> |
| 168 | <li>sign-in flow tested for every provider (manual)</li> |
| 169 | <li>getServerSession call sites replaced with brivenServer.session()</li> |
| 170 | <li>useSession imports updated to @briven/react</li> |
| 171 | <li>nextauth API routes (/api/auth/*) removed</li> |
| 172 | <li>session secret rotated (do not reuse nextauth's NEXTAUTH_SECRET)</li> |
| 173 | <li>parallel-run window planned + observed</li> |
| 174 | </ul> |
| 175 | </Section> |
| 176 | </DocsShell> |
| 177 | ); |
| 178 | } |
| 179 | |
| 180 | function Section({ title, children }: { title: string; children: React.ReactNode }) { |
| 181 | return ( |
| 182 | <section className="mt-10"> |
| 183 | <h2 className="font-mono text-lg">{title}</h2> |
| 184 | <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]"> |
| 185 | {children} |
| 186 | </div> |
| 187 | </section> |
| 188 | ); |
| 189 | } |
| 190 | |
| 191 | function Snippet({ children }: { children: string }) { |
| 192 | return ( |
| 193 | <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs"> |
| 194 | <code>{children}</code> |
| 195 | </pre> |
| 196 | ); |
| 197 | } |