Authentication is the part of a product everyone underestimates: sessions, token rotation, account linking, rate limiting, two-factor, and a dozen edge cases that only surface in production. The usual answer is a hosted provider, which trades that work for a monthly bill, a vendor-shaped user table and an integration you cannot read. Better Auth takes the opposite route: a framework-agnostic TypeScript library that puts the whole auth layer in your repository, against your own database, with types flowing from the server config all the way to the client call.
A single server config
Everything starts with one betterAuth call that declares the database adapter and the methods you accept. There is no hosted dashboard mirroring this configuration and no drift between the two, because the object below is the only source of truth:
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { prisma } from '@/lib/prisma';
export const auth = betterAuth({
appName: 'Rewind',
database: prismaAdapter(prisma, { provider: 'postgresql' }),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
},
},
});
The adapter writes to the schema you already own, so users, sessions and accounts are ordinary tables you can join against. That single detail removes the most common friction of hosted auth, where every query about a user crosses a network boundary you do not control.
The client is generated from the server
The browser side is created by createAuthClient, and it is deliberately thin. What makes it interesting is the type parameter: passing the server instance as a type gives the client full knowledge of what the server accepts, including anything a plugin added:
import { createAuthClient } from 'better-auth/react';
import { twoFactorClient } from 'better-auth/client/plugins';
import type { auth } from '@/lib/auth';
export const authClient = createAuthClient({
plugins: [twoFactorClient()],
});
export const { signIn, signUp, signOut, useSession } = authClient;
Rename a field in the server config and the call site stops compiling, which is exactly the feedback you want from an auth layer. The session hook returns a typed user rather than a bag of unknown claims, so the shape you read in a component is the shape your database actually stores.
Plugins instead of a monolith
The core handles users, sessions and accounts, and everything beyond that is opt-in. This matters because most auth libraries ship a single surface where you pay the complexity of features you never enable, while here the API grows only with what you register:
import { admin, organization, twoFactor } from 'better-auth/plugins';
export const auth = betterAuth({
database: prismaAdapter(prisma, { provider: 'postgresql' }),
plugins: [
twoFactor({ issuer: 'Rewind' }),
organization({ allowUserToCreateOrganization: true }),
admin(),
],
});
Adding organization brings multi-tenancy with teams, invitations and roles, and it extends the User and Session types at the same time. Passkeys, single sign-on, multi-session and Stripe subscriptions follow the same pattern, which keeps a small product small and lets an enterprise one grow without a migration.
Sessions read on the server
Server-side session access goes through auth.api, the same instance the routes use, so there is no separate SDK and no HTTP hop to your own backend. Wrapping it in React cache keeps a request that renders five nested components down to a single lookup:
import { cache } from 'react';
import { headers } from 'next/headers';
import { auth } from '@/lib/auth';
export const getSession = cache(async () => auth.api.getSession({ headers: await headers() }));
Because the session lives in your database and not behind a provider API, an expensive page can join user data and application data in one query instead of two round trips. That is the difference between auth as a dependency and auth as a service.
What you stop maintaining
The value is less in any single feature than in the list of things that stop being your problem. Rate limiting on sensitive endpoints, secure cookie flags, CSRF protection, account linking between a password and a social provider, verification and reset tokens with their expiry: all of it ships configured, and you tune it rather than build it.
The trade is real and worth stating. You own the database, so you own its migrations and its backups, and an outage in your auth tables is an outage in your product. A hosted provider absorbs that operational weight, which is a reasonable thing to pay for when a team has no one to carry it.
Worth choosing when
Better Auth fits products where the user table is part of the domain rather than an external detail, which covers most SaaS with organizations, roles or billing tied to accounts. It also fits teams that want auth reviewable in pull requests instead of clicked together in a dashboard. The typed bridge between server and client is what makes it pleasant day to day, and the plugin model is what keeps it viable as the product grows.