Skip to content

Authentication with Better Auth

Authentication is hard. You have to handle sessions, cookies, password hashing, database storage, and security best practices. Better Auth is a library that simplifies this for TypeScript full-stack applications.

Better Auth consists of two parts:

  1. Server: Handles API endpoints (/api/auth/*), database interactions, and session management.
  2. Client: A React library to easily call these endpoints (signIn, signUp, useSession).

First, we configure the server-side instance. We need to connect it to our Drizzle database and Hono.

Better Auth needs specific tables (user, session, account, verification).

src/worker/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const session = sqliteTable("session", {
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id),
});
export const account = sqliteTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
});
src/worker/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "../db/schema";
export const getAuth = (env: any) => {
const db = drizzle(env.DB, { schema });
return betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
schema: schema,
}),
emailAndPassword: {
enabled: true,
},
secret: env.BETTER_AUTH_SECRET, // Make sure to set this in .dev.vars
});
};

We mount the auth handler to Hono.

src/worker/index.ts
app.on(["POST", "GET"], "/api/auth/**", (c) => {
const auth = getAuth(c.env);
return auth.handler(c.req.raw);
});

Now we configure the React client to talk to our backend.

src/react-app/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: "http://localhost:8787/api/auth", // Your backend URL
});
export const { signIn, signUp, signOut, useSession } = authClient;
import { signIn, signUp } from "@/lib/auth-client";
// Login
await signIn.email({
email: "user@example.com",
password: "password123",
});
// Register
await signUp.email({
email: "user@example.com",
password: "password123",
name: "John Doe",
});
import { useSession } from "@/lib/auth-client";
const Dashboard = () => {
const { data: session, isPending } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Please log in</div>;
return <h1>Welcome back, {session.user.name}!</h1>;
};

Better Auth handles the complexity of tokens and cookies for you, so you can focus on building your app features.

  1. Where does Better Auth store user sessions?

    Answer In the database (specifically the session table).

  2. What hook do we use to check if a user is logged in on the frontend?

    Answer useSession()

  3. Why do we need to mount the auth handler to /api/auth/** in Hono?

    Answer To allow the Better Auth client (frontend) to communicate with the backend for login, registration, and session management endpoints.