Skip to content

Better Auth vs Clerk: why we chose Better Auth

For CTOs and engineersCTO decisions · Updated

Filip PapranecFounder, Digitaliko

Better Auth vs Clerk. Illustration: a users table inside your own database on the left, a hosted user directory behind an API on the right, with the question of where your sessions live.Better Auth vs Clerk. Illustration: a users table inside your own database on the left, a hosted user directory behind an API on the right, with the question of where your sessions live.

Better Auth vs Clerk comes down to one question: where do your users and sessions live? Clerk keeps them in its hosted service. Better Auth writes them into your own database, as plain tables next to the rest of your app.

We moved from NextAuth to Better Auth in every new app and never picked Clerk. Honest limit first: we have not run Clerk in production. So this is not a benchmark. It is why we did not choose it, with the configs we actually run.

What is the difference between Better Auth and Clerk?

Better Auth is a library. It describes itself as "a framework-agnostic, universal authentication and authorization framework for TypeScript" (Better Auth docs, as of 2026-10-24). It is MIT licensed. Clerk is a hosted service with prebuilt sign-in components and a user dashboard.

Better AuthClerk
What it isTypeScript library in your repositoryHosted auth service with UI components
Where users and sessions liveTables in your databaseClerk's infrastructure
PriceMIT, you pay your own database and emailFree up to 50,000 monthly retained users per app, then paid (pricing, as of 2026-10-24)
Sign-in UIYou build itPrebuilt components
Security patchesYou update the package and releaseClerk ships them
Verification and magic link emailsYour email serviceClerk sends them
LeavingNothing to leave, it is your schemaUser export with hashed passwords, then rewire UI and session checks

The rows that decided it for us are the second and the third. Our clients own their code, data and infrastructure from day one. A user table on someone else's servers, billed per user, does not fit that.

Why did we move from NextAuth to Better Auth?

Our older client apps run NextAuth v4 with the Prisma adapter. They still run, and we leave them alone. Auth that works is not something we rewrite for a blog post.

For new apps the decision got easier in September 2025. Auth.js, the project formerly called NextAuth.js, moved under the Better Auth team. Their announcement says it plainly:

We strongly recommend new projects to start with Better Auth unless there are some very specific feature gaps.

Bereket Engida
Better Auth, Auth.js is now part of Better Auth (Sep 22, 2025)

The gap they name is stateless sessions without a database. We want the database anyway, so it never applied to us.

What does Better Auth look like in a real app?

This is the auth package from our starter template, slightly shortened. TanStack Start on Cloudflare Workers, Drizzle on D1:

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { checkout, polar, portal } from '@polar-sh/better-auth'

export function createAuth() {
  const db = createDb()
  const plugins = [tanstackStartCookies()]

  // Billing only when the app has a payments token.
  if (env.POLAR_ACCESS_TOKEN) {
    plugins.push(polar({ client: polarClient, createCustomerOnSignUp: true, use: [checkout({ /* products */ }), portal()] }))
  }

  return betterAuth({
    database: drizzleAdapter(db, { provider: 'sqlite', schema }),
    trustedOrigins: [env.CORS_ORIGIN],
    emailAndPassword: { enabled: true },
    socialProviders: { /* Google and GitHub, only when their keys are set */ },
    secret: env.BETTER_AUTH_SECRET,
    baseURL: env.BETTER_AUTH_URL,
    plugins,
  })
}

The schema is ours too. Users and sessions are Drizzle tables like any other:

const user = sqliteTable('user', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
})

const session = sqliteTable('session', {
  id: text('id').primaryKey(),
  token: text('token').notNull().unique(),
  expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
  userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
})
Better Auth vs Clerk as two windows. Left, Better Auth: the user, session and account tables sit in your own database next to your app tables, and plugins like passkeys, two-factor and organizations run as code in your repository. Right, Clerk: a hosted user directory with prebuilt sign-in UI, billed per monthly retained user, and your app talks to it over an API.Better Auth vs Clerk as two windows. Left, Better Auth: the user, session and account tables sit in your own database next to your app tables, and plugins like passkeys, two-factor and organizations run as code in your repository. Right, Clerk: a hosted user directory with prebuilt sign-in UI, billed per monthly retained user, and your app talks to it over an API.

That is the practical difference. A record created by a user is a foreign key to user.id in the same database. A report of "who did what" is one SQL join, not a call to an external API plus a sync job.

How does auth become part of your own code?

Our internal cockpit app shows how far this goes. Only a short list of people may sign up, all through GitHub. No auth vendor setting does that. It is a few lines in a database hook:

return betterAuth({
  database: drizzleAdapter(db, { provider: 'sqlite', schema }),
  emailAndPassword: { enabled: false },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          if (allowedEmails.length > 0 && !allowedEmails.includes(user.email.toLowerCase())) return false
        },
      },
    },
  },
  socialProviders: { github: { clientId, clientSecret, scope: ['repo', 'read:user', 'user:email'] } },
})

What happens on a sign-up:

  1. GitHub sign-in

    The person signs in with GitHub. The app asks for repo access, because the cockpit reads issues and pull requests.

  2. The hook runs before the insert

    databaseHooks.user.create.before gets the new user before it is written.

  3. Not on the list: no user

    Returning false stops the insert. There is no user row and no session.

  4. On the list: a session row

    The user and the session land in the app's own D1 database, readable with any SQL client.

The allowlist is an environment variable. Changing it is a deploy, reviewed like any other change. With a hosted service, the same rule would live in a dashboard or a webhook outside the repository.

Which Better Auth plugins do we run in production?

Each plugin is code in the repository, so the list is easy to check:

  • Our starter template: tanstackStartCookies and the Polar billing plugin (checkout and customer portal, a customer created on sign-up). Polar is only added when the token exists.
  • A client web app: Better Auth inside Payload CMS, with admin, organization, twoFactor, emailOTP, magicLink, multiSession and the passkey plugin.
  • A client's online marketplace: a separate auth service on MongoDB with magicLink and oidcProvider. The marketplace's other apps sign in through it, like through any OpenID Connect provider.

Three databases, three setups, one library. That is the part we value most: the same library fits SQLite on the edge, Postgres behind a CMS and MongoDB in a separate service.

When is Clerk the better choice?

Clerk makes sense when you want sign-in done this week and nobody on the team should own auth code. Prebuilt components save you the screens. Clerk sends the emails and patches the service. For a prototype, the free plan is generous: 50,000 monthly retained users per app, Pro at $25 a month, and $0.02 per user from 50,001 to 100,000 (Clerk pricing, as of 2026-10-24).

Lock-in is smaller than people think. Clerk lets admins export users, including hashed passwords, as a CSV (Clerk docs, as of 2026-10-24). The data is not held hostage. What takes work is everything wired to it: sign-in UI, session checks on the server and any user data you copied into your own tables.

One more fact worth weighing. Better Auth joined Vercel on July 7, 2026. Their post says: "Vercel shares our commitment to keeping auth open source, framework and platform agnostic." (Better Auth blog). The MIT license stays what it is. We note it the way we note any owner change of a dependency we rely on.

What do you take on when you own auth?

Owning auth is not free of work. It moves the work to your team. Before you pick Better Auth, go through this list:

The last point is the cheapest insurance. Our starter runs Playwright tests for exactly that: the login form renders, sign-up works, and the dashboard redirects to login without a session.

How we expose app data to AI agents on top of this auth is covered in How to build an MCP server, and when you should not.

Need auth that stays in your database?

We build web apps where users, sessions and permissions live in the client's own database, as part of custom web application development. Code, data and infrastructure belong to the client from day one. AI writes all of our code today, and a senior is accountable for the result. Since 2023 we have delivered 15+ production projects.

We get back to you within two business days, and together we pick what to fix first.

If you'd rather talk first, book a 15-minute meeting with Filip.

Where do your users live today, and could you query them with one SQL join?

In short

  • Better Auth stores users and sessions in your database. Clerk stores them in its hosted service.
  • We moved from NextAuth to Better Auth for new apps and keep working NextAuth apps as they are.
  • With Better Auth, rules like an email allowlist are a few lines in your repository.
  • Clerk fits when nobody should own auth code. You can export users later, but rewiring takes work.

Frequently asked questions

What is the difference between Better Auth and Clerk?

Better Auth is an open source TypeScript library that stores users and sessions in your own database. Clerk is a hosted service that stores them for you and gives you prebuilt sign-in UI. The trade is control and no per-user bill versus less code to own.

Is Better Auth free?

Yes. Better Auth is MIT licensed. You pay for what it runs on: your database, your hosting and the email service that sends verification links and magic links.

How much does Clerk cost?

As of 2026-10-24, the Clerk Hobby plan is free with a limit of 50,000 monthly retained users per app, Pro is $25 a month ($20 billed annually), and users 50,001 to 100,000 cost $0.02 a month each. Check clerk.com/pricing for the current numbers.

Should I migrate from NextAuth to Better Auth?

For new projects, yes: Auth.js is now maintained by the Better Auth team, and they recommend Better Auth for new work. For a NextAuth app that works, we do not migrate just for the sake of it. We switch when the app needs a feature NextAuth does not have.

Can I leave Clerk later?

Yes. Clerk lets admins export users, including hashed passwords, as CSV from the dashboard. What takes work is the rest: sign-in UI, session checks and any user data you synced into your own tables.

Ask AI about this article

The AI also draws on our other articles and experience from real projects.

ChatGPTClaudePerplexity
Engineering

Other articles

CTO decisions

Claude Code pricing for a team: plans, API and agents

Claude Code pricing from Anthropic's public list (October 2026): Pro, Max, Team seats, Enterprise and API tokens. Which one fits developers, which one an always-on agent and a CI reviewer run on, and the guardrails that stop a background agent from burning the budget.

CTO decisions

Vercel alternatives: why we run Vercel and Cloudflare

We did not replace Vercel, we split the work. Next.js sites stay on Vercel, new apps run on Cloudflare Workers and D1, declared in TypeScript with Alchemy. Pricing with sources, our config, and the deploy block an AI agent hit on Vercel.

CTO decisions

Drizzle vs Prisma: why we run both in production

Drizzle vs Prisma from two ORMs we run in production: Prisma on Postgres in older client apps, Drizzle on Cloudflare D1 in everything new. Real config, the migration flow, what Prisma 7 changed and when we would not switch.

Time to stop planning and start solving.

Contact

  • Digitaliko s.r.o.
    Šípková 377/17
    958 03 Malé Uherce, Slovakia
  • Get in touch
    info@digitaliko.com
    +421 950 595 506
    WhatsApp