Upload Dewek: a serverless DAM on Cloudflare's edge
What it is
Upload Dewek is a headless Digital Asset Management API that runs entirely on Cloudflare's edge. No servers, no container orchestration, no cold starts. It is designed for the constraints that matter: 128MB RAM, 10ms CPU time, $0 infra, 0 users. So YAGNI and KISS over premature scale.
The core flow is zero-compute ingestion:
- Client requests an upload URL from the API
- API generates an S3-compatible presigned POST URL pointing directly at R2
- Client uploads straight to R2, so the binary payload never passes through Worker memory
- Client confirms the upload. Worker verifies the object exists via
R2 HEADand flips D1 statuspending → validated
This eliminates the classic anti-pattern of proxying file uploads through the application server and avoids OOM on the edge.
How an upload flows
sequenceDiagram
participant Client
participant Worker as Cloudflare Worker<br>(Hono)
participant R2 as R2 Bucket
participant D1 as D1 Database
Client->>Worker: POST /upload/init
Worker->>D1: Record pending asset<br>(id, r2Key, status=pending)
Worker->>R2: Generate presigned POST URL
Worker-->>Client: Return presigned URL + fields
Client->>R2: POST {presigned-url} (Direct, bypasses Worker)
Client->>Worker: POST /upload/confirm {assetId}
Worker->>R2: HEAD {r2Key} (verify exists)
Worker->>D1: UPDATE assets SET status='validated'<br>WHERE id=? AND status='pending'
Worker-->>Client: 200 validatedWhy no Durable Objects, queues, or Wasm?
The V1 whiteboard had distributed message queues, isolated background workers, and custom Wasm for image processing. All scrapped for YAGNI:
- With 0 users, a per-asset mutex is unnecessary. D1's
UNIQUE(r2_key)+ conditionalUPDATE ... WHERE status='pending'is atomic enough. A second concurrent confirm matches 0 rows, so no duplicate record is created. - Durable Objects require a paid Workers subscription, which conflicts with the
$0constraint. - At 128MB and 10ms of CPU, a plain Worker transaction costs microseconds. A Durable Object hop adds latency and state to manage.
If we ever need async pipelines (thumbnails, metadata extraction), we'll add them then. Not before.
Signing at the edge, transforms at the CDN
Presigned URL generation
R2 is S3-compatible. The Worker generates presigned URLs at the edge without proxying bytes, using the ~11kB aws4fetch instead of @aws-sdk/signature-v4, which blows the 10ms CPU and bundle limits:
import { AwsClient } from "aws4fetch";
const client = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
region: "auto",
service: "s3",
});
// expires in 15 min, scoped to exact r2Key + content-type
const url = await client.sign(
new Request(`https://${env.R2_BUCKET}.r2.cloudflarestorage.com/${r2Key}?X-Amz-Expires=900`, {
method: "PUT",
}),
{ aws: { signQuery: true } }
);For POST, the same signer returns the URL + fields (policy, signature) for a direct HTML form post, with zero Worker egress.
Image transforms via Cloudflare Images
No Wasm in the Worker. Transforms are offloaded to Cloudflare Images at the CDN layer, so the Worker spends zero compute:
https://assets.dewek.id/{asset-id}?width=800&format=webp&quality=85
Served from R2 cache via a Workers Images binding (IMAGES in wrangler.jsonc:29), not a custom processing pipeline.
Why these choices
| Decision | Rationale |
|---|---|
| Presigned POST over proxy | Binary never hits Worker memory, which avoids OOM, stays inside 128MB/10ms, and keeps egress at $0 |
aws4fetch over AWS SDK | 11kB vs 300kB+, stays inside the edge CPU and bundle limits |
| D1 conditional update over Durable Objects | UNIQUE(r2_key) + WHERE status='pending' is atomic for 0-1000 users. Durable Objects are YAGNI until contention shows up |
| D1 over KV | Relational queries for asset search and filtering |
| Hono over itty-router | Type-safe routing and composable middleware |
What exists and what's next
Scaffolding complete. What exists today in upload-dewek (src/index.ts:8):
- Hono bootstrap +
GET /health,AppErrorregistrysrc/lib/core/errors.ts:3, JSend responsessrc/lib/http/api-response.ts:25,drizzle-orm/D1schema (projects,api_keys,assetswithpending/validated/rejectedsrc/lib/db/schema/assets.ts:21) wrangler.jsonc:2bindings:DB (D1),ASSETS (R2),CACHE (KV),IMAGES
Not yet implemented. Next increment:
POST /upload/init→ presigned POST generation +D1 INSERT pendingPOST /upload/confirm→R2 HEAD+ conditionalUPDATEGET /assets/:idwith Images transforms
Planned after validation:
- Webhook support for post-upload pipelines
- Multi-tenant workspace isolation
- CLI for CI/CD integration