Hono Integration
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the installation.
Mount the handler
We need to mount the handler to Hono endpoint.
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);Cors
To configure cors, you need to use the cors plugin from hono/cors.
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono();
app.use(
"/api/auth/*", // or replace with "*" to enable cors for all routes
cors({
origin: "http://localhost:3001", // replace with your origin
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);Important: CORS middleware must be registered before your routes. This ensures that cross-origin requests are properly handled before they reach your authentication endpoints.
Middleware
You can add a middleware to save the session and user in a context and also add validations for every route.
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null;
session: typeof auth.$Infer.Session.session | null
}
}>();
app.use("*", async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
c.set("user", null);
c.set("session", null);
await next();
return;
}
c.set("user", session.user);
c.set("session", session.session);
await next();
});
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
serve(app);This will allow you to access the user and session object in all of your routes.
app.get("/session", (c) => {
const session = c.get("session")
const user = c.get("user")
if(!user) return c.body(null, 401);
return c.json({
session,
user
});
});Cross-Domain Cookies
By default, all Better Auth cookies are set with SameSite=Lax. If you need to use cookies across different domains, you’ll need to set SameSite=None and Secure=true. However, we recommend using subdomains whenever possible, as this allows you to keep SameSite=Lax. To enable cross-subdomain cookies, simply turn on crossSubDomainCookies in your auth config.
export const auth = createAuth({
advanced: {
crossSubDomainCookies: {
enabled: true
}
}
})If you still need to set SameSite=None and Secure=true, you can adjust these attributes globally through cookieOptions in the createAuth configuration.
export const auth = createAuth({
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
partitioned: true // New browser standards will mandate this for foreign cookies
}
}
})You can also customize cookie attributes individually by setting them within cookies in your auth config.
export const auth = createAuth({
advanced: {
cookies: {
sessionToken: {
attributes: {
sameSite: "none",
secure: true,
partitioned: true // New browser standards will mandate this for foreign cookies
}
}
}
}
})Client-Side Configuration
When using the Hono client (@hono/client) to make requests to your Better Auth-protected endpoints, you need to configure it to send credentials (cookies) with cross-origin requests.
import { hc } from "hono/client";
import type { AppType } from "./server"; // Your Hono app type
const client = hc<AppType>("http://localhost:8787/", {
init: {
credentials: "include", // Required for sending cookies cross-origin
},
});
// Now your client requests will include credentials
const response = await client.someProtectedEndpoint.$get();This configuration is necessary when:
- Your client and server are on different domains/ports during development
- You're making cross-origin requests in production
- You need to send authentication cookies with your requests
The credentials: "include" option tells the fetch client to send cookies even for cross-origin requests. This works in conjunction with the CORS configuration on your server that has credentials: true.
Note: Make sure your CORS configuration on the server matches your client's domain, and that
credentials: trueis set in both the server's CORS config and the client's fetch config.
Cloudflare Workers
When deploying Better Auth with Hono on Cloudflare Workers, you may encounter issues with the CLI if your auth configuration depends on runtime environment variables (like env.DB for D1 databases).
The Problem
In Cloudflare Workers, environment variables are passed at runtime through request handlers, not available via process.env at build time. This makes it challenging to use the Better Auth CLI for migrations since the CLI needs to import your auth configuration file.
// This won't work with the CLI because env is not available
export const createAuth = (env: Env) => betterAuth({
database: env.DB, // env.DB is only available at runtime
// ... rest of config
});Solution 1: Programmatic Migrations (Built-in Kysely Adapter)
If you're using the built-in Kysely adapter (SQLite/D1, PostgreSQL, MySQL), you can run migrations programmatically through a custom endpoint:
import { Hono } from "hono";
import { getMigrations } from "better-auth/db";
import { betterAuth } from "better-auth";
type Env = {
DB: D1Database;
// ... other bindings
};
const app = new Hono<{ Bindings: Env }>();
// Migration endpoint - call this to set up your database
app.post("/migrate", async (c) => {
const authConfig = {
database: c.env.DB,
// Add all your other auth config options here
// (socialProviders, plugins, etc.)
};
try {
const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(authConfig);
if (toBeCreated.length === 0 && toBeAdded.length === 0) {
return c.json({ message: "No migrations needed" });
}
await runMigrations();
return c.json({
message: "Migrations completed successfully",
tablesCreated: toBeCreated.map(t => t.table),
tablesUpdated: toBeAdded.map(t => t.table)
});
} catch (error) {
return c.json({
error: error instanceof Error ? error.message : "Migration failed"
}, 500);
}
});
// Your auth handler
app.on(["POST", "GET"], "/api/auth/*", async (c) => {
const auth = betterAuth({
database: c.env.DB,
// ... your config
});
return auth.handler(c.req.raw);
});
export default app;Usage:
- Deploy your worker
- Call the migration endpoint once:
curl -X POST https://your-worker.workers.dev/migrate - Remove or protect the migration endpoint after running
Security Note: Protect your migration endpoint in production! Consider:
- Adding authentication/authorization
- Using environment-based checks to only allow migrations in development
- Removing the endpoint entirely after initial setup
Solution 2: Use cloudflare:workers Import (Prisma/Drizzle)
For Prisma or Drizzle users, Cloudflare now supports importing environment variables from cloudflare:workers:
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: drizzle(env.DB),
// ... rest of your config
});This allows you to use the standard CLI commands:
npx @better-auth/cli generateSee the Cloudflare changelog for more details on this feature.
Solution 3: Use process.env with Compatibility Flag
Add the nodejs_compat_populate_process_env flag to your wrangler.toml:
compatibility_flags = ["nodejs_compat_populate_process_env"]Then use process.env in your auth config:
import { drizzle } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: drizzle(process.env.DB as any),
// ... rest of config
});For compatibility dates on or after 2025-04-01, this is the default behavior when nodejs_compat is enabled. See the Cloudflare documentation for more details.
For more information on programmatic migrations, see the Database documentation.