Blog
October 6, 20265 min readIbrahim Elkamali

Better Auth vs Clerk vs Auth.js: decide who owns your users

Compare Better Auth, Clerk, and Auth.js by user data ownership, hosted dependencies, framework fit, and what a generated Next.js project actually wires.

better-authclerkauthjsauthentication
Markdown

Stack decision map: choose an ecosystem, generate, inspect, and keep the code

Better Auth, Clerk, and Auth.js can all sign users into a Next.js application. They differ on a question that is expensive to reverse: who stores the user records, and who runs the code that issues sessions.

Better Auth and Auth.js are libraries that run inside your application. In the generated projects, both write user records to your database; the Better Auth option also stores sessions there, while the Auth.js option keeps sessions in encrypted JWT cookies (JWE, Auth.js's default). Both libraries also support stateless sessions that need no database. Clerk is a hosted user management service that your application calls. Start from that difference, then check framework support and maintenance status.

What you own

Better Auth describes itself as a framework-agnostic authentication and authorization framework for TypeScript. It stores data in your database through adapters such as Drizzle, Prisma, and Kysely, and offers features such as two-factor authentication, passkeys, and multi-tenancy, with a plugin system for more.

Clerk provides authentication and user management as a service, with prebuilt UI components such as <SignIn /> and <UserButton />. User records live in Clerk. Clerk's docs describe syncing Clerk user data to your own database for cases such as showing other users' profiles, and recommend reading data from the session token instead when that is enough.

Auth.js is an open source library that runs in your application and supports OAuth, magic links, credentials, and WebAuthn, with optional database adapters for Drizzle, Prisma, and others.

The trade is concrete. With a library, you run the auth endpoints yourself. When users live in your database, as in the generated Better Auth and Auth.js projects, you store and operate those tables too. With Clerk, those parts move to a vendor, and your app depends on Clerk's API keys and service for every sign-in.

Maintenance status

Auth.js documentation now states that the Auth.js project is part of Better Auth. The announcement from September 22, 2025 says existing Auth.js and NextAuth.js users can continue without disruption, with security patches and urgent issues still addressed, and strongly recommends that new projects start with Better Auth unless they hit specific feature gaps. Weigh that when starting a new project on Auth.js.

Framework and deployment fit

Better Auth is framework-agnostic and runs inside your own server code. Clerk publishes SDKs for many frontend and backend frameworks. Auth.js lists Next.js, SvelteKit, Express, and Qwik integrations.

Better Fullstack's options are narrower than the libraries themselves:

Option--auth valueWhere Better Fullstack generates it
Better Authbetter-authFullstack frameworks and separate backends such as Hono, Express, Fastify
ClerkclerkFullstack Next.js, fullstack TanStack Start, or Convex
Auth.jsnextauthFullstack Next.js only

Fullstack Next.js is the one setup all three share, so the commands below use it.

What Better Fullstack generates

All three commands use Next.js with its own backend, PostgreSQL, Drizzle, and tRPC. Only --auth changes.

bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend next \
  --backend self \
  --runtime none \
  --database postgres \
  --orm drizzle \
  --api trpc \
  --auth better-auth

Better Auth generates a packages/auth package that configures betterAuth with the Drizzle adapter, email and password sign-in, and the nextCookies plugin. The user, session, account, and verification tables live in packages/db/src/schema/auth.ts, and app/api/auth/[...all]/route.ts mounts the handler. The tRPC context reads the session with auth.api.getSession, so protectedProcedure rejects unauthenticated calls.

bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend next \
  --backend self \
  --runtime none \
  --database postgres \
  --orm drizzle \
  --api trpc \
  --auth clerk

Clerk generates ClerkProvider in the root layout, clerkMiddleware in src/middleware.ts, SignInButton and UserButton in the user menu, and a dashboard page that checks userId from auth(). The environment file expects NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY. No auth tables are added to the database schema. The middleware also runs for API routes, and the generated tRPC context reads userId from Clerk's auth(), so protectedProcedure rejects calls without a signed-in user.

bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend next \
  --backend self \
  --runtime none \
  --database postgres \
  --orm drizzle \
  --api trpc \
  --auth nextauth

The Auth.js option generates a project on next-auth 5, pinned to an exact beta release, with a NextAuth configuration in packages/auth and a /login page. src/proxy.ts redirects signed-out page requests to /login and answers 401 for API routes that do not check auth themselves. The Drizzle adapter is configured for users, accounts, sessions, and verification tokens, but the JWT session strategy keeps sessions in an encrypted cookie, so the database holds users and OAuth accounts. GitHub and Google providers read their credentials from the environment. The credentials provider checks the email and password against a bcrypt hash in the users table, and the sign-up form posts to /api/auth/register, which validates the input and stores that hash. The generated tRPC context reads the session with auth(), so protectedProcedure rejects unauthenticated calls.

These are generated projects. Configure real keys and callback URLs, then sign in end to end before depending on any of them.

Decision table

If this is the stronger constraintStart with
User tables in your own database, auth code in your repoBetter Auth
Hosted user management and prebuilt UI, vendor dependency is OKClerk
Existing Auth.js or NextAuth.js app that worksKeep Auth.js; track its security support
Separate API server such as Hono or ExpressBetter Auth, among these three
Sessions stored as rows in your own databaseBetter Auth

Compare real generated stacks

For where sessions should live once you pick a library, read Better Auth architecture. The Next.js Drizzle Better Auth guide walks through one complete project.

GitHub Sponsors