1---
2title: SQLite
3description: Run Better Auth in a local Bun server with a persistent SQLite file.
4---
5
6`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](/better-auth/databases/cloudflare-d1) instead.
7
8## Install
9
10```sh
11bun add "@effect/platform-bun@4.0.0-rc.115" @alchemy.run/better-auth better-auth@^1.7.5 kysely
12```
13
14Bun supplies the driver, while `kysely` supports the SQL adapter and schema migration. A standard Node.js Lambda cannot load `bun:sqlite`.
15
16## Define the Bun server
17
18```typescript title="src/server.ts"
19import { BetterAuth, Database } from "@alchemy.run/better-auth";
20import { SQLite } from "@alchemy.run/better-auth/SQLite";
21import * as BunHttpServer from "@effect/platform-bun/BunHttpServer";
22import * as BunRuntime from "@effect/platform-bun/BunRuntime";
23import * as BunServices from "@effect/platform-bun/BunServices";
24import { RuntimeContext } from "alchemy";
25import { getMigrations } from "better-auth/db/migration";
26import * as Config from "effect/Config";
27import * as Console from "effect/Console";
28import * as Effect from "effect/Effect";
29import * as Redacted from "effect/Redacted";
30import * as HttpServer from "effect/http/HttpServer";
31
32const program = Effect.gen(function* () {
33 const secret = yield* Config.Redacted("AUTH_SECRET");
34 const database = yield* Database;
35
36 yield* Effect.gen(function* () {
37 const connect = yield* database.migrate!.connect;
38 const raw = yield* connect;
39 const migrations = yield* Effect.tryPromise(() => getMigrations({
40 basePath: "/api/auth",
41 emailAndPassword: { enabled: true },
42 database: raw,
43 secret: Redacted.value(secret),
44 }));
45 yield* Effect.tryPromise(() => migrations.runMigrations());
46 }).pipe(Effect.scoped);
47
48 const auth = yield* BetterAuth({
49 basePath: "/api/auth",
50 emailAndPassword: { enabled: true },
51 secret,
52 migrate: false,
53 });
54 const server = yield* BunHttpServer.make({
55 hostname: "127.0.0.1",
56 port: 0,
57 });
58 yield* server.serve(auth.fetch);
59 yield* Console.log(HttpServer.formatAddress(server.address));
60 yield* Effect.never;
61}).pipe(
62 Effect.provide(SQLite("./auth.sqlite")),
63 Effect.provide(RuntimeContext.phantom),
64 Effect.scoped,
65 Effect.provide(BunServices.layer),
66);
67
68BunRuntime.runMain(program);
69```
70
71The 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.
72
73`SQLite` supplies migration support, and the scoped migration connection closes before requests begin.
74
75The 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.
76
77## Define the local stack
78
79```typescript title="alchemy.run.ts"
80import * as Alchemy from "alchemy";
81import * as Command from "alchemy/Command";
82import * as Effect from "effect/Effect";
83import * as Layer from "effect/Layer";
84
85export default Alchemy.Stack(
86 "BetterAuthSQLite",
87 {
88 providers: Layer.mergeAll(Command.providers(), Alchemy.RandomProvider()),
89 state: Alchemy.localState(),
90 },
91 Effect.gen(function* () {
92 const secret = yield* Alchemy.Random("BetterAuthSecret");
93 const server = yield* Command.Dev("AuthApi", {
94 command: "bun run src/server.ts",
95 env: { AUTH_SECRET: secret.text },
96 });
97 return { url: server.url };
98 }),
99);
100```
101
102The 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.
103
104## Start locally
105
106```sh
107bunx alchemy dev
108```
109
110The 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.
111
112Set `AUTH_ORIGIN` to the printed URL and check the endpoint:
113
114```sh
115curl -i "$AUTH_ORIGIN/api/auth/get-session"
116```
117
118## File and migration lifetime
119
120`./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.
121
122For 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.
123
124Use 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.
125
126The startup migration pattern here is for local development; review schema changes before pointing it at valuable data. See [migrations](/better-auth/guides/migrations), the [upgrade guide](/better-auth/upgrades/from-1-6-to-1-7), and the [`SQLite` reference](/providers/betterauth/sqlite).