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.
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.
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
# 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
D1
Neon
PlanetScale
Free tier
5GB, 5M reads/day
512MB, 3 projects
Removed free tier
Edge support
Native
Via HTTP adapter
Via HTTP adapter
SQL dialect
SQLite
PostgreSQL
MySQL
Egress cost
Free
Standard
Standard
D1 is ideal for edge-native apps. Neon is better for PostgreSQL features and larger datasets.
Practical deep-dives on LLMs, developer tools, and AI engineering. No filler. Unsubscribe any time.
// written byFIG. AUTH-01
530
Mahmudul Haque Qudrati
CEO & ML Engineer
CEO and ML Engineer at Pristren. Builds AI-powered software for teams and writes about machine learning, LLMs, developer tools, and practical AI applications.