Better Auth in Astro on Cloudflare Workers
By Flavio Copes
Better Auth setup for Astro SSR on Cloudflare Workers: D1 Drizzle adapter, catch-all auth route, session in middleware, no Node dependencies.
I needed auth in StackPlan, an Astro SSR app on Cloudflare Workers with D1.
Better Auth fit well. It stores sessions in SQLite, runs on plain fetch, and needs no Node APIs.
That last point matters. Astro on the Cloudflare adapter runs inside workerd, not Node.
Create the auth instance
Better Auth wants a database adapter. We use Drizzle with D1.
The pattern is a factory function. You pass env vars and the D1 binding, and get back an auth instance:
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { createDb, schema } from '../db'
export function createAuth(env: AuthEnv) {
const db = createDb(env.DB)
return betterAuth({
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: {
user: schema.user,
session: schema.session,
account: schema.account,
verification: schema.verification,
},
}),
emailAndPassword: { enabled: true },
})
}
Better Auth owns the user, session, account, and verification tables. You map them to your Drizzle schema.
My advice: keep createAuth() in one file. Every route and middleware call imports the same factory.
The catch-all API route
Better Auth exposes many endpoints: sign-in, sign-up, OAuth callbacks, session refresh.
You don’t want a separate Astro file for each one.
Astro supports rest params. One file handles every auth path:
import type { APIRoute } from 'astro'
import { createAuth } from '../../../lib/auth'
export const ALL: APIRoute = async (context) => {
const auth = createAuth(getEnv())
return auth.handler(context.request)
}
Put this at src/pages/api/auth/[...all].ts.
Better Auth receives /api/auth/sign-in/email, /api/auth/callback/github, and the rest. You forward the raw Request to auth.handler().
Read the session in middleware
Most pages need to know who is logged in. Do that once in middleware.
import { defineMiddleware } from 'astro:middleware'
import { createAuth } from './lib/auth'
export const onRequest = defineMiddleware(async (context, next) => {
const auth = createAuth(getEnv())
const sessionData = await auth.api.getSession({
headers: context.request.headers,
})
context.locals.user = sessionData?.user ?? null
context.locals.session = sessionData?.session ?? null
return next()
})
Now every .astro page and API route can read context.locals.user.
Protected routes are a simple prefix check. If the path starts with /dashboard and there’s no user, redirect to /login.
Why workerd is a good fit
A lot of auth libraries assume Node. They pull in crypto, fs, or native modules.
Better Auth targets the Web Crypto and fetch APIs. Same surface area Workers already provide.
D1 is SQLite. Better Auth’s Drizzle adapter speaks SQLite natively. No Postgres-only assumptions.
The dev experience is the same as production. astro dev with the Cloudflare adapter also runs in workerd. Auth code you write locally behaves the same after deploy.
One gotcha with Astro cookies
Better Auth sets session cookies on its Response object.
Sometimes Astro’s cookie helpers don’t see those Set-Cookie headers when you redirect after sign-in.
The fix is a small helper. Copy Set-Cookie from the auth response into your redirect:
export function redirectWithAuthResponse(response: Response, location: string) {
const headers = new Headers({ Location: location })
for (const [key, value] of response.headers.entries()) {
if (key.toLowerCase() === 'set-cookie') headers.append('set-cookie', value)
}
return new Response(null, { status: 302, headers })
}
Use this when a form POST hits your own route and you call Better Auth’s API directly.
GitHub OAuth
Email/password is enough to start. GitHub OAuth is one config block:
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
},
Set BETTER_AUTH_URL to your public origin. OAuth redirect URLs must match exactly.
That’s the whole auth layer: one factory, one catch-all route, session in middleware. No separate auth server, no Node runtime.
Related posts about astro: