Better-Auth Setup Guide for React: Self-Hosted Auth Without the Monthly Bill

By Aziz Ali
better-authreactauthenticationtypescriptsaas

Every SaaS project starts the same way: sign up, log in, protect a route. I've written this auth plumbing at least a dozen times. Clerk was magic until the invoice hit $25/month at 1,000 users. NextAuth felt "free" until I needed social login, sessions, and email verification in the same week and hit a wall of underdocumented edge cases.

Better-Auth is the library I wish I'd found sooner. It's self-hosted, open-source, TypeScript-first, and ships with sessions, OAuth, email/password, and 2FA out of the box. Here's how to get it running in a React app in under 30 minutes.

What Is Better-Auth (And Why Should You Care)?

Better-Auth is an open-source TypeScript authentication library. Unlike Clerk, your user data lives in your own database — not on a third-party server. Unlike Auth.js (NextAuth), it's not tightly coupled to Next.js and works with any backend: Express, Hono, Bun, TanStack Start, or a plain HTTP server.

Here's what you get out of the box:

  • Email/password authentication with verification flows
  • Social OAuth (GitHub, Google, Discord, and more)
  • Session management (database-backed, not just JWTs)
  • Password reset and email verification built-in
  • 2FA, passkeys, magic links, and impersonation as opt-in plugins
  • Full TypeScript types — no any hacks

I wrote a full comparison in Better-Auth vs Clerk: Why I Stopped Paying for Clerk if you want the deeper breakdown. But if you've already decided and just want the setup — read on.

Installing Better-Auth

bun add better-auth
# or: npm install better-auth / pnpm add better-auth

Better-Auth needs a database to persist users and sessions. It supports Drizzle ORM and Prisma adapters, plus raw database drivers. For this guide I'm using Drizzle + PostgreSQL (the production-grade choice for SaaS).

Generate the Schema

Run the CLI to generate your database tables:

bunx better-auth generate

This outputs the SQL migrations for user, session, account, and verification tables. Apply them with your migration tool of choice.

Server Config

Create src/lib/auth.ts:

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db"; // your Drizzle db instance

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg", // or "sqlite" for local dev
  }),
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
});

Then expose it as an API endpoint. With Hono on Bun:

import { Hono } from "hono";
import { auth } from "./lib/auth";

const app = new Hono();

// Better-Auth handles all /api/auth/* routes automatically
app.on(["POST", "GET"], "/api/auth/**", (c) => {
  return auth.handler(c.req.raw);
});

Better-Auth auto-generates handlers for /api/auth/sign-in, /api/auth/sign-up, /api/auth/session, /api/auth/sign-out, and all OAuth callback URLs. You don't wire these manually.

Setting Up the React Client

Install the React client:

bun add @better-auth/react

Create src/lib/auth-client.ts:

import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
  baseURL: import.meta.env.VITE_APP_URL, // e.g. http://localhost:3000
});

export const { signIn, signUp, signOut, useSession } = authClient;

Now you have fully-typed auth methods and hooks available everywhere in your app.

Sign In Form

import { signIn } from "@/lib/auth-client";

async function handleSignIn(email: string, password: string) {
  const { data, error } = await signIn.email({
    email,
    password,
    callbackURL: "/dashboard",
  });
  if (error) {
    toast.error(error.message);
  }
}

// OAuth sign-in is one line:
await signIn.social({ provider: "github", callbackURL: "/dashboard" });

Session Hook

import { useSession } from "@/lib/auth-client";

function Dashboard() {
  const { data: session, isPending } = useSession();

  if (isPending) return <LoadingSpinner />;
  if (!session) return <Navigate to="/login" />;

  return <div>Welcome back, {session.user.name} 👋</div>;
}

useSession() handles loading states, automatic re-fetching after sign-in, and null safety — no boilerplate required.

Protecting Routes

Here's a clean protected route wrapper that works with TanStack Router or React Router:

import { useSession } from "@/lib/auth-client";
import { Navigate, Outlet } from "@tanstack/react-router";

export function ProtectedRoute() {
  const { data: session, isPending } = useSession();

  if (isPending) return <FullPageLoader />;
  if (!session?.user) return <Navigate to="/login" />;

  return <Outlet />;
}

Wrap any route that requires authentication:

const router = createRouter({
  routeTree: rootRoute.addChildren([
    publicRoute,
    ProtectedRoute.addChildren([dashboardRoute, settingsRoute]),
  ]),
});

The Complete Auth Flow

flowchart LR
    A[User visits /dashboard] --> B{useSession: valid?}
    B -->|No| C[Redirect to /login]
    B -->|Yes| D[Render dashboard]
    C --> E[Email/Password OR OAuth]
    E --> F[POST /api/auth/sign-in]
    F --> G[Better-Auth validates credentials]
    G --> H[Session created in DB]
    H --> D

No token juggling, no manual cookie parsing. Better-Auth handles it end to end.

Why Not Clerk?

I've used Clerk on two projects. The DX is genuinely excellent. But the math doesn't work for indie hackers:

Better-Auth Clerk
Cost $0/month (self-hosted) $0.02/MAU after free tier
At 10k users ~$0 ~$200/month
Data ownership Your database Clerk's servers
Framework Any TypeScript Optimized for Next.js
Vendor lock-in None High
Setup time ~30 min ~10 min

Clerk's extra 20 minutes of setup time saved isn't worth $200/month when you scale. And you're betting your user data on a third party — if Clerk has an outage, your users can't log in.

For TanStack Start specifically, check out TanStack Start Auth Setup: The Right Way for the framework-specific wiring details and how sessions integrate with TanStack's loader pattern.

Frequently Asked Questions

Does Better-Auth work outside Next.js? Absolutely. Better-Auth is framework-agnostic by design. It works with any TypeScript backend — Hono, Express, Fastify, Bun HTTP, or TanStack Start. The React client works in any React app regardless of meta-framework.

Does Better-Auth support social login (Google, GitHub)? Yes. OAuth providers are first-class. You supply a clientId and clientSecret, and Better-Auth handles the redirect, callback, token exchange, and user creation automatically.

Is Better-Auth production-ready in 2026? Yes. It's actively maintained, widely used in production SaaS apps, and has a stable API. The plugin ecosystem (2FA, passkeys, magic links, multi-tenancy) keeps growing. Check the docs for the latest.

What database does Better-Auth require? Better-Auth supports PostgreSQL, MySQL, SQLite, and any database that Drizzle or Prisma can connect to. For production SaaS, Postgres is the standard choice.

Can I migrate from Clerk to Better-Auth? Yes, with some manual work. Export your users from Clerk, import them into your database with hashed passwords reset via forced password-reset emails. There's no automated tool yet, but it's a one-time migration worth doing.


Auth is the most repeated boilerplate in SaaS development — and the last thing you should be debugging when you have a product to ship. BetterStarter ships with Better-Auth, Drizzle ORM, Stripe, and Plunk email pre-wired and tested, so you open the repo and start building features on day one. One-time $99, yours forever.