Skip to main content

Better Auth Integration

Integrate Better Auth for authentication with your server framework. Better Auth handles session management, authentication flows, and user management.

Setup

First, install Better Auth:

pnpm add better-auth
# or
npm install better-auth
# or
yarn add better-auth

Create your Better Auth configuration:

// src/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { z } from "zod";

export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or your database provider
}),
emailAndPassword: {
enabled: true,
},
// Add other auth providers as needed
});

Validating User Session with Zod

Better Auth returns session data that should be validated. Use Zod to ensure type safety and validate the user structure:

import { z } from "zod";
import { auth } from "./auth.js";

// Define your user schema
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
// Add other user fields as needed
});

// Schema for Better Auth session
const SessionSchema = z.object({
user: UserSchema,
session: z.object({
id: z.string(),
userId: z.string(),
expiresAt: z.date(),
}),
});

// Infer TypeScript types from Zod schemas
export type User = z.infer<typeof UserSchema>;
export type Session = z.infer<typeof SessionSchema>;

export async function getAuthSession(request: Request): Promise<Session | null> {
const session = await auth.api.getSession({ headers: request.headers });

// Validate and parse the session with safeParse for optional validation
const result = SessionSchema.safeParse(session);

if (!result.success) {
// Log validation errors in development
if (process.env.NODE_ENV === "development") {
console.warn("Session validation failed:", result.error);
}
return null;
}

return result.data;
}

// Or if you only need the user:
export async function getAuthUser(request: Request): Promise<User | null> {
const session = await getAuthSession(request);
return session?.user ?? null;
}

Mounting Better Auth Routes

Mount Better Auth routes alongside your server framework routes. Better Auth handles all /api/auth/* routes:

import { Hono } from "hono";
import { createServer } from "@alt-stack/server-hono";
import { auth } from "./auth.js";
import { todosRouter } from "./routes/todos.js";

// Create base Hono app
const app = new Hono();

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

// Mount your server framework routes
const serverApp = createServer({
todos: todosRouter,
});

app.route("/", serverApp);

export default app;

Adding User to Context

Add the authenticated user to your custom context so it's available in all handlers. Use Zod validation to ensure type safety:

import { createServer } from "@alt-stack/server-hono";
import { getAuthUser, type User } from "./auth.js";
import type { Context } from "hono";
import { z } from "zod";

interface AppContext extends Record<string, unknown> {
user: User | null;
}

async function createContext(c: Context): Promise<AppContext> {
const user = await getAuthUser(c.req.raw);
return {
user,
};
}

const factory = init<AppContext>();
const router = factory.router()
.get("/profile", {
input: {},
output: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
}),
})
.handler((ctx) => {
if (!ctx.user) {
return ctx.hono.json({ error: "Unauthorized" }, 401);
}

// ctx.user is fully typed based on UserSchema
return {
id: ctx.user.id,
email: ctx.user.email,
name: ctx.user.name,
};
});

const app = createServer({
users: router,
}, {
createContext,
});