Using D1
Drizzle queries, dual-DB mode, and schema patterns with Cloudflare D1
This page covers how to use Cloudflare D1 with Drizzle ORM in your Quickback Stack application.
Drizzle Queries
The generated API uses Drizzle ORM for all database operations:
import { db } from '../db';
import { rooms } from '../schema';
import { eq } from 'drizzle-orm';
// Select
const allRooms = await db.select().from(rooms);
// Where clause
const room = await db.select().from(rooms).where(eq(rooms.id, id));
// Insert
await db.insert(rooms).values({ name: 'Conference A' });Dual-DB Mode
Quickback supports separate databases for auth and application data, useful for multi-tenant SaaS apps.
Migrations
Migrations are generated by the compiler using drizzle-kit generate. Apply them with:
npx wrangler d1 migrations apply my-dbBackend stalls
D1 statements occasionally wait tens of seconds in Cloudflare's backend
queue (a noisy neighbour on the shared execution queue — not your workload),
and the D1 binding has no deadline or AbortSignal of its own. Because every
query runs through generated code, the deadline is a config option rather than
something you wrap by hand:
database: defineDatabase("cloudflare-d1", {
queryTimeoutMs: 5000,
}),With it set, every D1 handle the compiler emits — the scoped db, the unsafe
db, the Better Auth adapter, cron / queue / email runners, the files and
audit databases — races each statement against the deadline:
| Statement | Response | Client should |
|---|---|---|
Read (SELECT, WITH, EXPLAIN, PRAGMA) | 503 DB_TIMEOUT | Retry. The read had no side effects |
Write (anything else, and any batch containing a write) | 500 DB_WRITE_OUTCOME_UNKNOWN | Re-read the record, then decide. The statement was not cancelled on the backend and may still commit |
Each timeout also logs one structured line —
{ level: "error", event: "DB_DEADLINE", binding, kind, ms, sql } (SQL
truncated to 200 chars). Workers Logs indexes the JSON fields, so in the
dashboard or wrangler tail --format json filter on event = "DB_DEADLINE"
and group by binding / kind to track stalls over time. The response the
client saw carries the same binding in details. Unset, nothing changes: no wrapper is emitted and queries are
unbounded, as before. The option is D1-only; Postgres drivers carry their own
statement timeouts. With readReplication on, the deadline wraps the request's D1 session too.
Related
- D1 Setup — Bindings and wrangler config
- Read replication — Serve reads from regional replicas with the Sessions API
- Schema — Defining your database schema