{"slug": "how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate", "title": "How I Built an AI Photo Restoration App with Next.js, Supabase, and Replicate", "summary": "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.", "body_md": "A few months ago I dug out a box of family photos from the 80s. Most were faded,\n\nscratched, or had those crease marks where they'd been folded for decades. I tried\n\nfixing them in Photoshop, but I had dozens of them and gave up after the second one.\n\nSo I did what any developer would do: I built a tool for it.\n\nThis post is a technical walkthrough of what I learned building **PixRestorer** —\n\nan AI photo restoration web app that repairs damaged, blurry, and black-and-white\n\nphotos in under 30 seconds. Hopefully the architecture decisions, the edge cases,\n\nand the failure-handling patterns are useful if you're building something similar.\n\n`@opennextjs/cloudflare`\n\nThe app itself is completely stateless: every dependency (database, auth, AI,\n\nstorage, payments) lives outside the app. That's the single most important\n\narchitecture decision, and it made deployment trivial.\n\nInitially the app ran on a VPS with PM2. Moving to Cloudflare Workers with OpenNext\n\nremoved the server entirely — there is no Node runtime to manage, no `sharp`\n\nimage\n\nprocessing on the origin (Cloudflare Image Resizing handles it), and rollbacks are\n\njust a DNS change.\n\nIt wasn't free, though. Three things bit me:\n\n`proxy.ts`\n\nonly runs on the Node runtime,\nbut Workers only support Edge middleware. I had to keep the old `middleware.ts`\n\nfilename 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\n\nexists to solve a real problem I hit along the way:\n\nThe API accepts either a data URL (base64 from the browser) or an HTTPS URL. The\n\nURL case was the sneaky one: accepting arbitrary URLs from users is an SSRF hole.\n\nThe rule I enforce is that any URL must come **from our own R2 bucket** — a pre-\n\nsigned upload URL the client got from us moments earlier. Anything else is\n\nrejected outright:\n\n```\nif (isHttpUrl) {\n  if (!hasConfiguredR2PublicUrl()) {\n    return NextResponse.json({ error: \"R2 public URL is not configured.\" }, { status: 500 });\n  }\n  uploadedFileKey = extractKeyFromUrl(image);\n  if (!uploadedFileKey) {\n    return NextResponse.json({ error: \"Invalid image source.\" }, { status: 400 });\n  }\n}\n\n2. Credits: the atomic deduction\nEach 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:\n\nsql\nUPDATE public.users\nSET credits = credits - p_amount, updated_at = NOW()\nWHERE id = p_user_id AND credits >= p_amount\nRETURNING credits INTO v_remaining;\n\nIF v_remaining IS NULL THEN\n  RETURN NULL;  -- insufficient credits\nEND IF;\nRetrying 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.\n\n3. Never charge for a failure\nThe 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:\n\nts\n} catch (modelError) {\n  await supabase.rpc(\"refund_credits\", { p_user_id: user.id, p_amount: RESTORE_CREDIT_COST });\n  throw modelError;\n}\n```\n\nModeration before the model\n\nAI 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.\n\nWhat this costs to run\n\nThe 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.\n\nWrapping up\n\nThe 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.\n\nIf you're curious, PixRestorer is live at [pixrestorer.com](https://pixrestorer.com)[. ]\n\nYou 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.", "url": "https://wpnews.pro/news/how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate", "canonical_source": "https://dev.to/skyler_7abb6ba5f692ca05e6/how-i-built-an-ai-photo-restoration-app-with-nextjs-supabase-and-replicate-55lk", "published_at": "2026-09-03 04:36:10+00:00", "updated_at": "2026-09-03 04:53:48.536611+00:00", "lang": "en", "topics": ["ai-products", "developer-tools", "ai-infrastructure"], "entities": ["PixRestorer", "Next.js", "Supabase", "Replicate", "Cloudflare Workers", "OpenNext", "R2"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate", "markdown": "https://wpnews.pro/news/how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate.md", "text": "https://wpnews.pro/news/how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate.txt", "jsonld": "https://wpnews.pro/news/how-i-built-an-ai-photo-restoration-app-with-next-js-supabase-and-replicate.jsonld"}}