A TODO app running on Cloudflare Containers with a D1 SQLite database.
Browser
│
▼
Cloudflare Worker (src/index.ts)
├── GET/POST/PATCH/DELETE /api/todos → D1 via Drizzle ORM
├── POST /api/todos → RateLimiter Durable Object (20 req/hour/IP)
└── /* → proxy to Container
│
▼
Bun HTTP server (app/server.ts)
Serves HTML + client-side JS
Why two runtimes?
D1 is a Cloudflare-native binding — it has no TCP endpoint and is only accessible from the Workers runtime. The container (Bun) handles stateless UI serving; the Worker owns all database access.
| Layer | Technology |
|---|---|
| Edge runtime | Cloudflare Workers |
| Container runtime | Cloudflare Containers (Firecracker, linux/amd64) |
| Container server | Bun |
| Database | Cloudflare D1 (SQLite) |
| ORM | Drizzle ORM |
| Rate limiting | Durable Object (in-memory, per-IP, hourly bucket) |
| Domain | todo.thecodeorigin.com |
├── app/
│ ├── server.ts # Bun HTTP server — serves UI, /api/instance
│ └── tsconfig.json # Bun-specific TS config (bun-types)
├── migrations/
│ ├── 0001_init.sql # todos table
│ └── 0002_rate_limits.sql # rate_limits table (unused, kept for history)
├── src/
│ ├── index.ts # Worker entry — API routes, RateLimiter DO, TodoContainer
│ └── schema.ts # Drizzle table definitions
├── Dockerfile # FROM oven/bun:1-alpine
├── tsconfig.json # Workers TS config (@cloudflare/workers-types)
└── wrangler.jsonc # Cloudflare deploy config
- Node.js + npm
- Docker Desktop
- Wrangler CLI (
npm i -g wrangler) - A Cloudflare account with
thecodeorigin.comon Cloudflare DNS
npm installSet your Cloudflare API token (requires Workers, D1, and Containers permissions):
export CLOUDFLARE_API_TOKEN=<your-token>
# Windows PowerShell:
# $env:CLOUDFLARE_API_TOKEN = "<your-token>"Create the D1 database (first time only):
npx wrangler d1 create todo-db
# Copy the database_id into wrangler.jsoncApply migrations:
npx wrangler d1 migrations apply todo-db --remotenpm run dev # wrangler dev (Workers local emulation)The container is not emulated locally — /api/todos routes work via wrangler dev, but the UI proxy to the container won't resolve. Run bun app/server.ts separately on port 3000 if you need the full UI locally.
npm run deploy # wrangler deployWrangler builds and pushes the Docker image to Cloudflare's registry, deploys the Worker, and wires up the custom domain automatically.
POST /api/todos is limited to 20 requests per IP per hour, enforced by a RateLimiter Durable Object. Each IP maps to a named DO instance that keeps an in-memory counter per hourly bucket. No database writes on the hot path.
Exceeding the limit returns:
HTTP 429 Too Many Requests
Retry-After: 3600
{"error": "Rate limit exceeded — max 20 todos per hour per IP."}