Status: Development / Prototype -- This project was built as an exploration of Cloudflare's full-stack platform (Workers, Durable Objects, Queues, Workflows, R2, D1). It is not production-hardened and is shared as-is for reference and learning purposes.
A full-stack application for batch AI image editing. Users upload images, provide a prompt, and the system processes them in parallel using OpenAI's gpt-image-1 model -- with a credit-based billing system, real-time progress tracking, and batch history.
The goal was to build a non-trivial, end-to-end SaaS prototype that exercises as many Cloudflare developer platform primitives as possible in a single project:
- Durable Objects for strongly consistent, per-user credit balances and per-batch coordination state
- Queues for decoupling request handling from async image processing
- Workflows for multi-step image editing with built-in retries
- R2 for object storage (uploaded + edited images)
- D1 for relational data (users, sessions, credit transactions, batch history)
- Workers as the compute layer for everything
The secondary goal was to wire up a real credit-based billing flow (via Polar.sh) so the system could theoretically charge for usage.
┌─────────────────────┐ ┌──────────────────────────────────────────────┐
│ React Frontend │ │ Cloudflare Workers (Hono) │
│ (Cloudflare Workers) │────────▶│ │
│ │ service│ ┌─────────┐ ┌─────────────────────────┐ │
│ React Router │ binding │ │ Auth │ │ Routes │ │
│ TailwindCSS │ │ │(Better- │ │ /api/images/generate │ │
│ Zustand │ │ │ Auth) │ │ /api/batch/:id/status │ │
│ TanStack Query │ │ └─────────┘ │ /api/billing/* │ │
│ Radix UI │ │ │ /api/files/* │ │
└─────────────────────┘ │ │ /api/polar/webhooks │ │
│ └────────┬────────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │UserCreditDO│ │BatchCoord │ │ Queues │ │
│ │ │ │ DO │ │ │ │
│ │ Per-user │ │ Per-batch │ │ image- │ │
│ │ credit │ │ progress │ │ process │ │
│ │ balance │ │ tracking │ │ queue │ │
│ │ (atomic) │ │ (SQLite) │ │ │ │
│ └────────────┘ └────────────┘ │ credit- │ │
│ │ update │ │
│ │ queue │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Workflow: │ │
│ │ ProcessUrl │ │
│ │ │ │
│ │ 1. Verify │ │
│ │ 2. Fetch │ │
│ │ 3. AI Edit │ │
│ │ 4. Save │ │
│ │ 5. Report │ │
│ └────────────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────────────────┐ │
│ │ D1 │ │ R2 │ │ External APIs │ │
│ │(SQL) │ │(Blob)│ │ OpenAI, Polar, │ │
│ │ │ │ │ │ Resend │ │
│ └──────┘ └──────┘ └──────────────────┘ │
└──────────────────────────────────────────────┘
- User uploads images to R2 via the file upload API
- User submits a batch with image keys + a prompt
- Server atomically consumes credits from the UserCreditDO (1 credit per image)
- BatchCoordinatorDO initializes tracking state for the batch
- Individual image tasks are enqueued to the image processing queue
- Queue consumer triggers a Cloudflare Workflow per image
- The workflow fetches the image from R2, calls OpenAI to edit it, saves the result back to R2, and reports status to the BatchCoordinatorDO
- Frontend polls
/api/batch/:id/statusto show real-time progress - When all images are done, the DO persists final state to D1 for history
- UserCreditDO (Durable Object) is the source of truth for each user's credit balance -- atomic reads and writes, no race conditions
- New users get 10 free credits on sign-up
- Credit packs are purchased via Polar.sh (one-time products)
- Polar sends an
order.succeededwebhook, which enqueues a message to the credit update queue, which callsUserCreditDO.addCredits() - All credit changes are logged to a
credit_transactionstable in D1 for auditability
- Each batch gets its own BatchCoordinatorDO instance (keyed by
batchId) - The DO uses SQLite storage (
ctx.storage.sql) with two tables:batch_infoandimage_info - Tracks per-image status (pending / processing / completed / failed)
- Detects batch completion and persists final results to D1
| Layer | Technology |
|---|---|
| Frontend | React Router, TailwindCSS v4, Radix UI, Zustand, TanStack Query |
| Backend | Hono (on Cloudflare Workers) |
| Database | Cloudflare D1 (via Drizzle ORM) |
| Object storage | Cloudflare R2 |
| State coordination | Cloudflare Durable Objects (2: UserCreditDO, BatchCoordinatorDO) |
| Async processing | Cloudflare Queues (2) + Cloudflare Workflows |
| Auth | BetterAuth (self-hosted, Google OAuth + email/password) |
| Payments | Polar.sh |
| AI | OpenAI API (gpt-image-1) |
| Resend | |
| Monorepo | pnpm workspaces + Turborepo |
batch-image-edit/
├── apps/
│ ├── server/ # Hono API (Cloudflare Worker)
│ │ ├── src/
│ │ │ ├── db/ # Drizzle schema & migrations
│ │ │ ├── durable-objects/
│ │ │ │ ├── user-credit.do.ts
│ │ │ │ └── batch-coordinator.do.ts
│ │ │ ├── routes/ # API routes
│ │ │ ├── services/ # AI, storage, email, Firecrawl
│ │ │ ├── workers/ # Queue consumers
│ │ │ └── workflows/ # Cloudflare Workflows
│ │ └── wrangler.jsonc
│ └── web/ # React frontend (Cloudflare Workers)
│ ├── app/
│ │ ├── routes/ # Page routes
│ │ ├── components/ # UI components
│ │ ├── stores/ # Zustand stores
│ │ └── hooks/ # Custom hooks
│ └── wrangler.jsonc
├── packages/
│ └── shared/ # Shared types & utilities
├── turbo.json
└── pnpm-workspace.yaml
- Node.js 20+
- pnpm
- A Cloudflare account with access to Workers, D1, R2, Queues, Durable Objects, and Workflows
- API keys for: OpenAI, Polar.sh, Resend (and optionally Firecrawl)
pnpm installCopy the relevant .dev.vars files into apps/server/ and apps/web/ with your API keys and secrets (these are not committed to the repo).
pnpm run devThis starts both the server (Wrangler) and web (Vite) dev servers via Turborepo.
pnpm run deployDeploys both workers to Cloudflare. Resources declared in wrangler.jsonc (D1 databases, R2 buckets, Queues, Durable Objects, etc.) are automatically provisioned by Wrangler on first deploy.
MIT