Schema Design Patterns for Cloudflare D1

Priya Nair · · 13 views

D1 is SQLite at the edge. This guide covers the schema patterns that work well with D1's execution model, including pagination, full-text search, and JSON columns.

D1 is SQLite

D1 databases are SQLite files served from Cloudflare's infrastructure. This gives you the full power of SQL — JOINs, transactions, CTEs, window functions — without the operational overhead of a traditional database server.

Pagination: Cursor vs Offset

Offset pagination (LIMIT 10 OFFSET 30) is simple but degrades with large tables. For D1, prefer cursor-based pagination:

-- Cursor pagination (fast even at large offsets)
SELECT * FROM articles
WHERE published_at < ?   -- cursor: last seen published_at
ORDER BY published_at DESC
LIMIT 10;

Full-Text Search

D1 supports SQLite FTS5:

CREATE VIRTUAL TABLE articles_fts USING fts5(
  title, excerpt, content,
  content=articles,
  content_rowid=id
);

-- Populate
INSERT INTO articles_fts SELECT title, excerpt, content FROM articles;

-- Query
SELECT a.* FROM articles a
JOIN articles_fts fts ON a.id = fts.rowid
WHERE articles_fts MATCH ?
ORDER BY rank;

JSON Columns

SQLite has native JSON functions. Store flexible metadata in a TEXT column:

ALTER TABLE articles ADD COLUMN meta TEXT DEFAULT '{}' CHECK(json_valid(meta));

-- Query a JSON field
SELECT * FROM articles WHERE json_extract(meta, '$.featured') = 1;

Transactions in D1

D1 supports batched statements that execute in a single round-trip:

const [articles, tags] = await db.batch([
  db.prepare("SELECT * FROM articles LIMIT 10"),
  db.prepare("SELECT * FROM tags"),
]);

Use .batch() for reads that need to be consistent, and .run() wrapped in explicit BEGIN/COMMIT for writes.