How I Built an AI Photo Restoration App with Next.js, Supabase, and Replicate A developer built PixRestorer, an AI photo restoration web app using Next.js, Supabase, and Replicate, and detailed the engineering challenges in a technical walkthrough. The app runs statelessly on Cloudflare Workers, handles atomic credit deductions via SQL, and includes safeguards like SSRF protection and moderation. The developer highlighted the importance of boring engineering over the AI itself. A few months ago I dug out a box of family photos from the 80s. Most were faded, scratched, or had those crease marks where they'd been folded for decades. I tried fixing them in Photoshop, but I had dozens of them and gave up after the second one. So I did what any developer would do: I built a tool for it. This post is a technical walkthrough of what I learned building PixRestorer — an AI photo restoration web app that repairs damaged, blurry, and black-and-white photos in under 30 seconds. Hopefully the architecture decisions, the edge cases, and the failure-handling patterns are useful if you're building something similar. @opennextjs/cloudflare The app itself is completely stateless: every dependency database, auth, AI, storage, payments lives outside the app. That's the single most important architecture decision, and it made deployment trivial. Initially the app ran on a VPS with PM2. Moving to Cloudflare Workers with OpenNext removed the server entirely — there is no Node runtime to manage, no sharp image processing on the origin Cloudflare Image Resizing handles it , and rollbacks are just a DNS change. It wasn't free, though. Three things bit me: proxy.ts only runs on the Node runtime, but Workers only support Edge middleware. I had to keep the old middleware.ts filename for the Supabase session-refresh logic to work on Workers.Here's what happens when a user uploads a photo. It looks simple, but each step exists to solve a real problem I hit along the way: The API accepts either a data URL base64 from the browser or an HTTPS URL. The URL case was the sneaky one: accepting arbitrary URLs from users is an SSRF hole. The rule I enforce is that any URL must come from our own R2 bucket — a pre- signed upload URL the client got from us moments earlier. Anything else is rejected outright: if isHttpUrl { if hasConfiguredR2PublicUrl { return NextResponse.json { error: "R2 public URL is not configured." }, { status: 500 } ; } uploadedFileKey = extractKeyFromUrl image ; if uploadedFileKey { return NextResponse.json { error: "Invalid image source." }, { status: 400 } ; } } 2. Credits: the atomic deduction Each user has a credit balance. Before running a model, the app deducts the cost atomically in the database — this is the part I'm most proud of. Instead of read-check-then-write which races under concurrency , the deduction is a single SQL update that only succeeds if the balance is sufficient: sql UPDATE public.users SET credits = credits - p amount, updated at = NOW WHERE id = p user id AND credits = p amount RETURNING credits INTO v remaining; IF v remaining IS NULL THEN RETURN NULL; -- insufficient credits END IF; Retrying the RPC on transient errors max 3 attempts, 100ms backoff made the system resilient against flaky network calls, and NULL cleanly signals "insufficient credits" to the API. 3. Never charge for a failure The model call happens after deduction. If the model errors, the app refunds the credit immediately — because the user paid for a restored image, not for an error message: ts } catch modelError { await supabase.rpc "refund credits", { p user id: user.id, p amount: RESTORE CREDIT COST } ; throw modelError; } Moderation before the model AI image APIs will happily spend your money on anything. I put a moderation gate in front of the restore calls — the prompt and filename are checked before the model runs, so policy violations return a clean 4xx instead of a bill. What this costs to run The restore model runs on Replicate, and costs are per-call. The credit-pricing model prepaid credits, deducted atomically, refunded on failure keeps the economics honest: users never pay for a failed request, and I never eat the cost of a successful one. Wrapping up The interesting part of this project wasn't the AI — it was all the boring engineering around it: stateless serverless deployment, atomic money-like arithmetic, SSRF defense, and making sure failures never cost the user anything. If you're curious, PixRestorer is live at pixrestorer.com https://pixrestorer.com . You can upload one of your own faded photos and see the whole thing end-to-end. I'd love to hear what you think — and if you're building something similar, I'm happy to answer questions about the details I didn't cover here.