Bringing SQL to the Edge with Cloudflare D1

Static sites are fast, but they lack the ability to serve truly dynamic content. Cloudflare D1, now in open alpha, aims to close that gap by adding SQL database capabilities directly to the Workers ecosystem. Instead of rebuilding your site every time data changes, D1 lets you keep your frontend static while running a queryable API alongside it on the edge.

Why SQL Belongs in Serverless

Cloudflare's developer platform already offers Workers KV for key-value storage and Durable Objects for coordinated, real-time state. But neither of those fills the role of a relational database. SQL gives you declarative queries with indexes for fast lookups and joins for expressing relationships between tables. For many applications—comment systems, user-generated content, or anything needing structured data—that's exactly the right tool.

D1 separates the dynamic data layer from the static frontend. The API and database run independently of the site itself, which makes deployment straightforward: your static assets go to Cloudflare Pages, and the D1-powered API runs on Workers.

Building a Dynamic Comments API

To demonstrate D1 in action, we'll build a simple JSON API for adding comments to a static blog. Start by creating a new Workers project with Wrangler:

$ mkdir d1-example && d1-example
$ wrangler init

For rapid API development, we'll use Hono, a lightweight framework in the Express.js style. Install it via NPM:

$ npm install hono

In src/index.ts, initialize a Hono app with two endpoints: a GET route to fetch comments for a post and a POST route to create new ones.

import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.get('/api/posts/:slug/comments', async c => {
  // do something
})

app.post('/api/posts/:slug/comments', async c => {
  // do something
})

export default app

Creating and Configuring the Database

Wrangler now includes the wrangler d1 subcommand, letting you create and manage D1 databases from the command line. Create a new database with a single command:

$ wrangler d1 create d1-example

Once created, associate the database with your project using a binding defined in wrangler.toml. Bindings are named references to Cloudflare resources like D1 databases, KV namespaces, or R2 buckets, giving you a simple variable to access them in your code. Here, we bind the database to the variable DB:

[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "d1-example"
database_id = "4e1c28a9-90e4-41da-8b4b-6cf36e5abb29"

Note that the [[d1_databases]] directive requires a beta version of Wrangler. Install it with npm install -D wrangler/beta.

Seeding and Querying Data

With the binding configured, you can interact with the database directly from the terminal. Issue raw SQL commands using wrangler d1 execute:

$ wrangler d1 execute d1-example --command "SELECT name FROM sqlite_schema WHERE type ='table'"
Executing on d1-example:
┌─────────────────┐
│ name │
├─────────────────┤
│ sqlite_sequence │
└─────────────────┘

You can also pass a full SQL file—useful for one-command data seeding. Create src/schema.sql to define a comments table:

drop table if exists comments;
create table comments (
  id integer primary key autoincrement,
  author text not null,
  body text not null,
  post_slug text not null
);
create index idx_comments_post_id on comments (post_slug);

-- Optionally, uncomment the below query to create data

-- insert into comments (author, body, post_slug)
-- values ("Kristian", "Great post!", "hello-world");

Execute it against the database with the --file flag:

$ wrangler d1 execute d1-example --file src/schema.sql

From your Workers function, the DB binding is now available. Use it to prepare and run SQL statements. To retrieve comments matching a post slug:

app.get('/api/posts/:slug/comments', async c => {
  const { slug } = c.req.param()
  const { results } = await c.env.DB.prepare(`
    select * from comments where post_slug = ?
  `).bind(slug).all()
  return c.json(results)
})

This handler takes a slug URL query parameter, runs a SELECT statement to find all comments where post_slug matches, and returns them as JSON.

Inserting new records works the same way. Add a POST endpoint that accepts a comment and writes it to the database:

app.post('/API/posts/:slug/comments', async c => {
  const { slug } = c.req.param()
  const { author, body } = await c.req.json<Comment>()

  if (!author) return c.text("Missing author value for new comment")
  if (!body) return c.text("Missing body value for new comment")

  const { success } = await c.env.DB.prepare(`
    insert into comments (author, body, post_slug) values (?, ?, ?)
  `).bind(author, body, slug).run()

  if (success) {
    c.status(201)
    return c.text("Created")
  } else {
    c.status(500)
    return c.text("Something went wrong")
  }
})

You can see the full source for this D1-powered comments API in the cloudflare/templates/worker-d1-api repository.

The Static/Dynamic Tradeoff, Solved

Tools like Hugo and Gatsby made static sites remarkably performant—build times of seconds with minimal asset sizes. But moving away from a platform like WordPress meant giving up dynamic features like user comments. Developers patched this by adding complexity to their build processes, fetching and generating pages whenever data changed.

That approach simulates dynamism through repeated rebuilds and redeploys. D1 offers an alternative: keep the site static and let the dynamic data live behind a queryable API, deployed geographically close to your users. For anyone who wants data-driven applications without managing database infrastructure, D1 provides an approachable SQL on-ramp that doesn't sacrifice performance or developer experience.