06 — D1: Serverless SQLite at the Edge
"A database for Workers, somehow" was my D1 summary, and the "somehow" hid the interesting part. The model that pinned it: D1 is serverless SQLite running on Cloudflare's network, and a Worker reaches it through a binding — not a connection string — which is the part that makes it actually feel edge-native. [1] The query language and schema are plain SQLite; the shift is in where it lives and how you talk to it.
The framing that landed is the comparison with what came before. A traditional database is a server in a region. My Worker, running next to the user, has to open a TCP connection across the world to reach it, pay that latency on every query, and manage connection pooling because each Worker invocation is short-lived. D1 collapses that: the database is on Cloudflare's network, the binding is a direct in-network channel, and there's no connection to pool [1][2]. The Worker reads and writes SQL with single-digit-millisecond latency to the nearest D1 replica.
Schema management
D1 uses SQLite-compatible DDL, so anything I know about SQLite schema transfers directly [3]. Tables, columns, data types, primary and foreign keys, indexes — all standard SQL. I design the schema from the application's data needs, write the CREATE TABLE statements, and apply them via wrangler:
wrangler d1 execute my-db --file=./schema.sqlThe decisions worth slowing down for are the usual SQL ones: data types for storage efficiency, indexes on columns used in WHERE/JOIN/ORDER BY, and foreign-key relationships. Because D1 is SQLite, the type system is flexible (SQLite uses dynamic typing), but I treat it as if it were strict — predictable types make for predictable queries.
Migrations
Schema evolves, and migrations are the structured way to apply changes repeatably [4]. The pattern:
- Write a numbered SQL file (0001_add_email_index.sql) containing ALTER TABLE or CREATE INDEX statements.
- Apply it with wrangler d1 execute --file=./migrations/0001_*.sql.
- Track which migrations have been applied (a _migrations table, or a tool that tracks it for you).
- Apply them in order, against every environment.
Two disciplines that pay off: version every migration script and apply them in order, and never edit a migration that's already been applied in production — write a new one to reverse it. Migrations are append-only history, and treating them that way prevents the data-inconsistency bugs that bite when schemas drift between environments [4].
Query optimization
D1 inherits SQLite's query optimizer, but I can't pretend it's magic — bad queries are still bad queries [5]. The practices worth internalizing:
- Index for your access patterns. Every column that shows up in a WHERE, JOIN, or ORDER BY deserves an index. Without one, the query falls back to a full table scan, which is linear in table size.
- Be explicit about columns. SELECT name, email instead of SELECT *. Fewer bytes transferred, and the query survives schema additions without surprising me.
- Filter early. Push WHERE clauses as deep as possible so joins operate on the smallest intermediate result sets.
- Read the plan. SQLite's EXPLAIN QUERY PLAN shows whether a query uses an index or scans. If a slow query shows SCAN TABLE, that's the smoking gun — add an index or rewrite the predicate.
- Consider denormalization deliberately. Sometimes duplicating a column to avoid a join is the right trade. It costs storage and consistency effort, so it's a choice, not a default.
Drizzle: the TypeScript ORM I reach for
Drizzle is a TypeScript-first ORM — an "object-relational mapper," which is a layer that turns my database tables into typed objects I query from code instead of raw SQL strings — and it targets D1 directly. It's become my default [6]. The pitch:
- Schema in TypeScript. I define tables and columns as typed objects, and Drizzle generates the SQL DDL for migrations.
- Type-safe query builder. Queries are constructed in TS with full type inference — the result's shape is known at compile time, and refactors that break a query show up as type errors.
- Migration tooling. Drizzle generates and applies migrations, integrating cleanly with the D1 workflow.
A Drizzle query reads almost like SQL but with end-to-end types:
const result = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(eq(users.id, 42));The result is { id: number; name: string }[], and any column rename propagates as a type error. For a TypeScript project on D1, this is the path of least friction.
Prisma: when you want its ecosystem
Prisma is the other major ORM, and it works with D1 through a dedicated adapter [7]. Its strengths are the things Drizzle trades away: a richer schema language, a generated type-safe client with a more opinionated API, mature migrations, and an ecosystem of tooling (Prisma Studio, the data platform).
The trade-off is that Prisma historically assumed a long-lived connection to a traditional Postgres/MySQL database, and the D1 adapter is the bridge that makes it work in the binding-based, serverless world of Workers. For a team already deep in Prisma, the adapter lets them bring their existing schemas and workflows. For a greenfield D1 project, Drizzle's lighter footprint and tighter D1 integration usually win.
How I use this
D1 is my default when a Worker needs structured, queryable, relational data — user accounts, application records, anything where SQL is the natural shape. The pattern I keep: schema designed upfront, migrations versioned and append-only, Drizzle for type-safe queries, and EXPLAIN QUERY PLAN consulted the moment a query feels slow. When I need single-object strong consistency across concurrent writers (real-time coordination, counters that must be exact), that's the signal to move that specific piece to Durable Objects rather than fighting D1's read/write model.
References
[1] Cloudflare, "Cloudflare D1 documentation," Cloudflare Docs, 2024. [Online]. Available: https://developers.cloudflare.com/d1/
[2] Cloudflare, "Getting started with D1," Cloudflare Docs, 2024. [Online]. Available: https://developers.cloudflare.com/d1/get-started/
[3] Cloudflare, "Schema validation · Cloudflare API Shield," Cloudflare Docs. [Online]. Available: https://developers.cloudflare.com/api-shield/security/schema-validation/
[4] Prisma, "Database migrations: what are the types of DB migrations?," Prisma Data Guide. [Online]. Available: https://www.prisma.io/dataguide/types/relational/what-are-database-migrations
[5] Cloudflare, "Query parameters and cached responses," Cloudflare Docs. [Online]. Available: https://developers.cloudflare.com/automatic-platform-optimization/reference/query-parameters/
[6] Drizzle, "Drizzle ORM," 2024. [Online]. Available: https://orm.drizzle.team/
[7] Prisma, "Prisma documentation," 2024. [Online]. Available: https://www.prisma.io/docs
Knowledge check · Question 1 of 5
How does a Worker connect to a D1 database?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!