SQLite
SQLite uses Bun’s built-in bun:sqlite driver, not a cloud database. This example runs a local Bun server through Command.Dev; a Worker, including one started by alchemy dev, must use D1 instead.
Install
Section titled “Install”bun add "@effect/platform-bun@4.0.0-rc.115" @alchemy.run/better-auth better-auth@^1.7.5 kyselyBun supplies the driver, while kysely supports the SQL adapter and schema migration. A standard Node.js Lambda cannot load bun:sqlite.
Define the Bun server
Section titled “Define the Bun server”import { BetterAuth, Database } from "@alchemy.run/better-auth";import { SQLite } from "@alchemy.run/better-auth/SQLite";import * as BunHttpServer from "@effect/platform-bun/BunHttpServer";import * as BunRuntime from "@effect/platform-bun/BunRuntime";import * as BunServices from "@effect/platform-bun/BunServices";import { RuntimeContext } from "alchemy";import { getMigrations } from "better-auth/db/migration";import * as Config from "effect/Config";import * as Console from "effect/Console";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";import * as HttpServer from "effect/unstable/http/HttpServer";
const program = Effect.gen(function* () { const secret = yield* Config.Redacted("AUTH_SECRET"); const database = yield* Database;
yield* Effect.gen(function* () { const connect = yield* database.migrate!.connect; const raw = yield* connect; const migrations = yield* Effect.tryPromise(() => getMigrations({ basePath: "/api/auth", emailAndPassword: { enabled: true }, database: raw, secret: Redacted.value(secret), })); yield* Effect.tryPromise(() => migrations.runMigrations()); }).pipe(Effect.scoped);
const auth = yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, secret, migrate: false, }); const server = yield* BunHttpServer.make({ hostname: "127.0.0.1", port: 0, }); yield* server.serve(auth.fetch); yield* Console.log(HttpServer.formatAddress(server.address)); yield* Effect.never;}).pipe( Effect.provide(SQLite("./auth.sqlite")), Effect.provide(RuntimeContext.phantom), Effect.scoped, Effect.provide(BunServices.layer),);
BunRuntime.runMain(program);The standalone Bun process has no Alchemy migration Action, so it applies the schema once at startup before listening. Keep schema-related options aligned between getMigrations and BetterAuth when adding plugins or fields.
SQLite supplies migration support, and the scoped migration connection closes before requests begin.
The Effect HTTP server provides a fresh scope per request; Better Auth opens and closes its SQLite connection within that scope. RuntimeContext.phantom satisfies the runtime-only methods here because this process has no resource Output accessors to resolve.
Define the local stack
Section titled “Define the local stack”import * as Alchemy from "alchemy";import * as Command from "alchemy/Command";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "BetterAuthSQLite", { providers: Layer.mergeAll(Command.providers(), Alchemy.RandomProvider()), state: Alchemy.localState(), }, Effect.gen(function* () { const secret = yield* Alchemy.Random("BetterAuthSecret"); const server = yield* Command.Dev("AuthApi", { command: "bun run src/server.ts", env: { AUTH_SECRET: secret.text }, }); return { url: server.url }; }),);The stack generates a stable signing secret and passes it to the child process automatically; do not create or export AUTH_SECRET yourself. This explicit command binding replaces the automatic binding provided by built-in Worker and Lambda hosts.
Start locally
Section titled “Start locally”bunx alchemy devThe server asks the OS for an available port and prints its URL, which Alchemy exposes as the stack output. Command.Dev runs only during development; alchemy deploy does not publish this local server.
Set AUTH_ORIGIN to the printed URL and check the endpoint:
curl -i "$AUTH_ORIGIN/api/auth/get-session"File and migration lifetime
Section titled “File and migration lifetime”./auth.sqlite is relative to the command’s working directory, and its parent directory must exist. Protect and back up both the database and .alchemy state; deleting the latter loses the generated signing secret.
For an Alchemy host that supplies AlchemyContext, SQLite() defaults to better-auth.sqlite in its resolved runtime directory, normally .alchemy. The standalone server above uses an explicit filename so it does not require that context.
Use a persistent filename rather than :memory: because migration and request connections are separate. Deploy-time migrations in a built-in Alchemy host affect the deployer’s file; they do not copy that file to another machine.
The startup migration pattern here is for local development; review schema changes before pointing it at valuable data. See migrations, the upgrade guide, and the SQLite reference.