← migration

nextauth → briven

moving from NextAuth (or Auth.js) to briven — which uses Better Auth under the hood. the protocols line up cleanly; the work is at the integration seam (your prisma adapter vs briven's schema, your getServerSession vs briven's client SDK). plan a 2-3 day window for a single-app cutover.

read this first:the typical pitfall is forgetting that briven also owns the database. nextauth lives next to your db; briven IS the db boundary. you can keep your existing postgres, but the schema for users/accounts/sessions/verifications now lives in briven's control plane, not in your app. step 2 of the playbook is where this lands.

account preservation

two cutover shapes — pick one before you start:

  • preserve user ids(recommended): import your nextauth users into briven's users table with the existing ids. all your foreign keys keep working (posts.userId still resolves). users sign in fresh once after cutover. covered in step 3 of the playbook.
  • preserve sessions: bridge nextauth sessions to briven for a window via a custom middleware. only worth it if you can't stomach a forced re-auth (consumer app with millions of active sessions). file a ticket — this path is supported but not self-service.

provider port

nextauth providers map to briven providers 1:1 — same OAuth endpoints, same scopes, same redirect URIs. you just register them on the briven side instead of in your nextauth config. supported on briven today:

  • magic link via email — drop-in for nextauth's EmailProvider. briven uses mittera.eu (configurable); sendMagicLink already wired.
  • email + password — drop-in for nextauth's CredentialsProvider when you used password hashing. briven uses argon2id (better-auth default).
  • Google OAuth — register a new client at console.cloud.google.com with redirect URI https://api.briven.tech/v1/auth/callback/google.
  • GitHub OAuth — same shape, redirect URI https://api.briven.tech/v1/auth/callback/github.

not supported yet (file a ticket if you're blocked on one): Apple, Twitter, Facebook, Discord. the underlying Better Auth library covers all of these — they just need a small wire-up patch.

schema port

nextauth's users / accounts / sessions / verification_tokens tables map 1:1 to briven's users / accounts / sessions / verifications. column names line up exactly because both libraries target the same Better Auth schema shape. import script:

-- inside the briven control-plane meta-db, after a fresh briven install
INSERT INTO users (id, email, email_verified, name, image, created_at)
SELECT id, email, email_verified IS NOT NULL, name, image, created_at
FROM nextauth_dump.users;

INSERT INTO accounts (id, user_id, provider_id, account_id, access_token,
                      refresh_token, scope, id_token, password)
SELECT id, user_id, provider, "providerAccountId", access_token,
       refresh_token, scope, id_token, NULL
FROM nextauth_dump.accounts;

-- sessions intentionally NOT imported; users sign in fresh after cutover.

api shape

nextauth's getServerSession(authOptions)becomes briven's server-side session helper. nextauth's useSession()becomes briven's useSession() hook from @briven/react.

// before — nextauth on next.js
import { getServerSession } from 'next-auth';

export default async function Page() {
  const session = await getServerSession(authOptions);
  if (!session) return <SignInPrompt />;
  return <Dashboard user={session.user} />;
}

// after — briven
import { brivenServer } from '@briven/react/server';

export default async function Page() {
  const session = await brivenServer.session();
  if (!session) return <SignInPrompt />;
  return <Dashboard user={session.user} />;
}

callback / event hooks

nextauth's callbacks.session, callbacks.jwt, and events.signIn become briven server functions called from the auth lifecycle. instead of mutating the session object in a callback, write a briven function that returns whatever shape your app needs and call it from your dashboard:

// before — nextauth callback
callbacks: {
  async session({ session, user }) {
    session.user.role = await getRoleFromDb(user.id);
    return session;
  },
}

// after — briven function the client calls after sign-in
// briven/functions/getCurrentUser.ts
import { query } from '@briven/cli/server';

export const getCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    if (!ctx.user) return null;
    const role = await ctx.db('user_roles')
      .select(['role'])
      .where({ user_id: ctx.user.id })
      .first();
    return { ...ctx.user, role: role?.role ?? 'free' };
  },
});

cutover checklist

  • users + accounts imported, row counts match nextauth source
  • every nextauth provider re-registered with briven callback URLs
  • sign-in flow tested for every provider (manual)
  • getServerSession call sites replaced with brivenServer.session()
  • useSession imports updated to @briven/react
  • nextauth API routes (/api/auth/*) removed
  • session secret rotated (do not reuse nextauth's NEXTAUTH_SECRET)
  • parallel-run window planned + observed