Cloudflare Hyperdrive
This example provisions a Neon Postgres database, a Hyperdrive connection, and a Better Auth Worker. Hyperdrive provides pooled access to the origin; it does not create the origin database itself.
Install
Section titled “Install”bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely pgConfigure Cloudflare and Neon through Alchemy profiles, or supply NEON_API_KEY for Neon. If you only need direct Neon access, the Neon serverless layer avoids Hyperdrive and pg.
Define the database layer
Section titled “Define the database layer”import { CloudflareHyperdrive } from "@alchemy.run/better-auth/CloudflareHyperdrive";import * as Cloudflare from "alchemy/Cloudflare";import * as Neon from "alchemy/Neon";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
const AuthDb = Neon.Project("AuthDb");
export const AuthDatabase = Layer.unwrap( Effect.gen(function* () { const db = yield* AuthDb; const connection = yield* Cloudflare.Hyperdrive.Connection("AuthConnection", { origin: db.origin, caching: { disabled: true }, }); return CloudflareHyperdrive(connection, { migrate: db.connectionUri, }); }),);Keep Hyperdrive caching disabled so session and verification reads cannot return stale data. Migrations connect directly to the origin URL because Hyperdrive’s runtime connection string exists only inside the Worker.
Define the auth Worker
Section titled “Define the auth Worker”import { BetterAuth } from "@alchemy.run/better-auth";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { AuthDatabase } from "./auth-database.ts";
export default class AuthApi extends Cloudflare.Worker<AuthApi>()( "AuthApi", { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] }, }, Effect.gen(function* () { const auth = yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, }); return { fetch: auth.fetch }; }).pipe(Effect.provide(AuthDatabase)),) {}The layer registers Hyperdrive’s native binding and opens a driver pool inside each request, never during Worker construction. Better Auth’s signing secret is generated and bound automatically.
Define the stack
Section titled “Define the stack”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Neon from "alchemy/Neon";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import AuthApi from "./src/worker.ts";
export default Alchemy.Stack( "BetterAuthHyperdrive", { providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()), state: Alchemy.localState(), }, Effect.gen(function* () { const api = yield* AuthApi; return { url: api.url.as<string>() }; }),);Preserve and protect the local .alchemy state directory so later deployments retain resource identities and the signing secret.
Deploy and migrate through the origin
Section titled “Deploy and migrate through the origin”bunx alchemy deployThe deployer must reach the origin with schema-changing permissions; the migration completes before the Worker is updated. Set AUTH_ORIGIN to the printed URL and verify the endpoint:
curl -i "$AUTH_ORIGIN/api/auth/get-session"Omitting the database layer’s migrate source disables automatic migration support. In that case, leave Better Auth’s migrate unset or false and manage the schema yourself; explicitly requesting migrate: true fails.
MySQL origins
Section titled “MySQL origins”For an existing MySQL origin, install its driver instead of pg:
bun add mysql2Configure the Hyperdrive resource with that MySQL origin, select dialect: "mysql" in CloudflareHyperdrive, and supply the origin’s deploy-resolvable URL as migrate. Keep caching disabled; changing the dialect alone neither converts the Neon origin above nor moves existing auth records.
The pool option passes through to the selected driver, and migration credentials may come from a resource Output without binding them to the Worker. See configuration, migrations, the upgrade guide, and the CloudflareHyperdrive reference.