db-drizzle
| Package | @theoven/db-drizzle |
| Adds to context | ctx.db — the Drizzle instance, typed from your schema |
| Endpoints | none |
| Creates files | none directly; oven db generate writes migrations |
| Creates tables | only what your own schema declares |
| Status | shipped |
Install
Section titled “Install”bun add @theoven/db @theoven/db-drizzle drizzle-ormimport { db } from '@theoven/db'import { drizzleSqlite } from '@theoven/db-drizzle'import * as schema from './schema'
export const app = createApp().use(db(drizzleSqlite({ url: './data.db', schema })))What it does
Section titled “What it does”Connects Drizzle to SQLite or Postgres, verifies the connection at boot, and closes it on
shutdown. ctx.db is the Drizzle instance itself — not a wrapper, not a proxy. Your queries are
Drizzle queries, which is also why a coding model already knows how to write them (D16).
The brick’s job is only the part every application repeats and gets subtly wrong: connecting once, failing loudly at boot rather than on the first request, transactions that actually roll back, and a clean shutdown.
Endpoints
Section titled “Endpoints”None by default. The db brick can mount one health endpoint if you ask:
db(drizzleSqlite({ schema }), { healthPath: '/healthz' })| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
whatever you pass as healthPath |
runs select 1; 200 healthy, 503 not |
none — guard it yourself |
Off unless asked for, because a brick that silently adds a route is one that appears unexplained
in oven routes, and health endpoints are something people want to place and protect themselves.
Why this is the default
Section titled “Why this is the default”bun:sqlite and Drizzle are not alternatives — Drizzle runs on bun:sqlite, so this is
Bun’s own driver underneath.
The reason the default is Drizzle rather than raw SQL is portability. Raw SQL strings are SQLite-dialect-bound, so a raw default means moving to Postgres rewrites every query. With Drizzle, moving is one line:
// beforedb(drizzleSqlite({ url: './data.db', schema }))// after — every query unchangeddb(drizzlePostgres({ url: env.url('DATABASE_URL'), schema }))Configuration
Section titled “Configuration”SQLite
Section titled “SQLite”| Option | Default | Purpose |
|---|---|---|
url |
./data.db |
file path, or :memory: |
schema |
— | your Drizzle schema, which is what types ctx.db |
logger |
false |
log every statement |
tune |
true |
apply the pragmas below |
tune sets journal_mode = WAL, synchronous = NORMAL, busy_timeout = 5000 and
foreign_keys = ON. Without WAL, SQLite serialises readers behind a writer, and a web
application meets this as sporadic SQLITE_BUSY under exactly the concurrency it was built for.
Postgres
Section titled “Postgres”| Option | Default | Purpose |
|---|---|---|
url |
— | connection string; read it from the environment |
schema |
— | your Drizzle schema |
max |
10 |
pooled connections |
The connection URL is deliberately kept out of the provider’s name, because provider names reach logs and a connection string carries a password.
ctx.db is Drizzle. Everything you know about Drizzle applies, and nothing here is Oven-specific:
import { desc } from 'drizzle-orm'import { users } from '../../schema'
export default async ({ db }) => db.select().from(users).orderBy(desc(users.createdAt)).limit(20)import { eq } from 'drizzle-orm'import { z } from 'zod'import { users } from '../../schema'
export const params = z.object({ id: z.uuid() })
export default async ({ db, params }) => { const [user] = await db.select().from(users).where(eq(users.id, params.id)) if (!user) throw new NotFound(`No user ${params.id}.`) return user}Relational queries, prepared statements, db.query.*, partial selects — all of it is Drizzle’s,
untouched.
Transactions that roll back
Section titled “Transactions that roll back”Use transaction() from @theoven/db, not Drizzle’s own on SQLite — see the warning below:
import { transaction } from '@theoven/db'
await transaction(ctx.db, async (tx) => { await tx.insert(orders).values(order) await tx.update(stock).set({ count: remaining }).where(eq(stock.sku, order.sku)) // Anything thrown here rolls both statements back, awaits included.})To wrap a whole request instead, register the middleware — a request that throws writes nothing:
import { transactional } from '@theoven/db'
app.use(transactional())What it creates
Section titled “What it creates”Files: the SQLite database file at url (default ./data.db) when it does not exist —
.gitignore it. Postgres creates nothing locally.
Tables: only what your own schema declares. This brick never migrates anything on its own.
Generate and apply migrations with drizzle-kit — see Limitations on oven db.
Transactions
Section titled “Transactions”Sharing one connection
Section titled “Sharing one connection”Something may need a client before the app exists — auth-basic
builds its store at construction, so it takes one. Let the brick adopt yours rather than opening
a second:
const sqlite = new Database('./data.db')const client = drizzle(sqlite, { schema })
const app = createApp() .use(db(drizzleSqlite({ client: sqlite, schema }))) .use(auth(basicAuth({ db: client, secret })))Two connections to a file work and waste a handle. Two connections to :memory: are two
separate databases — which shows up as no such table on a schema you can watch being created,
and is a genuinely confusing hour.
An adopted connection is not closed on shutdown: it was not the brick’s to open.
Capabilities
Section titled “Capabilities”connect, health, close |
✓ both drivers |
transaction |
✓ both — SQLite via explicit begin/commit/rollback, and serialised |
| Adopting an existing client | ✓ SQLite (client) |
| Connection pooling | Postgres only (max); SQLite is one connection by design |
| Migrations | ✗ — drizzle-kit’s job, not this brick’s |
| MySQL | ✗ — no entry point yet |
What fails at boot: an unreachable database, or a SQLite path that cannot be opened. With
checkOnBoot (on by default) that is one failed restart instead of a deploy that looks green and
500s on its first request.
How it is verified
Section titled “How it is verified”SQLite runs on every bun test: booting in memory and from a file, data surviving a restart, a bad
path refused at boot, insert/select through ctx.db, and that the client really is Drizzle’s API
rather than a wrapper. Health is checked live, and the mounted endpoint too.
Transactions get the most attention, because this is where the provider does something of its own:
commit, rollback on failure, and — kept as an explicit test — a demonstration that Drizzle’s own
bun-sqlite transaction does not roll back an async failure, so the difference stays a documented
fact rather than folklore. The transactional() middleware is tested for rollback on a failed
request, commit on a successful one, path scoping, and doing nothing when no database is registered.
Tuning is asserted by reading the pragmas back, on and off.
Postgres runs the same shape — connect, typed ctx.db, async rollback, commit, request-scoped
transaction, pool release — but only when POSTGRES_URL is set. Without it the suite skips with a
printed notice, and it has not run on the author’s machine; CI provides the server.
Limitations
Section titled “Limitations”- MySQL is untested. Drizzle supports it; this provider has no MySQL entry point yet.
oven db generateis not wired up. The CLI reports that the command needs this package; migrations are run withdrizzle-kitdirectly for now.- One connection per app. Read replicas and multiple databases are not yet supported.