# Better Auth in Astro on Cloudflare Workers

> Better Auth setup for Astro SSR on Cloudflare Workers: D1 Drizzle adapter, catch-all auth route, session in middleware, no Node dependencies.

Author: Flavio Copes | Published: 2026-07-16 | Canonical: https://flaviocopes.com/better-auth-astro-cloudflare/

I needed auth in [StackPlan](https://stackplan.dev), an Astro SSR app on Cloudflare Workers with D1.

[Better Auth](https://flaviocopes.com/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:

```ts
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:

```ts
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.

```ts
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:

```ts
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:

```ts
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.
