Better Auth Integration
This guide demonstrates how to integrate Better Auth with your Koritsu for comprehensive authentication and session management.
For complete Better Auth documentation, features, and configuration options, visit the official Better Auth documentation.
When integrated with Koritsu, Better Auth routes are handled through the proxy system, allowing seamless authentication alongside your API routes.
Note: This guide uses SQLite with
bun:sqliteas an example, but Better Auth works with any database system supported by Drizzle ORM (PostgreSQL, MySQL, SQLite, etc.). Simply adjust the database driver and connection configuration for your chosen database.
Quick Start
A complete working example is available in the packages/examples/auth directory of the Koritsu repository. This example includes:
- Full Better Auth integration
- Database setup with automatic schema generation
- OpenAPI documentation generation
You can use this as a starting point for your own authentication implementation.
Installation
1. Init the project
bun init -y2. Install the required dependencies
bun add better-auth drizzle-orm
bun add -d drizzle-kitFolder structure setup
Let's create a lib directory in which we'll be able to store our database and authentication configuration files.
mkdir -p lib/dbDatabase Setup
1. Configure Database Connection
// lib/db/index.ts
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
const sqlite = new Database("sqlite.db");
export const db = drizzle({ client: sqlite });2. Generate Database Schema
Better Auth can automatically generate the required database schema:
# Will prompt to save the schema file as auth-schema.ts, say yes.
bunx --bun @better-auth/cli generate
# Move the generated schema to the correct location
mv auth-schema.ts lib/db/schema.ts3. Run drizzle generation
bunx --bun drizzle-kit generateBetter Auth Configuration
Create your Better Auth instance:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { openAPI } from "better-auth/plugins";
import { db } from "./db";
import * as schema from "./db/schema";
export const auth = betterAuth({
basePath: "/auth",
database: drizzleAdapter(db, {
provider: "sqlite",
schema,
}),
emailAndPassword: {
enabled: true,
},
plugins: [
openAPI({
path: "/openapi",
}),
],
});Koritsu Integration
Integrate Better Auth with your Koritsu using the proxy system. You can handle authentication directly in the proxy handler for protected routes:
// index.ts
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { Api } from "koritsu";
import { auth } from "./lib/auth";
import { db } from "./lib/db";
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function runMigrations() {
console.info("Migrating database...");
let retries = 10;
while (retries > 0) {
try {
console.info(`Running migrations (retries left: ${retries})`);
migrate(db, {
migrationsFolder: "drizzle/",
});
console.info("Database migrated successfully.");
return;
} catch (error) {
console.error(
`Database connection failed. Retrying... (${retries} attempts left)`
);
if (retries === 1) {
console.error("Could not connect to the database. Exiting.");
console.error(error);
process.exit(1);
}
retries -= 1;
await sleep(300);
}
}
}
const server = new Api({
title: "Example Auth API",
description: "API with Better Auth integration",
server: {
routes: {
dir: "./routes",
},
},
swagger: {
enabled: true,
path: "/",
// Unified OpenAPI documentation
externalSpecs: [
{
url: "http://localhost:8080/auth/openapi",
name: "better-auth",
tags: ["Authentication"],
},
],
},
proxy: {
enabled: true,
configs: [
{
pattern: "/auth/**",
description: "Authentication endpoints handled by better-auth",
handler: async ({ request }) => {
console.log(`[AUTH] ${request.method} ${request.url}`);
try {
const response = await auth.handler(request);
if (response) {
return {
proceed: false,
response,
};
}
return {
proceed: false,
response: new Response("Auth endpoint not found", {
status: 404,
}),
};
} catch (error) {
console.error("[AUTH] Error handling request:", error);
return {
proceed: false,
response: new Response("Internal auth error", { status: 500 }),
};
}
},
},
{
pattern: "/protected/**",
description: "Protected routes requiring authentication",
handler: async ({ request }) => {
try {
// Validate session before proceeding to route handlers
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
return {
proceed: false,
response: new Response("Unauthorized", { status: 401 }),
};
}
// Add user info to request headers for route handlers to use
const headers = new Headers(request.headers);
headers.set("x-user-id", session.user.id);
headers.set("x-user-email", session.user.email);
headers.set("x-user-name", session.user.name || "");
// Create new request with user info
const authenticatedRequest = new Request(request.url, {
method: request.method,
headers,
body: request.body,
});
return {
proceed: true, // Continue to route handlers
request: authenticatedRequest,
};
} catch (error) {
console.error("[AUTH] Session validation error:", error);
return {
proceed: false,
response: new Response("Invalid session", { status: 401 }),
};
}
},
},
],
},
});
await runMigrations();
server.start();Protecting Routes
With the proxy configuration above, any route under /protected/ will automatically require authentication. Your route handlers can access user information from the request headers:
// routes/protected/profile/route.ts
import { createRoute } from "koritsu";
import { z } from "zod";
export const GET = createRoute({
method: "GET",
handler: async ({ request }) => {
// User info is already validated and available in headers
const userId = request.headers.get("x-user-id");
const userEmail = request.headers.get("x-user-email");
const userName = request.headers.get("x-user-name");
return Response.json({
message: `Hello, ${userName}!`,
user: {
id: userId,
email: userEmail,
name: userName,
},
});
},
spec: {
format: "json",
tags: ["Protected"],
summary: "Get user profile",
responses: {
200: {
schema: z.object({
message: z.string(),
user: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
}),
}),
},
401: {
schema: z.object({
error: z.string(),
}),
},
},
},
});Optional: Route-Level Authentication
For more granular control, you can also create a middleware function for specific routes:
// lib/middleware.ts
import { auth } from "./auth";
export async function requireAuth(request: Request) {
try {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
return { user: session.user, session: session.session };
} catch (error) {
return new Response("Invalid session", { status: 401 });
}
}Use this for routes that need authentication but aren't under the /protected/ path:
// routes/admin/route.ts
import { createRoute } from "koritsu";
import { requireAuth } from "../../lib/middleware";
import { z } from "zod";
export const GET = createRoute({
method: "GET",
handler: async ({ request }) => {
// Check authentication manually for this specific route
const authResult = await requireAuth(request);
if (authResult instanceof Response) {
return authResult;
}
const { user } = authResult;
return Response.json({
message: "Admin access granted",
user: user,
});
},
spec: {
format: "json",
tags: ["Admin"],
summary: "Admin endpoint",
responses: {
200: {
schema: z.object({
message: z.string(),
user: z.object({
id: z.string(),
email: z.string(),
name: z.string(),
}),
}),
},
401: {
schema: z.object({
error: z.string(),
}),
},
},
},
});Unified OpenAPI Documentation
The externalSpecs configuration in the Swagger settings automatically merges Better Auth's comprehensive OpenAPI specification with your main API documentation. This integration provides:
Single Documentation Source
All endpoints (your routes + Better Auth) are unified in one Swagger UI interface. Instead of having separate documentation for authentication endpoints, everything is accessible from your main API documentation.
Consistent Schemas
Better Auth's schemas (User, Session, Account, Verification) become available throughout your API documentation, ensuring consistency across your entire API surface.
Integrated Security
Better Auth's authentication schemes (cookie-based apiKeyCookie and bearerAuth) are automatically integrated into your OpenAPI spec, providing proper security documentation.
Custom Tagging
The tags: ["Authentication"] configuration groups all Better Auth endpoints under a custom tag in your Swagger UI, making it easy to organize and navigate the documentation.
How It Works
The external specs feature supports both JSON and HTML responses from OpenAPI endpoints. When Better Auth serves its OpenAPI spec as an HTML page with embedded JSON (which is the default), Koritsu automatically extracts the JSON specification and merges it with your main API documentation.
The merge process handles:
- Paths: All Better Auth routes (
/sign-in/email,/get-session, etc.) appear alongside your routes - Components: Schemas and security schemes are merged into your main spec
- Tags: Custom or existing tags are preserved and organized
- Security: Authentication requirements are properly documented
This means developers working with your API get complete documentation in one place, with authentication flows clearly documented alongside your business logic endpoints.