Auth with Hono, Better Auth & Drizzle
Learn how to quickly build a Hono auth backend using Better Auth and Drizzle ORM.
Project Structure
This project utilizes a monorepo architecture to enforce separation of concerns. The structure isolates the core API application from shared internal packages, specifically separating authentication logic and database management into workspaces.
Setup Environment
Configure environment variables for API, auth, and database. These secrets will be used across the project.
Create .env Files
Each package has its own .env for its specific configuration.
DATABASE_URL=
BETTER_AUTH_SECRET=DATABASE_URL=DATABASE_URL=
BETTER_AUTH_SECRET=Configure Database
The database package manages the connection and schema definitions.
Database Client
import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres";
export function createDBClient(database_url: string) {
return drizzle(database_url!);
}Create Package Config
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/schema',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Define Schema
Create database tables to match Better Auth's requirements.
Generate Better Auth Schemas
This command generates all required auth tables automatically.
npx @better-auth/cli generate --config ./packages/auth/better-auth.config.ts --output ./packages/db/src/schema/auth.tsExport Schemas
We split the schema into modules. The auth.ts file will be auto-generated by the Better Auth CLI later.
import * as authTables from "./auth";
export { authTables as authSchema };Migrate Database
Apply schema changes to your database.
Generate Migration Files
npx drizzle-kit generateApply Migrations
npx drizzle-kit migrateView Database in Drizzle Studio
Inspect your tables and data visually.
npx drizzle-kit studioConfigure Auth
Set up the Better Auth package to handle authentication with your Hono backend.
Create Better Auth Package
This file initializes a Better Auth instance with a Drizzle adapter and optional plugins.
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins"
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createDBClient } from "@better-hr/db";
import { authSchema } from "@better-hr/db/schema";
export function createAuth({ database_url }: { database_url: string }) {
const db = createDBClient(database_url);
return betterAuth({
appName: "BTTR Blocks Authentication",
basePath: "/auth",
experimental: { joins: true },
database: drizzleAdapter(db, {
provider: "pg",
schema: {
...authSchema,
},
}),
plugins: [
openAPI({ disableDefaultReference: false })
]
});
}
type AuthInstance = ReturnType<typeof createAuth>
export type AuthType = {
user: AuthInstance["$Infer"]["Session"]["user"] | null
session: AuthInstance["$Infer"]["Session"]["session"] | null
}For more plugins and advanced configuration, see Better Auth Docs.
Create Package Config
Define the database connection and schema location for migrations.
import { createAuth } from "./src";
export const auth = createAuth({ database_url: process.env.DATABASE_URL! });For more plugins and advanced configuration, see Better Auth Docs.
Build API with Hono
The Hono application serves as the primary backend service. It uses the `@bttr-blocks/auth package to expose authentication endpoints.
Create Hono Package
The entry point initializes the Hono app and mounts the authentication routes.
import { Hono } from 'hono'
import { default as AuthRoute } from "@/paths/auth";
const app = new Hono();
app.get(
"/auth/docs",
Scalar({
title: "BTTR Blocks Auth API",
slug: "bttr-blocks-auth",
pageTitle: "BTTR Blocks Auth API",
sources: [{ url: "/auth/open-api/generate-schema", title: "Auth" }],
}),
);
app.get("/", (c) => {
return c.text('Welcome to BTTR Blocks!')
});
app.route("/", AuthRoute);
export default app;In the auth route handler, we instantiate the auth client using the request's environment bindings and attach the Better Auth handler. CORS middleware is configured to allow cross-origin requests from the client application.
import { Hono } from 'hono'
import { createAuth, AuthType } from "@better-hr/auth";
import { cors } from "hono/cors";
type Bindings = AuthType & {
DATABASE_URL: string;
};
const app = new Hono<{ Bindings: Bindings }>({});
// CORS middleware
app.use(
"/auth/*",
cors({
origin: "http://localhost:3001",
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
// Auth route
app.on(["POST", "GET"], "/auth/*", (c) => {
const auth = createAuth({
database_url: c.env.DATABASE_URL,
});
return auth.handler(c.req.raw);
});
export default app;Create Package Config
{
"name": "@bttr-blocks/api",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
},
"dependencies": {
"@scalar/hono-api-reference": "^0.9.28",
"better-auth": "^1.4.5",
"drizzle-orm": "^0.44.7",
"hono": "^4.10.7",
},
"devDependencies": {
"@bttr-hr/auth": "workspace:*",
"@bttr-hr/db": "workspace:*",
"wrangler": "^4.4.0"
}
}Start the API server
npm run devInspired by the shadcn login block!