Disclosure: I'm the maker of the site below.
I recently launched Colorize Old Photos, a small web app that colorizes black and white photos and repairs scratches, creases and fading. You get a free watermarked preview; full-resolution downloads use one-time credit packs (no subscription). This post is a short write-up of the stack and the things that surprised me.
Stack
- Next.js 16 (App Router) deployed to Cloudflare Workers with OpenNext
- D1 for accounts, credits, jobs and rate limits; R2 for uploads and results (1-day lifecycle rule)
- Cloudflare Images binding for the watermarked 720px preview
-
fal.ai for the models:
fal-ai/ddcolorfor colorizing,fal-ai/nano-banana-2/editat 2K for restoration - PayPal Orders v2 + webhook for payments, Google OAuth and email magic links for login
1. Workers can't wait for a slow model
A cold DDColor start on fal can take about 90 seconds, and a Worker request can't just sit there. I ended up with a poll-driven job flow: the client submits, the server stores the job and returns an id, and each poll asks fal for the status and advances the job. No background task has to outlive the request.
2. Models: the cheap ones didn't fix damage
I tested a damaged copy of a public-domain Library of Congress photo. The "standard" tier (CodeFormer + DDColor) colorizes fine but doesn't remove scratches or creases, and CodeFormer smooths faces. An image-editing model (nano-banana-2) was the only option that both cleaned the damage and stayed faithful to the original. So colorize-only uses DDColor, and anything involving restoration uses the editing model.
3. "Deleted within 24 hours" wasn't true until I set a header
My privacy page promised uploads are deleted within 24 hours. R2 handled my copy, but fal keeps generated media indefinitely unless each request sets an object lifecycle preference header. I now send X-Fal-Object-Lifecycle-Preference with a 3600 second expiry on every submit and verified that the file really stops being served. If you promise deletion, check your providers' copies too.
4. Free previews need a hard cost cap
Anyone can upload a photo for a free preview, so I added a daily USD budget cap, per-IP, per-subnet and per-device rate limits (fixed windows in D1), Turnstile, and an upload moderation step. It's cheap insurance against someone scripting the free tier.
5. Small things
- Cloudflare Images can't decode HEIC, so iPhone photos are converted client-side first.
- NEXT_PUBLIC_* variables must be build variables on Workers Builds, not runtime ones.
- Dashboard-only vars get wiped by
wrangler deployunless you setkeep_vars: true.
If you try it, I'd genuinely like feedback on the before/after quality, especially faces: colorizeoldphotos.com. Happy to answer questions about any part of the stack.
Top comments (0)