<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ship Kit</title>
    <description>The latest articles on DEV Community by Ship Kit (@authlayerdev).</description>
    <link>https://dev.to/authlayerdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995717%2Ff0f0d651-64a9-41b1-be54-6c5c9c7ce4d5.png</url>
      <title>DEV Community: Ship Kit</title>
      <link>https://dev.to/authlayerdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/authlayerdev"/>
    <language>en</language>
    <item>
      <title>Migrating from NextAuth to Better Auth in Next.js (and What the Boring Parts Actually Are)</title>
      <dc:creator>Ship Kit</dc:creator>
      <pubDate>Sun, 21 Jun 2026 22:45:55 +0000</pubDate>
      <link>https://dev.to/authlayerdev/migrating-from-nextauth-to-better-auth-in-nextjs-and-what-the-boring-parts-actually-are-4lf3</link>
      <guid>https://dev.to/authlayerdev/migrating-from-nextauth-to-better-auth-in-nextjs-and-what-the-boring-parts-actually-are-4lf3</guid>
      <description>&lt;p&gt;If you've shipped a Next.js app on NextAuth (now Auth.js), you know it works. The reason people move to Better Auth usually isn't that NextAuth is bad — it's that Better Auth gives you typed, first-class access to sessions, organizations, and database-backed concepts without bolting adapters and callbacks together by hand. The session calls are typed end to end, the schema is generated for you, and features like multi-tenancy or admin tooling are first-party rather than community glue.&lt;/p&gt;

&lt;p&gt;This post is the honest version of that migration: the mechanical parts you can rewrite almost blindly, and the parts you still have to do by hand. I'll show real before/after code for each.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Coming from Clerk instead? See the companion guide: &lt;a href="https://betterauth.app/blog/migrating-from-clerk-to-better-auth/" rel="noopener noreferrer"&gt;Moving off Clerk to Better Auth&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: NextAuth is a library, so a lot of the rewrite is mechanical
&lt;/h2&gt;

&lt;p&gt;Much of a NextAuth integration is &lt;em&gt;call sites&lt;/em&gt; — &lt;code&gt;getServerSession&lt;/code&gt; here, &lt;code&gt;useSession&lt;/code&gt; there, a &lt;code&gt;signIn&lt;/code&gt; button. Those follow patterns, which means they're transformable. The part that &lt;em&gt;isn't&lt;/em&gt; mechanical is your &lt;strong&gt;configuration&lt;/strong&gt;: providers, the database adapter, and callbacks. That's the logic only you understand, and no tool should silently rewrite it.&lt;/p&gt;

&lt;p&gt;Keep that split in your head for the whole migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Call sites&lt;/strong&gt; → mechanical, fast, low-risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config + schema + data&lt;/strong&gt; → manual, deliberate, where the real work lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Imports
&lt;/h2&gt;

&lt;p&gt;NextAuth spreads its surface across several entry points. Better Auth centralizes server access in your &lt;code&gt;auth&lt;/code&gt; instance and client access in an &lt;code&gt;authClient&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;next-auth&lt;/code&gt; and &lt;code&gt;next-auth/react&lt;/code&gt; imports collapse into three: your server &lt;code&gt;auth&lt;/code&gt; instance, &lt;code&gt;next/headers&lt;/code&gt; (you'll need it for session reads), and the &lt;code&gt;authClient&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Server-side session reads
&lt;/h2&gt;

&lt;p&gt;This is the most common change in a real codebase. NextAuth's &lt;code&gt;getServerSession(authOptions)&lt;/code&gt; becomes a call on the Better Auth instance that takes the request headers explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing &lt;code&gt;headers()&lt;/code&gt; is not boilerplate you can skip — Better Auth reads the session cookie from those headers, so every server-side session read needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Client-side session reads
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same shape, different origin — the hook now comes off &lt;code&gt;authClient&lt;/code&gt; instead of a top-level &lt;code&gt;next-auth/react&lt;/code&gt; import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: signIn / signOut (watch the arguments)
&lt;/h2&gt;

&lt;p&gt;This is the first place where a find-and-replace is &lt;em&gt;not&lt;/em&gt; enough, and it's worth slowing down. The import source changes cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// the call shape is different — see below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better Auth doesn't use a single &lt;code&gt;signIn(provider, options)&lt;/code&gt; function. Credentials and social are distinct methods with their own argument shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// email + password&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// social provider&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;social&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;signIn&lt;/code&gt;/&lt;code&gt;signOut&lt;/code&gt; &lt;em&gt;reference&lt;/em&gt; can be rewritten automatically, but the &lt;strong&gt;arguments cannot&lt;/strong&gt; — &lt;code&gt;("credentials", { ... })&lt;/code&gt; and &lt;code&gt;.email({ ... })&lt;/code&gt; are genuinely different APIs. Expect to revisit every sign-in call by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Environment variable renames
&lt;/h2&gt;

&lt;p&gt;The names change, and there are enough of them to be error-prone. Here's the full mapping:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NextAuth&lt;/th&gt;
&lt;th&gt;Better Auth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXTAUTH_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BETTER_AUTH_URL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GITHUB_CLIENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GITHUB_CLIENT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GOOGLE_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GOOGLE_CLIENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GOOGLE_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GOOGLE_CLIENT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing to be precise about: there are two places these names live — &lt;strong&gt;in code&lt;/strong&gt; (&lt;code&gt;process.env.NEXTAUTH_SECRET&lt;/code&gt;) and &lt;strong&gt;in your &lt;code&gt;.env&lt;/code&gt; file&lt;/strong&gt;. Renaming the code references is mechanical. Renaming the actual &lt;code&gt;.env&lt;/code&gt; entries is something you do yourself, because nothing should be reaching into your secrets file and editing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: The API route
&lt;/h2&gt;

&lt;p&gt;NextAuth mounts its handler at &lt;code&gt;[...nextauth]&lt;/code&gt;. Better Auth uses a catch-all &lt;code&gt;[...all]&lt;/code&gt; route wired to its handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — app/api/auth/[...nextauth]/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NextAuth&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// after — app/api/auth/[...all]/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toNextJsHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;better-auth/next-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toNextJsHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a file move plus a rewrite, not an in-place edit — the directory name itself changes from &lt;code&gt;[...nextauth]&lt;/code&gt; to &lt;code&gt;[...all]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Middleware → cookie check + server &lt;code&gt;requireSession&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;NextAuth's &lt;code&gt;withAuth&lt;/code&gt; middleware has no direct equivalent in Better Auth, and that's deliberate. The recommended pattern is two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A lightweight &lt;strong&gt;cookie check in middleware/proxy&lt;/strong&gt; for cheap edge-level redirects.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;authoritative server-side &lt;code&gt;requireSession()&lt;/code&gt;&lt;/strong&gt; in the protected route or layout, which actually validates the session.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth/middleware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// after — a cookie presence check at the edge, plus:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requireSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// authoritative, server-side&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge check is fast but only checks for a cookie's presence; the server check is the one you trust. Don't collapse them into one — the whole point is that the cheap check doesn't have authority and the authoritative check isn't on the hot edge path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config, the schema, the data — what you still do by hand
&lt;/h2&gt;

&lt;p&gt;Everything above is the boring, transformable part. Here's the part that isn't — and it's the part that actually decides whether the migration goes well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The auth config itself.&lt;/strong&gt; Your &lt;code&gt;authOptions&lt;/code&gt; / &lt;code&gt;NextAuth({})&lt;/code&gt; body doesn't translate one-to-one. You port it deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;providers: [...]&lt;/code&gt; → &lt;code&gt;socialProviders: { ... }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;your adapter → the matching Better Auth adapter (e.g. &lt;code&gt;drizzleAdapter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;callbacks: { ... }&lt;/code&gt; → &lt;code&gt;databaseHooks&lt;/code&gt; or plugin options&lt;/li&gt;
&lt;li&gt;email/password, verification, and reset are explicit features you enable, not implicit defaults
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the shape you're porting *to* (illustrative)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;drizzleAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;socialProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The database schema.&lt;/strong&gt; NextAuth's tables are not Better Auth's tables. You generate the Better Auth schema and migrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm auth:generate   &lt;span class="c"&gt;# produce the Better Auth schema&lt;/span&gt;
pnpm db:migrate      &lt;span class="c"&gt;# apply it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Existing user rows.&lt;/strong&gt; This is the awkward one. Better Auth won't move your users for you — you map columns from the old tables to the new ones yourself. Password hashes only carry over &lt;strong&gt;if the hashing scheme matches&lt;/strong&gt;; if it doesn't, those users need a password reset. Plan a real data-migration task here, not a script you run once and hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sane order to do this in
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;git commit&lt;/code&gt; or &lt;code&gt;stash&lt;/code&gt; first — you want a clean diff to review.&lt;/li&gt;
&lt;li&gt;Rewrite the call sites (imports, &lt;code&gt;getServerSession&lt;/code&gt;, &lt;code&gt;useSession&lt;/code&gt;, sign-in references).&lt;/li&gt;
&lt;li&gt;Port the config, generate the schema, run the migration.&lt;/li&gt;
&lt;li&gt;Fix the sign-in/sign-out &lt;strong&gt;arguments&lt;/strong&gt; by hand.&lt;/li&gt;
&lt;li&gt;Move the API route to &lt;code&gt;[...all]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rebuild middleware as cookie-check + &lt;code&gt;requireSession&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rename &lt;code&gt;.env&lt;/code&gt; entries.&lt;/li&gt;
&lt;li&gt;Verify: run your typecheck, then &lt;code&gt;pnpm auth:generate &amp;amp;&amp;amp; pnpm db:migrate&lt;/code&gt;, then &lt;code&gt;pnpm dev&lt;/code&gt; and click through the actual flows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing to expect: even though the call-site rewrites are the most repetitive change, most of your &lt;em&gt;time&lt;/em&gt; will go into the config, schema, and data — that's normal, and that's where to be careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure: I build a starter that automates the mechanical part
&lt;/h2&gt;

&lt;p&gt;I make &lt;strong&gt;&lt;a href="https://betterauth.app/" rel="noopener noreferrer"&gt;Ship Kit&lt;/a&gt;&lt;/strong&gt;, a commercial Better Auth starter for Next.js 16 + TypeScript. (It's an independent, unofficial project — not affiliated with or endorsed by Better Auth.) I sell it, so treat this section as the ad it is.&lt;/p&gt;

&lt;p&gt;Those migration codemods are &lt;strong&gt;free and open source&lt;/strong&gt; — &lt;a href="https://github.com/authlayerdev/ship-kit-migrate" rel="noopener noreferrer"&gt;&lt;code&gt;authlayerdev/ship-kit-migrate&lt;/code&gt;&lt;/a&gt; (MIT), deterministic and non-AI. Clone the repo and run the NextAuth transform against your project, dry-run first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# preview every change — writes nothing&lt;/span&gt;
node ./migrate/cli.mjs &lt;span class="nt"&gt;--transform&lt;/span&gt; nextauth &lt;span class="nt"&gt;--dry&lt;/span&gt; ./src
&lt;span class="c"&gt;# apply it (rewrites files in place)&lt;/span&gt;
node ./migrate/cli.mjs &lt;span class="nt"&gt;--transform&lt;/span&gt; nextauth ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's idempotent (running it twice is safe), but commit or stash first regardless.&lt;/p&gt;

&lt;p&gt;What it does: rewrites the imports, turns &lt;code&gt;getServerSession(authOptions)&lt;/code&gt; into &lt;code&gt;auth.api.getSession({ headers: await headers() })&lt;/code&gt;, swaps &lt;code&gt;useSession()&lt;/code&gt; for &lt;code&gt;authClient.useSession()&lt;/code&gt;, repoints &lt;code&gt;signIn&lt;/code&gt;/&lt;code&gt;signOut&lt;/code&gt; references (leaving a &lt;code&gt;// TODO(ship-kit):&lt;/code&gt; marker because the arguments differ), renames the in-code env references, and annotates your config block with a TODO.&lt;/p&gt;

&lt;p&gt;What it explicitly does &lt;strong&gt;not&lt;/strong&gt; do — and leaves marked so you can find the remainder with one grep — is the genuinely manual work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"TODO(ship-kit)"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# the manual checklist the codemod left you&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…that remainder being: porting the config body, the sign-in argument shapes, the DB schema, the user-row migration, the API route move, the &lt;code&gt;.env&lt;/code&gt; file itself, and middleware. In other words, exactly the "by hand" section above. It's an &lt;strong&gt;assist for the mechanical part, not a one-click migrator&lt;/strong&gt; — the parts that need judgment stay yours.&lt;/p&gt;

&lt;p&gt;For full context: Ship Kit is one-time ($179 solo / $499 agency, lifetime updates, 14-day refund, crypto −5%), built by one developer in Berlin. It's brand new, so there are no buyer testimonials yet. More established starters like Makerkit (from $349) and supastarter (from €349) have more templates and maturity; Ship Kit's narrower distinctions are the in-product migration codemods (most rivals are docs-only) and crypto checkout. It also runs a nightly canary CI job that bumps Better Auth to the latest version and re-runs the test suite — that's a signal it tracks upstream, not a guarantee that any given upgrade will be painless.&lt;/p&gt;

&lt;p&gt;If you'd rather not hand-write the repetitive call-site rewrites, the codemod handles that part. The config, schema, and data migration are still yours to do.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm the author of Ship Kit, a commercial product mentioned above, and I earn revenue from its sales.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>betterauth</category>
      <category>nextauth</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
