page.tsx197 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · nextauth → briven' };
6
7export 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&apos;s schema, your getServerSession vs briven&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;s <code>users</code> / <code>accounts</code> / <code>sessions</code> /{' '}
87 <code>verification_tokens</code> tables map 1:1 to briven&apos;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
93INSERT INTO users (id, email, email_verified, name, image, created_at)
94SELECT id, email, email_verified IS NOT NULL, name, image, created_at
95FROM nextauth_dump.users;
96
97INSERT INTO accounts (id, user_id, provider_id, account_id, access_token,
98 refresh_token, scope, id_token, password)
99SELECT id, user_id, provider, "providerAccountId", access_token,
100 refresh_token, scope, id_token, NULL
101FROM 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&apos;s <code>getServerSession(authOptions)</code> becomes briven&apos;s
109 server-side session helper. nextauth&apos;s <code>useSession()</code> becomes
110 briven&apos;s <code>useSession()</code> hook from <code>@briven/react</code>.
111 </p>
112 <Snippet>{`// before — nextauth on next.js
113import { getServerSession } from 'next-auth';
114
115export 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
122import { brivenServer } from '@briven/react/server';
123
124export 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&apos;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
140callbacks: {
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
149import { query } from '@briven/cli/server';
150
151export 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&apos;s NEXTAUTH_SECRET)</li>
173 <li>parallel-run window planned + observed</li>
174 </ul>
175 </Section>
176 </DocsShell>
177 );
178}
179
180function 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
191function 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}