Backend & Cloudflare platform engineer

Upload Dewek: edge-native DAM on Cloudflare

Serverless Digital Asset Management (DAM) API on Cloudflare's edge. Uploads go directly to R2 via S3 presigned POST, so ingestion is zero-compute. D1 holds metadata and Cloudflare Images applies transforms on the fly.

Year
2026
Stack
Cloudflare WorkersCloudflare R2Cloudflare D1TypeScriptHonoS3 API
GitHub ↗

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:

  1. Client requests an upload URL from the API
  2. API generates an S3-compatible presigned POST URL pointing directly at R2
  3. Client uploads straight to R2, so the binary payload never passes through Worker memory
  4. Client confirms the upload. Worker verifies the object exists via R2 HEAD and flips D1 status pending → 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 validated

Why 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:

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

DecisionRationale
Presigned POST over proxyBinary never hits Worker memory, which avoids OOM, stays inside 128MB/10ms, and keeps egress at $0
aws4fetch over AWS SDK11kB vs 300kB+, stays inside the edge CPU and bundle limits
D1 conditional update over Durable ObjectsUNIQUE(r2_key) + WHERE status='pending' is atomic for 0-1000 users. Durable Objects are YAGNI until contention shows up
D1 over KVRelational queries for asset search and filtering
Hono over itty-routerType-safe routing and composable middleware

What exists and what's next

Scaffolding complete. What exists today in upload-dewek (src/index.ts:8):

Not yet implemented. Next increment:

Planned after validation: