Part 1: Create an Auth service
Build one application with email/password authentication, an Effect HTTP API, and GitHub sign-in. This tutorial uses Bun and Cloudflare D1; other databases use the same Auth service.
Use your existing Alchemy project with Bun and a configured Cloudflare profile. The examples use Effect 4 and Better Auth 1.7.5.
Install the module
Section titled “Install the module”bun add @alchemy.run/better-auth better-auth@1.7.5 kyselyUse matching Alchemy and Better Auth integration releases. D1 uses Kysely internally; application code does not need to construct a Kysely client.
Declare the database
Section titled “Declare the database”import * as Cloudflare from "alchemy/Cloudflare";
export const AuthDb = Cloudflare.D1.Database("AuthDb");Alchemy creates the database when you deploy. During local development, D1 runs in the local simulator.
Choose the authentication method
Section titled “Choose the authentication method”import { BetterAuth } from "@alchemy.run/better-auth";
const makeAuth = BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true },});Alchemy provisions a stable signing secret and applies D1 schema migrations. You do not need to create auth tables or supply a signing secret.
Expose an Effect service
Section titled “Expose an Effect service”import { BetterAuth } from "@alchemy.run/better-auth";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
const makeAuth = BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true },});
export class Auth extends Context.Service< Auth, Effect.Success<typeof makeAuth>>()("app/Auth") { static readonly layer = Layer.effect(Auth, makeAuth);}Handlers and middleware can now request the same typed service with yield* Auth.
Create the Worker
Section titled “Create the Worker”import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class AuthApi extends Cloudflare.Worker<AuthApi>()( "AuthApi", { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] }, }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("Ready")) }; }),) {}Alchemy selects an available local port. Use the URL it prints rather than assuming a particular port.
Provide the database layer
Section titled “Provide the database layer”import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";import * as Layer from "effect/Layer";import { Auth } from "./auth.ts";import { AuthDb } from "./database.ts";
Effect.gen(function* () { yield* Auth; return { fetch: Effect.succeed(HttpServerResponse.text("Ready")) }; }), }).pipe( Effect.provide(Auth.layer.pipe(Layer.provide(CloudflareD1(AuthDb)))), ),The host supplies the database; the Auth service stays independent of Cloudflare. This also registers the database binding and migration action.
Create the stack
Section titled “Create the stack”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import AuthApi from "./src/worker.ts";
export default Alchemy.Stack( "BetterAuthTutorial", { providers: Cloudflare.providers(), state: Alchemy.localState() }, Effect.gen(function* () { const api = yield* AuthApi; return { url: api.url }; }),);Local state keeps this walkthrough self-contained. Preserve its state directory so future deployments retain the signing secret and resource identities.
Start development
Section titled “Start development”bunx alchemy dev --stage tutorialIn another terminal, paste the printed URL after running read, then press Enter:
read -r DEV_URLexport DEV_URLcurl "$DEV_URL"Keep this shell for the following API checks. Update DEV_URL if a later restart prints a different address.
ReadyContinue to Part 2: Mount the HTTP API.