<?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>Moving off Clerk to Better Auth: what's mechanical and what's a rebuild</title>
      <dc:creator>Ship Kit</dc:creator>
      <pubDate>Mon, 12 Oct 2026 23:51:57 +0000</pubDate>
      <link>https://dev.to/authlayerdev/moving-off-clerk-to-better-auth-whats-mechanical-and-whats-a-rebuild-kgp</link>
      <guid>https://dev.to/authlayerdev/moving-off-clerk-to-better-auth-whats-mechanical-and-whats-a-rebuild-kgp</guid>
      <description>&lt;p&gt;I've done a fair number of NextAuth to Better Auth moves, and they're mostly mechanical: same general shape, swap the library, fix a few call sites. Clerk is a different animal, and you should know that going in.&lt;/p&gt;

&lt;p&gt;NextAuth is a library. Clerk is a hosted product: pre-built React components plus a hosted user store that lives on Clerk's servers. When you move off Clerk you're not swapping one auth library for another — you're replacing UI you never wrote, re-implementing session logic Clerk did for you, and migrating user records out of someone else's database. The import rewrites are find-and-replace. The rest is a rebuild. This post walks the whole thing so you can scope it honestly before you start.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The two halves of a Clerk migration
&lt;/h2&gt;

&lt;p&gt;Split the work into two buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mechanical&lt;/strong&gt; — imports and session calls. A codemod can handle the textual part and flag the spots where the return shapes don't line up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild&lt;/strong&gt; — UI components, middleware, the auth config, and the user-data export. None of this is find-and-replace, because Clerk did it for you and Better Auth expects you to own it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's do the mechanical half first, because it's the part that looks like a library swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session calls: same intent, different shape
&lt;/h2&gt;

&lt;p&gt;Clerk gives you &lt;code&gt;auth()&lt;/code&gt; and &lt;code&gt;currentUser()&lt;/code&gt; on the server, and &lt;code&gt;useAuth()&lt;/code&gt; / &lt;code&gt;useUser()&lt;/code&gt; on the client. Better Auth has one server call and one client hook. A codemod can rewrite the calls, but the &lt;strong&gt;return shapes are different&lt;/strong&gt;, and that's the part you have to fix by hand.&lt;/p&gt;

&lt;p&gt;Here's a typical server component before:&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="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="nx"&gt;currentUser&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;@clerk/nextjs/server&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="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&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;userId&lt;/span&gt; &lt;span class="p"&gt;}&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;auth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sign-in&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;user&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;currentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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 shape you're moving to is different. Better Auth's &lt;code&gt;getSession&lt;/code&gt; returns &lt;code&gt;{ user, session }&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; — there is no &lt;code&gt;userId&lt;/code&gt; at the top level. A naive rewrite of the destructure would compile and be subtly wrong, so the safe approach is to leave a &lt;code&gt;TODO&lt;/code&gt; at every rewritten call rather than guess a "plausible" shape:&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="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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO(better-auth): Clerk auth() returned { userId }; getSession returns&lt;/span&gt;
  &lt;span class="c1"&gt;// { user, session } | null. Destructure from session.user, not userId.&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;userId&lt;/span&gt; &lt;span class="p"&gt;}&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sign-in&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;user&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;currentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// TODO(better-auth): fold into the session above&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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 point of leaving the broken destructure next to a TODO is that it fails loudly instead of compiling into something quietly wrong. The corrected version a human writes:&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="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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&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;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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sign-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call now covers both &lt;code&gt;auth()&lt;/code&gt; and &lt;code&gt;currentUser()&lt;/code&gt; — the user object is already on the session, so the separate &lt;code&gt;currentUser()&lt;/code&gt; round-trip goes away.&lt;/p&gt;

&lt;p&gt;The client side is the same idea. Before:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;useUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useAuth&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;@clerk/nextjs&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;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&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;isSignedIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isSignedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the call rewrite, with the shape fix still owed to you:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO(better-auth): useSession() returns { data, isPending, error }.&lt;/span&gt;
  &lt;span class="c1"&gt;// data is { user, session } | null. Replace isSignedIn / user accordingly.&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;isSignedIn&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;code&gt;authClient.useSession()&lt;/code&gt; returns &lt;code&gt;{ data, isPending, error }&lt;/code&gt;, and &lt;code&gt;data&lt;/code&gt; is your &lt;code&gt;{ user, session }&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt;. The human-fixed version:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So that's the mechanical half: drop the &lt;code&gt;@clerk/*&lt;/code&gt; imports, point them at &lt;code&gt;@/lib/auth&lt;/code&gt;, &lt;code&gt;next/headers&lt;/code&gt;, and &lt;code&gt;@/lib/auth-client&lt;/code&gt;, and rewrite the call sites. A codemod can do the textual part, but it can't make the shapes match — that's on you, which is why every rewritten call is worth marking with a TODO before you move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rebuild half: UI components
&lt;/h2&gt;

&lt;p&gt;This is where Clerk-being-a-product hurts. Clerk ships React components that render entire flows. Better Auth ships none of that — it's headless. So every Clerk component is a TODO, not a rewrite. A codemod can remove the component imports and note what it pulled, but it cannot generate UI for you.&lt;/p&gt;

&lt;p&gt;What you have to replace by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;ClerkProvider&amp;gt;&lt;/code&gt;&lt;/strong&gt; — delete it. Better Auth has no client-side provider to wrap your app in; the session hook works without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;SignedIn&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;SignedOut&amp;gt;&lt;/code&gt;&lt;/strong&gt; — these gated children on auth state. Replace with an actual session check: read the session (server) or &lt;code&gt;useSession()&lt;/code&gt; (client) and branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;UserButton&amp;gt;&lt;/code&gt;&lt;/strong&gt; — the avatar dropdown with sign-out. You build this: an account menu and a sign-out button calling &lt;code&gt;authClient.signOut()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;SignIn&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;SignUp&amp;gt;&lt;/code&gt;&lt;/strong&gt; — the hosted forms. These become real routes and real components — an &lt;code&gt;(auth)&lt;/code&gt; route group plus form components under &lt;code&gt;src/components/auth/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;Protect&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;RedirectToSignIn&amp;gt;&lt;/code&gt;&lt;/strong&gt; — replace with your own session-gating and redirects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no shortcut here. If you were leaning on Clerk's components, budget real time to build sign-in, sign-up, and the account menu as first-class UI. A starter that already ships these (more on that at the end) saves you most of this, but the work itself is genuine UI work either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware
&lt;/h2&gt;

&lt;p&gt;Clerk's &lt;code&gt;clerkMiddleware()&lt;/code&gt; (formerly &lt;code&gt;authMiddleware()&lt;/code&gt;) ran auth on the edge for you. This is a rewrite, not a rename — there's no clean textual mapping. You replace it with an edge check plus an authoritative server check.&lt;/p&gt;

&lt;p&gt;On Next.js 16 the file is &lt;code&gt;src/proxy.ts&lt;/code&gt;; on Next.js 15 and below it's &lt;code&gt;middleware.ts&lt;/code&gt;. The pattern is a cheap cookie check at the edge, then a real &lt;code&gt;requireSession&lt;/code&gt; call in the server code that actually loads protected data. The edge check keeps unauthenticated traffic out early; the server check is the one that's authoritative. Don't rely on the edge check alone for anything that matters — it only inspects the cookie, it doesn't validate the session against the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The auth config and the API route
&lt;/h2&gt;

&lt;p&gt;Two more things a codemod can't write for you, because there's nothing in your Clerk code to translate from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;betterAuth({})&lt;/code&gt;&lt;/strong&gt; — the server config: your database adapter, which providers you enable, session options. This didn't exist under Clerk (Clerk &lt;em&gt;was&lt;/em&gt; the config, hosted), so it's net-new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;[...all]&lt;/code&gt; API route&lt;/strong&gt; — Better Auth handles its own endpoints through a catch-all route handler. Clerk routed to its own servers, so again, net-new.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Environment variables
&lt;/h2&gt;

&lt;p&gt;Don't reuse anything. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generate a fresh &lt;code&gt;BETTER_AUTH_SECRET&lt;/code&gt;.&lt;/strong&gt; Do not reuse &lt;code&gt;CLERK_SECRET_KEY&lt;/code&gt; — it's a different key for a different system. Generate a new secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the &lt;code&gt;NEXT_PUBLIC_CLERK_*&lt;/code&gt; routing vars&lt;/strong&gt; — &lt;code&gt;NEXT_PUBLIC_CLERK_SIGN_IN_URL&lt;/code&gt; and friends. Those pointed at Clerk's hosted flows; your routes live in your app now, so they're dead config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cleaning up &lt;code&gt;.env&lt;/code&gt; is on you regardless of tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: user data
&lt;/h2&gt;

&lt;p&gt;Your users live in Clerk's database, not yours. Moving off Clerk means a real data migration, and this is usually the line item people forget to scope.&lt;/p&gt;

&lt;p&gt;The shape of it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export users from Clerk&lt;/strong&gt; (their API / dashboard export).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import them into your &lt;code&gt;user&lt;/code&gt; table&lt;/strong&gt; so Better Auth can read them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot carry over password hashes.&lt;/strong&gt; Clerk doesn't hand you usable password hashes, so password users have to go through a &lt;strong&gt;password reset&lt;/strong&gt; on first login. OAuth users (GitHub, Google) re-onboard through the social flow — their identity is the provider, so they just sign in again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plan the comms for this. If you have password users, they'll all need a reset email or a "set a new password" prompt the first time they show up after the cutover. That's a product decision, not just a code one, and it's the single biggest reason a Clerk migration isn't a weekend job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoping it honestly
&lt;/h2&gt;

&lt;p&gt;Mechanical: imports and session calls (with the shape fixes you do by hand). Rebuild: every Clerk component, middleware, the &lt;code&gt;betterAuth({})&lt;/code&gt; config, the &lt;code&gt;[...all]&lt;/code&gt; route, the &lt;code&gt;.env&lt;/code&gt; cleanup, and the user-data export. Expect &lt;strong&gt;more manual work than a NextAuth migration&lt;/strong&gt; — NextAuth is a library-to-library move; Clerk is a product-to-library move, and the product included a lot you now own.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;&lt;a href="https://betterauth.app/" rel="noopener noreferrer"&gt;Ship Kit&lt;/a&gt;&lt;/strong&gt; (betterauth.app), a production-ready Better Auth starter for Next.js 16 + TypeScript. Its 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 (Clerk and NextAuth). Clone the repo and run the Clerk 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; clerk &lt;span class="nt"&gt;--dry&lt;/span&gt; ./src
&lt;span class="c"&gt;# apply it&lt;/span&gt;
node ./migrate/cli.mjs &lt;span class="nt"&gt;--transform&lt;/span&gt; clerk ./src
&lt;span class="c"&gt;# then find the manual remainder it marked for you:&lt;/span&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being straight about what they do: they're an &lt;strong&gt;assist, not a one-click migrator&lt;/strong&gt;. They delete your Clerk wiring and mark the spots that need a human — the import and session-call rewrites, plus &lt;code&gt;// TODO(ship-kit):&lt;/code&gt; markers on the shape mismatches and on removed component imports (and they intentionally leave a wrong destructure like &lt;code&gt;const { userId } = …&lt;/code&gt; in place so the TODO forces you to fix the session read, never a silently-compiling-but-wrong one). They do &lt;strong&gt;not&lt;/strong&gt; recreate Clerk's components, middleware, or config for you, and a Clerk migration is &lt;strong&gt;more manual than the NextAuth one&lt;/strong&gt; for exactly the product-vs-library reason above. The shape fixes are still yours to make.&lt;/p&gt;

&lt;p&gt;What the kit gives you on the other side is the UI and wiring you'd otherwise hand-build: &lt;code&gt;(auth)&lt;/code&gt; routes and components, the account menu and sign-out, two-layer route protection (edge cookie check + server &lt;code&gt;requireSession&lt;/code&gt;), organizations/teams, and org-level Stripe billing. It's pinned to &lt;code&gt;better-auth 1.6.18&lt;/code&gt; with a nightly canary CI job that bumps Better Auth to latest and re-runs the test suite — that's a CI signal, not a promise that upgrades are painless. It's brand new, so there are no buyer testimonials yet; honest competitors like Makerkit (from $349) and supastarter (from EUR349) are more mature with more templates, and NEXTY (~$188) is cheaper. Ship Kit's distinct edges are the in-product migration codemods (rivals are docs-only) and crypto checkout. Solo is $179, Agency $499 — one-time (not a subscription), lifetime + free updates, crypto -5%, 14-day refund.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Ship Kit is my commercial product and I earn money when you buy it. It is independent and unofficial — not affiliated with or endorsed by Better Auth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>authentication</category>
      <category>typescript</category>
    </item>
    <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>
