The Cloudflare Edge Stack
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.
# 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"
// 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);
});
Drizzle ORM With D1
Drizzle has first-class D1 support — the only ORM that does:
// 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"),
});
// 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.
// wrangler.toml
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-app-files"
File Upload App: End-to-End Example
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.