Migrating from Better Auth to WorkOS AuthKit: A Next.js guide
The code side of the move: routes, session management, reading the user, and step up auth in a Next.js App Router app.
If you have decided to move off Better Auth, the data half of the migration is covered separately in what the Vercel acquisition changes and how to migrate to WorkOS, which walks through exporting users, passwords, and organizations and handling the cutover.
This guide is the other half: the code. If you are on Better Auth with a Next.js App Router app, here is how to wire in AuthKit.
Before you start
Two prerequisites that are easy to miss:
- Next.js App Router. The library is built for it and is not intended for the Pages Router.
- Node 22.11.0 or later. The package declares it in
engines, so an older runtime fails at install rather than at runtime.
Step 1: Install AuthKit
AuthKit ships a first-party Next.js library. Install it alongside the WorkOS Node SDK, which is a peer dependency:
Stay on the 4.x line. Check npm for the current release rather than pinning to a version you read in a blog post.
Step 2: Configure environment variables
Grab your client ID and API key from the WorkOS dashboard and add four values to .env.local:
WORKOS_COOKIE_PASSWORD is the private key that encrypts the session cookie. It has to be at least 32 characters. Generate one with openssl:
If you are coming from Better Auth, this is the mental shift. Instead of configuring a database adapter and running auth tables yourself, you point at a hosted service and let it hold the session.
Step 3: Add the callback and sign-in routes
WorkOS redirects users back to your app after they authenticate. Create a callback route handler that matches your NEXT_PUBLIC_WORKOS_REDIRECT_URI. For the value above, that is app/callback/route.ts:
You can send users to a specific page after sign-in with returnPathname:
Then add a route that starts the sign-in flow, and set it as the sign-in URL in the dashboard under Redirects:
Do not skip the dashboard step. The sign-in URL is what lets WorkOS-initiated flows, impersonation among them, route through your app first so the PKCE and CSRF checks can be set up. Without it those flows fail with a missing auth parameter error.
Step 4: Wire up session management
AuthKit uses Next.js proxy, which was called middleware in Next.js 15 and earlier. For Next.js 16 and later, create proxy.ts in the project root. For Next.js 15 and earlier, create middleware.ts:
The matcher array is where you declare which routes require auth, the direct analog to the route protection you were doing by hand.
One trap worth knowing before you reach for a catch-all pattern: a broad matcher intercepts static assets, which breaks styles, and Tailwind v4 is particularly prone to it. If you need broad coverage, exclude the Next.js static paths:
Step 5: Read the user
Wrap your root layout in AuthKitProvider so client components can reach auth state:
In a server component, withAuth() returns the current user or null:
In a client component, use useAuth(). Note the import path, because this one catches people out:
Client-side helpers come from @workos-inc/authkit-nextjs/components, not the package root. Importing withAuth into a client component instead of using useAuth produces an UnhandledSchemeError on node:crypto, which is a confusing error for a simple mistake.
For pages where a signed-in user is mandatory, pass ensureSignedIn and AuthKit redirects unauthenticated visitors for you:
That replaces the getSession checks and manual redirects you wrote against Better Auth. One caveat: do not wrap it in a try/catch, because withAuth redirects, and Next.js requires redirects to be called outside try/catch.
Step 6: Move your users over
The routing swap is the easy part. Moving live users without forcing a mass password reset is the part that needs a plan, and it is covered in full in the Better Auth migration guide and the companion post: exporting from your Better Auth tables, importing users and scrypt password hashes, moving organizations and memberships, and choosing between pausing signups or dual-writing during the cutover.
What belongs here is the hook that makes reconciliation possible. handleAuth accepts an onSuccess function that receives the authenticated user along with oauthTokens, authenticationMethod, organizationId, and any state you passed through the flow, so you can match a WorkOS user against your existing records on first login:
authenticationMethod is only present on the initial authentication callback, not on later requests or session refreshes, so do any recording of it here.
What you get past the swap
The point of moving is not a different sign-in button. It is that a category of work becomes configuration instead of a project. Enterprise SSO and directory sync become dashboard settings rather than sprints.
Step up auth is a good example of something you would otherwise build. checkRecentAuth reads the access token's auth_time claim and tells you whether the user authenticated recently. It returns data and never redirects, so it is safe to call as the enforcement step inside a server action, and it fails closed: a session with no usable auth_time is reported as stale.
Passing maxAge to getSignInUrl forwards the OIDC max_age parameter, so AuthKit forces a fresh login rather than accepting the existing session. There is a useRecentAuth hook for reflecting staleness in the UI, but treat it as presentation only and always enforce on the server.
Wrapping up
Better Auth earned its reputation. It went from first release in September 2024 to 1.0 by that November, took over Auth.js along the way, and is heading somewhere interesting with Vercel. If owning your auth stack is a feature for your team, that is a reasonable place to be.
If your app is past the prototype stage and auth has become load-bearing for revenue, the six steps above are the whole code migration. Start with the WorkOS dashboard and the authkit-nextjs library, and use the migration guide for the data.