AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 06 — D1: Serverless SQLite at the Edge

06 — D1: Serverless SQLite at the Edge

August 13, 20267 min read
Download as Markdown

"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.

Traditional DB — remote server D1 — edge, via binding Worker · edge DB · one region TCP · pooled · ~100ms connection crosses the world per query Worker · edge D1 · on-network binding · in-network · ~ms no connection string, nothing to pool plain SQLite — schema, queries, syntax all standard await env.DB.prepare("SELECT * FROM users WHERE id=?") .bind(42).first() ORM-friendly: Drizzle and Prisma both target D1

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.sql

The 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:

  1. Write a numbered SQL file (0001_add_email_index.sql) containing ALTER TABLE or CREATE INDEX statements.
  2. Apply it with wrangler d1 execute --file=./migrations/0001_*.sql.
  3. Track which migrations have been applied (a _migrations table, or a tool that tracks it for you).
  4. 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!