Cloudflare D1 + R2 + Workers: Build a Full-Stack App at the Edge for Free

D1 gives you SQLite at the edge, R2 gives you S3-compatible object storage with free egress - here is how to wire them together in a Workers app with Drizzle ORM.

Mahmudul Haque Qudrati — CEO & ML Engineer at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

April 17, 2026
7 min read
Cloudflare D1 + R2 + Workers: Build a Full-Stack App at the Edge for Free

The Cloudflare Edge Stack

One AI engineering post, weekly

LLM benchmarks, prompt techniques, and token-cost breakdowns — not another AI news roundup.

Cloudflare's edge platform has matured into a full-stack environment. D1 (SQLite at edge), R2 (object storage), and Workers (JavaScript runtime) together let you build complete applications that run globally with sub-10ms latency - and the free tier is genuinely useful.

Cloudflare D1

D1 is SQLite distributed across Cloudflare's edge network. The free tier gives you 5GB storage and 5 million row reads per day.

bash
# Create a D1 database
wrangler d1 create my-app-db

# Add to wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "your-database-id"
typescript
// src/index.ts
import { Hono } from "hono";

type Bindings = { DB: D1Database };

const app = new Hono<{ Bindings: Bindings }>();

app.get("/files", async (c) => {
  const result = await c.env.DB
    .prepare("SELECT * FROM files ORDER BY created_at DESC")
    .all();
  return c.json(result.results);
});

Team workspace

Ship faster with chat, meetings, and projects in one place — Zlyqor.

Start free

Drizzle ORM With D1

Drizzle has first-class D1 support - the only ORM that does:

typescript
// schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const files = sqliteTable("files", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  key: text("key").notNull(),   // R2 object key
  size: integer("size").notNull(),
  createdAt: text("created_at").default("CURRENT_TIMESTAMP"),
});
typescript
// src/db.ts
import { drizzle } from "drizzle-orm/d1";

export function getDb(d1: D1Database) {
  return drizzle(d1);
}

Cloudflare R2

R2 is S3-compatible object storage. The key advantage: zero egress fees. You pay for storage (~$0.015/GB/month) but not for serving files to users.

typescript
// wrangler.toml
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-app-files"

File Upload App: End-to-End Example

typescript
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/d1";
import { files } from "./schema";

type Bindings = { DB: D1Database; BUCKET: R2Bucket };

const app = new Hono<{ Bindings: Bindings }>();

// Upload endpoint
app.post("/upload", async (c) => {
  const form = await c.req.formData();
  const file = form.get("file") as File;

  if (!file) return c.json({ error: "No file" }, 400);

  // 1. Store file in R2
  const key = `${crypto.randomUUID()}/${file.name}`;
  await c.env.BUCKET.put(key, file.stream(), {
    httpMetadata: { contentType: file.type },
  });

  // 2. Store metadata in D1
  const db = drizzle(c.env.DB);
  const record = { id: crypto.randomUUID(), name: file.name, key, size: file.size };
  await db.insert(files).values(record);

  return c.json({ id: record.id, key });
});

// Download endpoint
app.get("/files/:id", async (c) => {
  const db = drizzle(c.env.DB);
  const [record] = await db.select().from(files)
    .where(eq(files.id, c.req.param("id")));

  if (!record) return c.json({ error: "Not found" }, 404);

  const object = await c.env.BUCKET.get(record.key);
  if (!object) return c.json({ error: "File missing" }, 404);

  return new Response(object.body, {
    headers: { "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream" },
  });
});

export default app;

Local Development and Deploy

bash
# Run locally with live D1 + R2 simulation
wrangler dev

# Apply migrations to local D1
wrangler d1 execute my-app-db --local --file=./migrations/001.sql

# Deploy to production
wrangler deploy

# Apply migrations to production D1
wrangler d1 execute my-app-db --file=./migrations/001.sql

D1 vs Neon vs PlanetScale

D1NeonPlanetScale
Free tier5GB, 5M reads/day512MB, 3 projectsRemoved free tier
Edge supportNativeVia HTTP adapterVia HTTP adapter
SQL dialectSQLitePostgreSQLMySQL
Egress costFreeStandardStandard

D1 is ideal for edge-native apps. Neon is better for PostgreSQL features and larger datasets.

References: D1 docs · R2 docs · Workers

#cloudflare-d1#cloudflare-r2#workers#edge#sqlite

// discussion

Comments

0/4000
Mahmudul Haque Qudrati — CEO & ML Engineer at Pristren

Mahmudul Haque Qudrati

CEO & ML Engineer

Visionary technologist, software engineer, and machine learning specialist. Founder and CEO of Pristren, directing engineering teams that ship production-grade AI/ML pipelines, mission-critical full-stack applications, and developer tooling. Creator of Zlyqor, the unified team workspace platform. Author of 540+ technical guides and benchmark research reports on large language models, agentic workflows, Model Context Protocol (MCP), and modern web stacks.

PristrenZlyqor