cd /news/ai-products/how-i-built-an-ai-photo-restoration-… · home topics ai-products article
[ARTICLE · art-119843] src=dev.to ↗ pub= topic=ai-products verified=true sentiment=· neutral

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.

read4 min views10 publishedSep 3, 2026

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[. ]

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.

── more in #ai-products 4 stories · sorted by recency
── more on @pixrestorer 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-built-an-ai-ph…] indexed:0 read:4min 2026-09-03 ·