Hono + Cloudflare D1 security — edge-native SQL stack

Hono + Cloudflare D1 is the edge-native minimalist stack. Workers isolate by design, D1 is a managed SQLite that lives in the same Cloudflare backbone, and Hono is a tiny framework that composes cleanly with Worker bindings. The security model is thin: most of what you need is already enforced by the platform (Worker isolation, HTTPS-only, fetch origin). Your risk is in three places. Binding scope is first. wrangler.toml lets you bind D1, KV, R2, and Durable Objects to each environment. If you accidentally bind your production D1 to a preview environment (or vice versa), you can leak or corrupt data during testing. Review bindings per environment on every wrangler.toml change. D1 queries need parameterized bindings. The D1 API has `prepare('...').bind(v1, v2)` — never template-string concatenate. Request body size unbounded kills Workers. Workers bill per CPU millisecond; an attacker sending a 100 MB JSON body to your Worker makes you pay for parsing it. Cap request body size at the middleware layer, rate-limit by IP, and reject oversized requests before they reach your handlers.

What breaks on this stack

D1 SQL injection via template strings

`db.prepare("select * from users where id = ${id}").all()` is injection-prone. Always use `.bind(value)`: `db.prepare('select * from users where id = ?').bind(id).all()`.

wrangler.toml binding scope mismatch

A single wrong environment label on a binding can route preview traffic to production data. Review `[env.*]` sections of wrangler.toml on every deploy.

Unbounded request body size

Workers don't cap request body by default. A 10 MB JSON POST consumes parse CPU for its full size. Add Content-Length check in middleware or use Hono's body-limit helper.

KV or R2 binding too broad

A KV binding with write access when only read is needed increases blast-radius of a Worker compromise. Audit bindings for minimum required permissions per environment.

Missing rate limiting

Workers are cheap per-request but not free. An attacker can loop requests to exhaust your CPU quota. Use Cloudflare Rate Limiting rules at the edge, not just application-level.

Pre-ship checklist

  • All D1 queries use .bind() — no template-string concatenation
  • wrangler.toml [env.*] bindings reviewed on every deploy
  • Content-Length check or body-limit middleware on every route
  • Per-IP rate limiting configured in Cloudflare dashboard
  • KV/R2 bindings use minimum required access (read-only where possible)
  • Secrets managed via `wrangler secret put`, not wrangler.toml
  • CORS policy explicit per route (not wildcard)
  • Hono secure() middleware or manual security headers applied
  • D1 migrations version-controlled and applied via `wrangler d1 execute`
  • Durable Object bindings scoped per tenant

Starter config

# wrangler.toml — environment-scoped bindings
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-04-01"

[[d1_databases]]
binding = "DB"
database_name = "my-api-prod"
database_id = "xxx-prod-uuid"

[env.preview]
[[env.preview.d1_databases]]
binding = "DB"
database_name = "my-api-preview"
database_id = "xxx-preview-uuid"

# Secrets via: wrangler secret put STRIPE_SECRET

// src/index.ts — Hono + D1 + security headers + body limit
import { Hono } from "hono";
import { secureHeaders } from "hono/secure-headers";
import { bodyLimit } from "hono/body-limit";

type Env = { DB: D1Database; STRIPE_SECRET: string };
const app = new Hono<{ Bindings: Env }>();

app.use("*", secureHeaders());
app.use("*", bodyLimit({ maxSize: 100 * 1024 })); // 100 KB limit

app.get("/orders/:id", async (c) => {
  const user = c.get("user"); // set by auth middleware
  const { id } = c.req.param();
  const row = await c.env.DB
    .prepare("select * from orders where id = ? and user_id = ?")
    .bind(id, user.id)
    .first();
  if (!row) return c.notFound();
  return c.json(row);
});

export default app;