| 1 | --- |
| 2 | title: "Part 1: Create an Auth service" |
| 3 | description: Install Better Auth, provide a D1 database layer, and create an Effect service. |
| 4 | sidebar: |
| 5 | order: 1 |
| 6 | --- |
| 7 | |
| 8 | Build one application with email/password authentication, an Effect HTTP API, and GitHub sign-in. This tutorial uses Bun and Cloudflare D1; [other databases](/better-auth/databases) use the same Auth service. |
| 9 | |
| 10 | Use your existing Alchemy project with [Bun](https://bun.sh) and a [configured Cloudflare profile](/cloudflare/setup). The examples use Effect 4 and Better Auth 1.7.5. |
| 11 | |
| 12 | ## Install the module |
| 13 | |
| 14 | ```sh |
| 15 | bun add @alchemy.run/better-auth better-auth@1.7.5 kysely |
| 16 | ``` |
| 17 | |
| 18 | Use matching Alchemy and Better Auth integration releases. D1 uses Kysely internally; application code does not need to construct a Kysely client. |
| 19 | |
| 20 | ## Declare the database |
| 21 | |
| 22 | ```typescript |
| 23 | // src/database.ts |
| 24 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 25 | |
| 26 | export const AuthDb = Cloudflare.D1.Database("AuthDb"); |
| 27 | ``` |
| 28 | |
| 29 | Alchemy creates the database when you deploy. During local development, D1 runs in the local simulator. |
| 30 | |
| 31 | ## Choose the authentication method |
| 32 | |
| 33 | ```typescript |
| 34 | // src/auth.ts |
| 35 | import { BetterAuth } from "@alchemy.run/better-auth"; |
| 36 | |
| 37 | const makeAuth = BetterAuth({ |
| 38 | basePath: "/api/auth", |
| 39 | emailAndPassword: { enabled: true }, |
| 40 | }); |
| 41 | ``` |
| 42 | |
| 43 | Alchemy provisions a stable signing secret and applies D1 schema migrations. You do not need to create auth tables or supply a signing secret. |
| 44 | |
| 45 | ## Expose an Effect service |
| 46 | |
| 47 | ```diff lang="typescript" |
| 48 | // src/auth.ts |
| 49 | import { BetterAuth } from "@alchemy.run/better-auth"; |
| 50 | +import * as Context from "effect/Context"; |
| 51 | +import * as Effect from "effect/Effect"; |
| 52 | +import * as Layer from "effect/Layer"; |
| 53 | |
| 54 | const makeAuth = BetterAuth({ |
| 55 | basePath: "/api/auth", |
| 56 | emailAndPassword: { enabled: true }, |
| 57 | }); |
| 58 | + |
| 59 | +export class Auth extends Context.Service< |
| 60 | + Auth, |
| 61 | + Effect.Success<typeof makeAuth> |
| 62 | +>()("app/Auth") { |
| 63 | + static readonly layer = Layer.effect(Auth, makeAuth); |
| 64 | +} |
| 65 | ``` |
| 66 | |
| 67 | Handlers and middleware can now request the same typed service with `yield* Auth`. |
| 68 | |
| 69 | ## Create the Worker |
| 70 | |
| 71 | ```typescript |
| 72 | // src/worker.ts |
| 73 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 74 | import * as Effect from "effect/Effect"; |
| 75 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 76 | |
| 77 | export default class AuthApi extends Cloudflare.Worker<AuthApi>()( |
| 78 | "AuthApi", |
| 79 | { |
| 80 | main: import.meta.url, |
| 81 | compatibility: { flags: ["nodejs_compat"] }, |
| 82 | }, |
| 83 | Effect.gen(function* () { |
| 84 | return { fetch: Effect.succeed(HttpServerResponse.text("Ready")) }; |
| 85 | }), |
| 86 | ) {} |
| 87 | ``` |
| 88 | |
| 89 | Alchemy selects an available local port. Use the URL it prints rather than assuming a particular port. |
| 90 | |
| 91 | ## Provide the database layer |
| 92 | |
| 93 | ```diff lang="typescript" |
| 94 | // src/worker.ts |
| 95 | +import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1"; |
| 96 | +import * as Layer from "effect/Layer"; |
| 97 | +import { Auth } from "./auth.ts"; |
| 98 | +import { AuthDb } from "./database.ts"; |
| 99 | |
| 100 | Effect.gen(function* () { |
| 101 | + yield* Auth; |
| 102 | return { fetch: Effect.succeed(HttpServerResponse.text("Ready")) }; |
| 103 | - }), |
| 104 | + }).pipe( |
| 105 | + Effect.provide(Auth.layer.pipe(Layer.provide(CloudflareD1(AuthDb)))), |
| 106 | + ), |
| 107 | ``` |
| 108 | |
| 109 | The host supplies the database; the Auth service stays independent of Cloudflare. This also registers the database binding and migration action. |
| 110 | |
| 111 | ## Create the stack |
| 112 | |
| 113 | ```typescript |
| 114 | // alchemy.run.ts |
| 115 | import * as Alchemy from "alchemy"; |
| 116 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 117 | import * as Effect from "effect/Effect"; |
| 118 | import AuthApi from "./src/worker.ts"; |
| 119 | |
| 120 | export default Alchemy.Stack( |
| 121 | "BetterAuthTutorial", |
| 122 | { providers: Cloudflare.providers(), state: Alchemy.localState() }, |
| 123 | Effect.gen(function* () { |
| 124 | const api = yield* AuthApi; |
| 125 | return { url: api.url }; |
| 126 | }), |
| 127 | ); |
| 128 | ``` |
| 129 | |
| 130 | Local state keeps this walkthrough self-contained. Preserve its state directory so future deployments retain the signing secret and resource identities. |
| 131 | |
| 132 | ## Start development |
| 133 | |
| 134 | ```sh |
| 135 | bunx alchemy dev --stage tutorial |
| 136 | ``` |
| 137 | |
| 138 | In another terminal, paste the printed URL after running `read`, then press Enter: |
| 139 | |
| 140 | ```sh |
| 141 | read -r DEV_URL |
| 142 | export DEV_URL |
| 143 | curl "$DEV_URL" |
| 144 | ``` |
| 145 | |
| 146 | Keep this shell for the following API checks. Update `DEV_URL` if a later restart prints a different address. |
| 147 | |
| 148 | ```text |
| 149 | Ready |
| 150 | ``` |
| 151 | |
| 152 | Continue to [Part 2: Mount the HTTP API](/better-auth/tutorial/part-2). |