Build a Photo Restoration Web App
Build a shared-account web app that restores photos and syncs private history.
Introduction
30 Second Summary
A damaged family photo can matter far more than its scratches suggest. Restoring that memory becomes more useful when the result follows you from computer to phone.
In this project, you will build PixelFixer entirely on your OPPO A58. The web app restores uploaded photos, then keeps one private Supabase history available in separate regular and Incognito Google Chrome sessions on the same phone.
What You'll Build
Your live PixelFixer app restores a photo from your OPPO A58 and shows the same private history in two separate Chrome sessions.
By the end of this project, you'll have:
- A responsive restoration studio with five choices: Smart Restore, Face Upscale, Natural Enhance, Sepia, or Cartoon.
- A private user history protected by PostgreSQL Row Level Security that only shows the signed-in user's restored photos.
- A live Vercel deployment that uses Cloudinary to produce authenticated before-and-after results across both devices.
- Secret Mission: Add a chained preset that restores damage before balancing color, contrast, and brightness.
Are there any prerequisites?
You only need your OPPO A58, a stable internet connection, and enough free storage for the project files. This project assumes you have never built or run a development project on a phone before.
You install Acode and its terminal in Step 1. Every mobile tool is introduced before you use it, with no JavaScript, terminal, or database knowledge assumed.
Before We Start
This first checkpoint locks in what PixelFixer does before any setup begins. PixelFixer restores or restyles JPG and PNG photos through five presets using one shared Supabase Auth identity with private restoration_jobs rows for the web MVP and a future Expo mobile client.
Set Up the PrimeFrame Web Project
Set Up the PixelFixer Web Project
Your OPPO A58 becomes both the workspace and the test device for PixelFixer. You edit files in Acode, run commands in its terminal, and check the app in Chrome.
This project assumes you have never done development work from a phone before. This step introduces the editor, the terminal, and Node.js before you create and run the Next.js project.
In this step, get ready to:
- Install Acode and prepare its Alpine Linux terminal.
- Create the PixelFixer project files on your phone.
- Launch the PixelFixer starter page in mobile Chrome.
Install Acode and its terminal
Acode is an Android code editor with an integrated Alpine Linux terminal. The editor changes your project files while the terminal runs commands such as npm.
The first terminal launch downloads its Linux environment and can take about one minute. Keep Acode open while that setup finishes.
- Open the Acode website in Chrome.
- Use its Android download link to open the Google Play listing.
- Tap Install.
- Tap Open after the installation finishes.
- Allow Acode to access files when Android requests permission.
- Tap the terminal icon in Acode's bottom toolbar.
- Follow the one-tap setup if Acode asks to install the Alpine Linux terminal.
Next.js 16 requires Node.js 20.9.0 or newer. Acode installs the version available from its Alpine Linux package repository, so you will check the installed version before continuing.
- Wait until the terminal shows a command prompt.
- Update the Alpine package list and install Node.js plus npm by running:
apk update && apk add nodejs npm
✔️ I see Node 20.9.0 or higher
- Check the installed versions by running:
node --version
npm --version
You should see Node.js v20.9.0 or newer plus an npm version. Your phone is ready to run PixelFixer.
ⓧ I see an older version
The Alpine repository supplied an older Node.js release. Update Acode's terminal packages before checking again.
- Upgrade the installed Alpine packages by running:
apk update && apk upgrade
node --version
Continue when the result is v20.9.0 or newer.
ⓧ Command not found
The installation did not complete, so the terminal cannot find Node.js yet.
- Repeat the installation after confirming that your phone has an internet connection:
apk update && apk add nodejs npm
node --version
Node.js still unavailable?
Run the installation command again after checking your internet connection. If Android closed Acode in the background, reopen the terminal before retrying.
help me fix Node.js in Acode
Create the pinned project configuration
The package manifest pins React and every supporting library to known versions. The remaining root files configure styling, path aliases, exclusions, and future service settings.
- Return to Acode's terminal tab.
- Create the project inside the terminal's home folder and open it in Acode by running:
mkdir -p ~/pixelfixer-web
cd ~/pixelfixer-web
acode .
Acode should open an empty folder named pixelfixer-web. The terminal keeps this project in its Linux home folder, where Node.js can access it reliably.
- Open Acode's Files panel.
- Tap the new-file control.
- Enter package.json as the file name.
- Paste the package manifest below into the empty editor:
{
"name": "pixelfixer-web",
"version": "0.1.0",
"private": true,
"engines": { "node": ">=20.9.0" },
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start"
},
"dependencies": {
"@supabase/ssr": "0.12.7",
"@supabase/supabase-js": "2.118.0",
"cloudinary": "2.11.0",
"cn": "0.5.0",
"next": "16.4.2",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "4.3.3",
"tailwindcss": "4.3.3"
}
}
What Does This Manifest Control?
- The engines field records the minimum Node.js version required by Next.js.
- The development and build scripts use --webpack because Next.js uses its WebAssembly fallback on Android.
- The dependencies pin Next.js, React, Supabase, and Cloudinary to known versions.
- The development dependencies add Tailwind CSS through PostCSS.
- Save package.json.
- Install the pinned packages by running this command:
npm install
The first installation can take a few minutes while npm downloads the packages. You will know it has finished when the terminal returns to its prompt without an error.
Did the package installation fail?
Confirm that the terminal prompt ends with pixelfixer-web. Check that package.json uses the Android-safe Webpack scripts above.
help me troubleshoot npm in Acode
- Tap the new-file control in Acode's Files panel.
- Enter postcss.config.mjs as the file name.
- Paste the Tailwind PostCSS configuration below:
const config = { plugins: { "@tailwindcss/postcss": {} } }
export default config
What Does This Configuration Do?
PostCSS loads Tailwind's processor during development builds. This makes PixelFixer's utility classes available to the browser.
- Save postcss.config.mjs using Acode's save control.
- Confirm that postcss.config.mjs appears beside package.json in the Files panel.
- Tap the new-file control in Acode's Files panel.
- Enter jsconfig.json as the file name.
- Paste the source path alias below:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
Why Use a Path Alias?
The @/* alias points imports at the src folder. Later files can use short imports without counting parent folders.
- Save jsconfig.json using Acode's save control.
- Confirm that the Files panel lists both configuration files.
- Tap the new-file control in Acode's Files panel.
- Enter .gitignore as the file name.
- Paste the local file exclusions below:
node_modules
.next
.vercel
.env*
!.env.local.example
What Stays Out of Source Control?
Generated packages, framework output, deployment metadata, and local environment files stay excluded. The shareable .env.local.example template remains available.
- Save .gitignore using Acode's save control.
- Confirm that .gitignore appears in the Files panel.
- Tap the new-file control in Acode's Files panel.
- Enter .vercelignore as the file name.
- Paste the deployment upload exclusions below:
node_modules
.next
.env*
Why Exclude These Deployment Files?
Vercel installs packages during deployment. Excluding local packages, build output, and environment files keeps machine-specific data out of the upload.
- Save .vercelignore using Acode's save control.
- Confirm that both ignore files appear in the Files panel.
- Tap the new-file control in Acode's Files panel.
- Enter .env.local.example as the file name.
- Paste the future service placeholders below:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
What Belongs in This Template?
The template records the variable names that Supabase and Cloudinary require later. It contains example placeholders instead of live credentials.
A separate local file holds the real values in later steps. The ignore rules keep that local file outside source control.
- Save .env.local.example using Acode's save control.
- Confirm that the Files panel shows all six project configuration files.
Add and launch the starter page
The Next.js App Router needs a global stylesheet, a root layout, and a home page. This first screen proves that the pinned toolchain can compile PixelFixer locally.
- Tap the new-folder control in Acode's Files panel.
- Enter src as the folder name.
- Open src in the Files panel.
- Create an app folder inside src.
- Open src/app in the Files panel.
- Create globals.css inside src/app.
- Paste the PixelFixer visual tokens below:
@import "tailwindcss";
:root{--background:#07111f;--foreground:#eef6ff;--card:#0d1b2dcc;--card-foreground:#eef6ff;--muted-foreground:#9fb2c8;--border:#29415f;--primary:#6ee7f9;--primary-foreground:#06202a}
@theme inline{--color-background:var(--background);--color-foreground:var(--foreground);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-muted-foreground:var(--muted-foreground);--color-border:var(--border);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground)}
*{box-sizing:border-box}html{background:var(--background)}body{min-height:100vh;margin:0;color:var(--foreground);background:radial-gradient(circle at 15% 15%,#0c728055 0,transparent 34%),radial-gradient(circle at 85% 0%,#5b4fb655 0,transparent 30%),linear-gradient(160deg,#07111f 0%,#09172a 55%,#07111f 100%);font-family:Arial,Helvetica,sans-serif}input,select,button{font:inherit}input,select{width:100%;border:1px solid var(--border);border-radius:.65rem;background:#081425;color:var(--foreground);padding:.8rem .9rem}button,.button-link{display:inline-flex;min-height:2.75rem;align-items:center;justify-content:center;border:0;border-radius:.7rem;background:var(--primary);color:var(--primary-foreground);padding:.75rem 1rem;font-weight:700;text-decoration:none;cursor:pointer}button:hover,.button-link:hover{filter:brightness(1.08)}img{display:block;width:100%}
What Does This Stylesheet Establish?
- The Tailwind import activates the pinned styling system.
- The color variables create PixelFixer's dark canvas and cyan accent.
- The body gradients create depth behind the page content.
- The shared rules prepare later inputs, selectors, links, buttons, and images.
- Save src/app/globals.css using Acode's save control.
- Confirm that globals.css appears inside src/app in the Files panel.
- Tap the new-file control while src/app is open in Acode's Files panel.
- Enter layout.js as the file name.
- Paste the root layout and metadata below:
import "./globals.css"
export const metadata={title:"PixelFixer",description:"AI-assisted photo restoration with shared account history"}
export default function RootLayout({children}){return <html lang="en"><body>{children}</body></html>}
What Does the Root Layout Provide?
The layout imports the global visual system for every route. Its metadata gives the browser tab the PixelFixer title and description.
The children value marks the position where each page renders.
- Save src/app/layout.js using Acode's save control.
- Confirm that layout.js appears beside globals.css in the Files panel.
- Tap the new-file control while src/app is open in Acode's Files panel.
- Enter page.js as the file name.
- Paste the PixelFixer starter page below:
export default function HomePage(){return <main className="grid min-h-screen place-items-center px-5 py-12"><section className="max-w-lg rounded-xl border border-border bg-card p-7 text-center shadow-sm"><div aria-label="Logo placeholder" className="mx-auto grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mt-4 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="mt-3 text-2xl font-semibold">Repair the photo. Keep the memory.</h1><p className="mt-3 text-muted-foreground">Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</p></section></main>}
What Does the Starter Page Prove?
The HomePage component renders PixelFixer's first visible screen with a logo placeholder. Its utility classes use the theme values from globals.css.
A successful render proves that the layout, stylesheet, React component, and pinned packages work together.
- Save src/app/page.js using Acode's save control.
- Return to the terminal tab that is inside pixelfixer-web.
Before you start the server, what page do you expect these three app files to produce?
- Start the PixelFixer development server by running this command:
npm run dev
The terminal should report a local development address without a build error. Leave this terminal running because it serves PixelFixer while you work.
- Swipe up from the OPPO A58 home screen to open the app drawer.
- Tap Chrome.
- Tap the address bar.
- Enter http://localhost:3000.
You should see the dark PixelFixer starter page with a dashed Your logo placeholder and the heading Repair the photo. Keep the memory. The browser tab should show PixelFixer.
That foundation is working. PixelFixer now has a local page that can accept the restoration studio in the next step.
Keep Acode Running
Keep the current Acode terminal running while you build the app. Android may stop long-running background apps, so return to Acode and restart npm run dev if Chrome can no longer reach the page.
Do You See an Error Page?
Confirm that the terminal is inside pixelfixer-web. Check that every file is saved under the exact path shown above.
If the terminal reports a syntax or module error, help me debug the PixelFixer starter page.
✔️ Awesome, I've got everything!
Your pinned PixelFixer workspace is running. Double-check that every file is saved before moving on.
ⓧ I'd like to double check the full code
Compare each file with the complete project state for this step.
{
"name": "pixelfixer-web",
"version": "0.1.0",
"private": true,
"engines": { "node": ">=20.9.0" },
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start"
},
"dependencies": {
"@supabase/ssr": "0.12.7",
"@supabase/supabase-js": "2.118.0",
"cloudinary": "2.11.0",
"cn": "0.5.0",
"next": "16.4.2",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "4.3.3",
"tailwindcss": "4.3.3"
}
}
const config = { plugins: { "@tailwindcss/postcss": {} } }
export default config
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
node_modules
.next
.vercel
.env*
!.env.local.example
node_modules
.next
.env*
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
@import "tailwindcss";
:root{--background:#07111f;--foreground:#eef6ff;--card:#0d1b2dcc;--card-foreground:#eef6ff;--muted-foreground:#9fb2c8;--border:#29415f;--primary:#6ee7f9;--primary-foreground:#06202a}
@theme inline{--color-background:var(--background);--color-foreground:var(--foreground);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-muted-foreground:var(--muted-foreground);--color-border:var(--border);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground)}
*{box-sizing:border-box}html{background:var(--background)}body{min-height:100vh;margin:0;color:var(--foreground);background:radial-gradient(circle at 15% 15%,#0c728055 0,transparent 34%),radial-gradient(circle at 85% 0%,#5b4fb655 0,transparent 30%),linear-gradient(160deg,#07111f 0%,#09172a 55%,#07111f 100%);font-family:Arial,Helvetica,sans-serif}input,select,button{font:inherit}input,select{width:100%;border:1px solid var(--border);border-radius:.65rem;background:#081425;color:var(--foreground);padding:.8rem .9rem}button,.button-link{display:inline-flex;min-height:2.75rem;align-items:center;justify-content:center;border:0;border-radius:.7rem;background:var(--primary);color:var(--primary-foreground);padding:.75rem 1rem;font-weight:700;text-decoration:none;cursor:pointer}button:hover,.button-link:hover{filter:brightness(1.08)}img{display:block;width:100%}
import "./globals.css"
export const metadata={title:"PixelFixer",description:"AI-assisted photo restoration with shared account history"}
export default function RootLayout({children}){return <html lang="en"><body>{children}</body></html>}
export default function HomePage(){return <main className="grid min-h-screen place-items-center px-5 py-12"><section className="max-w-lg rounded-xl border border-border bg-card p-7 text-center shadow-sm"><div aria-label="Logo placeholder" className="mx-auto grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mt-4 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="mt-3 text-2xl font-semibold">Repair the photo. Keep the memory.</h1><p className="mt-3 text-muted-foreground">Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</p></section></main>}
Your local PixelFixer foundation is running with its pinned packages, visual tokens, and logo placeholder. Next, you will turn this starter screen into the visible restoration studio.
Build the Restoration Studio
Your Next.js starter proves that the local development loop works. It does not yet show what PixelFixer offers someone with a damaged photo.
You will turn that starter into a responsive studio with Tailwind CSS and the official-style shadcn/ui Card source. The interface will also surface the missing account boundary before backend work begins.
In this step, get ready to:
- Add reusable Card components for the studio layout.
- Build five preset choices with a sample comparison.
- Verify the responsive layout and the sign-in boundary.
Add the reusable Card components
A Card component gives each part of the studio the same structure. Its smaller pieces keep titles, descriptions, controls, and footers consistent.
- Open Acode's Files panel.
- Create a components folder inside src.
- Create a ui folder inside src/components.
You should now see src/components/ui in Acode's Files panel.
- Open src/components/ui in Acode's Files panel.
- Tap the new-file control.
- Enter card.jsx as the file name.
- Paste the Card primitives below:
import * as React from "react"
import {cn} from "cn"
function Card({className,...props}){return <div data-slot="card" className={cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",className)} {...props}/>}
function CardHeader({className,...props}){return <div data-slot="card-header" className={cn("grid gap-2 px-6",className)} {...props}/>}
function CardTitle({className,...props}){return <div data-slot="card-title" className={cn("font-semibold leading-none",className)} {...props}/>}
function CardDescription({className,...props}){return <div data-slot="card-description" className={cn("text-sm text-muted-foreground",className)} {...props}/>}
function CardContent({className,...props}){return <div data-slot="card-content" className={cn("px-6",className)} {...props}/>}
function CardFooter({className,...props}){return <div data-slot="card-footer" className={cn("flex items-center px-6",className)} {...props}/>}
export{Card,CardHeader,CardFooter,CardTitle,CardDescription,CardContent}
Why Use Card Primitives?
- Each function returns a focused React wrapper for one part of a card.
- The cn helper combines the default styles with any extra classes supplied by the page.
- The data-slot values provide stable names for the card sections.
- The exports let the page compose each studio panel from matching pieces.
- Save src/components/ui/card.jsx using Acode's save control.
Acode's Files panel should list card.jsx under src/components/ui.
Card File in the Wrong Place?
Check that the complete path is src/components/ui/card.jsx. The page import depends on that exact location.
Make sure the file extension is .jsx.
help me fix my PixelFixer Card component path
Build the static restoration studio
The studio needs to communicate the whole restoration flow before any service processes a file. A visible upload control, preset menu, comparison area, and product boundary make that flow concrete.
- Open src/app/page.js from Acode's Files panel.
- Select all existing code in the editor.
- Replace the selected code with the studio code below:
import{Card,CardContent,CardDescription,CardFooter,CardHeader,CardTitle}from "@/components/ui/card"
export default function HomePage(){return (
<main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14">
{/* Keep this static studio focused on the visible restoration flow. */}
<header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div className="flex items-start gap-4"><div aria-label="Logo placeholder" className="grid h-14 w-14 shrink-0 place-items-center rounded-xl border border-dashed border-primary px-2 text-center text-xs text-primary">Your logo</div><div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Choose a photo and preview how a PixelFixer restoration session is organized.</p></div></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">Static studio preview</p></header>
<section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]">
<Card className="border-border/80 bg-card/90 backdrop-blur">
<CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Choose a JPG or PNG demonstration photo and an output preset.</CardDescription></CardHeader>
<CardContent>
<form className="grid gap-5">
<label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png"/></label>
<label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore"><option value="restore">Smart Restore</option><option value="upscale">Face Upscale</option><option value="improve">Natural Enhance</option><option value="sepia">Sepia</option><option value="cartoon">Cartoon</option></select></label>
<button type="button" disabled className="disabled:cursor-not-allowed disabled:opacity-50">Process Photo</button>
<p className="text-sm text-muted-foreground">Sign in required before PixelFixer can process a photo.</p>
</form></CardContent>
<CardFooter><p className="text-sm text-muted-foreground">Use non-sensitive demonstration photos while testing.</p></CardFooter>
</Card>
<Card className="border-border/80 bg-card/90 backdrop-blur">
<CardHeader><CardTitle>Sample before and after</CardTitle><CardDescription>The processed result will appear beside its source photo.</CardDescription></CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<figure className="overflow-hidden rounded-xl border border-border bg-background"><div className="grid aspect-square place-items-center bg-gradient-to-br from-slate-700 to-slate-950 text-muted-foreground">Sample source</div><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure>
<figure className="overflow-hidden rounded-xl border border-border bg-background"><div className="grid aspect-square place-items-center bg-gradient-to-br from-cyan-950 to-slate-800 text-muted-foreground">Sample result</div><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure>
</div>
</CardContent>
<CardFooter><p className="text-sm text-muted-foreground">Preview only. No photo leaves your browser in this step.</p></CardFooter>
</Card>
</section></main>
)}
What Does This Page Do?
- The first Card presents the file control, the preset menu, and the processing action.
- The preset menu exposes Smart Restore, Face Upscale, Natural Enhance, Sepia, and Cartoon.
- The second Card reserves a responsive comparison area for the source and result.
- The disabled action exposes the missing account requirement without claiming that processing works.
- Save src/app/page.js using Acode's save control.
- Return to Chrome at http://localhost:3000.
- Refresh the page.
You should see a dashed Your logo placeholder beside the PixelFixer heading. Two dark responsive cards appear below it, with photo controls on the left and the sample comparison on the right.
That is the product shell in place: PixelFixer now reads like a restoration tool instead of a starter page.
Still Seeing the Starter Page?
Confirm that you replaced src/app/page.js inside the open pixelfixer-web folder.
Check the running terminal for a syntax or import error. Make sure the Card file is saved at src/components/ui/card.jsx.
help me debug the PixelFixer studio page
Test the sign-in boundary
A polished interface can make an unfinished workflow look complete. This check confirms that PixelFixer communicates its current limit clearly.
- Hold the OPPO A58 in portrait orientation.
You should see the controls remain readable above the sample comparison. The two sample panels also stack when the available width becomes smaller.
- Rotate the OPPO A58 to landscape orientation.
- Select Natural Enhance from the Output preset menu.
Before you test the action, do you think this static studio can process the selected photo?
- Try to click Process Photo.
The button stays disabled. The message Sign in required before PixelFixer can process a photo. identifies the missing account boundary.
Why Block Processing Now?
This static page has no authenticated user. It also has no connected restoration service.
Blocking the action keeps the interface honest. The next account step can replace this visible boundary with a private signed-in workflow.
✔️ Awesome, I've got everything!
Great work. Save both files before moving on to the shared account layer.
ⓧ I'd like to double check the full code
Compare your reusable Card source with this complete src/components/ui/card.jsx file.
import * as React from "react"
import {cn} from "cn"
function Card({className,...props}){return <div data-slot="card" className={cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",className)} {...props}/>}
function CardHeader({className,...props}){return <div data-slot="card-header" className={cn("grid gap-2 px-6",className)} {...props}/>}
function CardTitle({className,...props}){return <div data-slot="card-title" className={cn("font-semibold leading-none",className)} {...props}/>}
function CardDescription({className,...props}){return <div data-slot="card-description" className={cn("text-sm text-muted-foreground",className)} {...props}/>}
function CardContent({className,...props}){return <div data-slot="card-content" className={cn("px-6",className)} {...props}/>}
function CardFooter({className,...props}){return <div data-slot="card-footer" className={cn("flex items-center px-6",className)} {...props}/>}
export{Card,CardHeader,CardFooter,CardTitle,CardDescription,CardContent}
Your complete src/app/page.js file should match this static studio.
import{Card,CardContent,CardDescription,CardFooter,CardHeader,CardTitle}from "@/components/ui/card"
export default function HomePage(){return (
<main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14">
{/* Keep this static studio focused on the visible restoration flow. */}
<header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div className="flex items-start gap-4"><div aria-label="Logo placeholder" className="grid h-14 w-14 shrink-0 place-items-center rounded-xl border border-dashed border-primary px-2 text-center text-xs text-primary">Your logo</div><div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Choose a photo and preview how a PixelFixer restoration session is organized.</p></div></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">Static studio preview</p></header>
<section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]">
<Card className="border-border/80 bg-card/90 backdrop-blur">
<CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Choose a JPG or PNG demonstration photo and an output preset.</CardDescription></CardHeader>
<CardContent>
<form className="grid gap-5">
<label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png"/></label>
<label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore"><option value="restore">Smart Restore</option><option value="upscale">Face Upscale</option><option value="improve">Natural Enhance</option><option value="sepia">Sepia</option><option value="cartoon">Cartoon</option></select></label>
<button type="button" disabled className="disabled:cursor-not-allowed disabled:opacity-50">Process Photo</button>
<p className="text-sm text-muted-foreground">Sign in required before PixelFixer can process a photo.</p>
</form></CardContent>
<CardFooter><p className="text-sm text-muted-foreground">Use non-sensitive demonstration photos while testing.</p></CardFooter>
</Card>
<Card className="border-border/80 bg-card/90 backdrop-blur">
<CardHeader><CardTitle>Sample before and after</CardTitle><CardDescription>The processed result will appear beside its source photo.</CardDescription></CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<figure className="overflow-hidden rounded-xl border border-border bg-background"><div className="grid aspect-square place-items-center bg-gradient-to-br from-slate-700 to-slate-950 text-muted-foreground">Sample source</div><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure>
<figure className="overflow-hidden rounded-xl border border-border bg-background"><div className="grid aspect-square place-items-center bg-gradient-to-br from-cyan-950 to-slate-800 text-muted-foreground">Sample result</div><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure>
</div>
</CardContent>
<CardFooter><p className="text-sm text-muted-foreground">Preview only. No photo leaves your browser in this step.</p></CardFooter>
</Card>
</section></main>
)}
PixelFixer now has a responsive restoration studio with an honest product boundary. Next, you will add the shared account and private history that make processing user-specific.
Add Shared Accounts and Private History
The polished studio now makes the missing ownership layer obvious. A photo result needs a user before PixelFixer can keep it private.
In this step, Supabase Auth gives each user one identity. A PostgreSQL table stores that user's restoration history. Row Level Security limits every signed-in user to their own rows.
In this step, get ready to:
- Connect PixelFixer to a dedicated Supabase project.
- Create a private restoration history protected by Row Level Security.
- Replace the sign-in blocker with a working account experience.
Connect PixelFixer to Supabase
The app needs a dedicated Supabase project so its users stay separate from your other work. The project URL tells PixelFixer where to connect.
- Return to the Supabase account from earlier.
- Start the flow for creating a new project.
- Select the organization that should own PixelFixer.
- Enter PixelFixer as the project name.
The database password controls direct access to the project. Use a unique password that you can store safely.
- Create a strong database password.
- Choose the available region closest to you.
- Submit the project creation form.
Provisioning can sit quietly for a while. That pause means Supabase is creating the database and authentication services.
Why a dedicated project?
Each Supabase project has its own database and authentication configuration. Keeping PixelFixer separate makes its users easier to manage.
- Open the Connect panel after the project becomes ready.
- Keep the project URL and publishable key visible in Chrome.
- Use Android's Recent Apps view to switch to Acode.
- Duplicate .env.local.example inside the pixelfixer-web folder.
- Rename the duplicate to .env.local.
- Open .env.local in Acode.
- Compare the file with this template:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
What does this configuration do?
- The NEXT_PUBLIC_SUPABASE_URL value points the app to your Supabase project.
- The NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY value identifies the browser-facing client. Row Level Security still decides which database rows that client can access.
- The CLOUDINARY_URL placeholder remains unchanged until the next step.
- Use Android's Recent Apps view to return to the Supabase Connect panel.
- Copy the project URL.
- Use Recent Apps to return to Acode.
- Replace https://your-project.supabase.co with the project URL.
- Use Recent Apps to return to Supabase.
- Copy the publishable key.
- Use Recent Apps to return to Acode.
- Replace your-publishable-key with the publishable key.
- Save .env.local using Acode's save control.
- Return to the Acode terminal running PixelFixer.
- Press Ctrl+C to stop the current server.
- Restart PixelFixer with the new Supabase settings by running:
npm run dev
You should see the local PixelFixer URL in the terminal again. The restarted process now reads your Supabase configuration.
Environment values not loading?
Confirm that the file is named .env.local. Check that it sits beside package.json in the pixelfixer-web folder.
Make sure the copied values do not contain extra spaces. Restart the development server after correcting the file.
help me diagnose why my Next.js app cannot read its Supabase environment values
Protect restoration history with RLS
A restoration record contains links to a user's original photo and processed result. The database must enforce ownership even if a request bypasses the page interface.
What is Row Level Security?
Row Level Security applies access rules inside PostgreSQL. Each policy compares the signed-in user's identity with the row's user_id value.
PixelFixer defines separate policies for reading, creating, and deleting restoration records. Database ownership stays enforced beyond the browser.
- Create database.sql inside the pixelfixer-web folder with Acode's new-file control.
- Paste the private history schema below into database.sql:
create table public.restoration_jobs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade default auth.uid(),
preset text not null,
source_public_id text not null,
original_url text not null,
result_url text not null,
created_at timestamptz not null default now()
);
alter table public.restoration_jobs enable row level security;
revoke all on table public.restoration_jobs from anon, authenticated;
grant select, insert, delete on table public.restoration_jobs to authenticated;
create policy "Users can view their own restoration jobs." on public.restoration_jobs for select to authenticated using ((select auth.uid()) = user_id);
create policy "Users can create their own restoration jobs." on public.restoration_jobs for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "Users can delete their own restoration jobs." on public.restoration_jobs for delete to authenticated using ((select auth.uid()) = user_id);
What does this schema do?
- The restoration_jobs table records the preset plus both image URLs.
- The user_id column links every job to a Supabase Auth user.
- The revoke statement removes anonymous access plus broad signed-in access.
- The three policies permit signed-in users to work only with rows whose user_id matches auth.uid().
- Save database.sql.
- Return to your PixelFixer project in the Supabase dashboard.
- Open SQL Editor.
- Start a blank SQL query.
- Paste the contents of database.sql into the query.
- Click Run.
You should see a successful query result with no database error. The table plus its access policies now exist.
- Open Table Editor from the Supabase dashboard.
- Select restoration_jobs to confirm its columns.
You should see id, user_id, preset, source_public_id, original_url, result_url, and created_at.
SQL query failed?
Check that you pasted the entire contents of database.sql into one query. A missing parenthesis can stop the table from being created.
If the table already exists after a partial run, remove that test table before running the complete file again.
help me diagnose my PixelFixer database schema error
Turn the studio into a signed-in experience
A cookie-backed session lets the server identify the current user during each request. The Next.js App Router reads that session before loading private history.
- Create src/lib/supabase with Acode's new-folder control.
- Create server.js inside src/lib/supabase.
- Paste the server-side Supabase client below:
import {createServerClient} from "@supabase/ssr"
import {cookies} from "next/headers"
export async function createClient(){const cookieStore=await cookies();return createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL,process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,{cookies:{getAll(){return cookieStore.getAll()},setAll(cookiesToSet){try{cookiesToSet.forEach(({name,value,options})=>cookieStore.set(name,value,options))}catch{}}}})}
What does the server client do?
The createClient helper reads the current request's cookies. Supabase uses those cookies to recover the signed-in session.
The helper also writes refreshed cookie values when authentication state changes. Server-rendered pages receive the latest identity.
- Save src/lib/supabase/server.js using Acode's save control.
- Confirm that server.js appears inside src/lib/supabase in the Files panel.
Server client showing an import error?
Check that the path is exactly src/lib/supabase/server.js. Confirm that @supabase/ssr remains listed in package.json.
help me fix the Supabase server client import
- Create src/app/login with Acode's new-folder control.
- Create actions.js inside src/app/login.
- Paste the account actions below:
"use server"
import {revalidatePath} from "next/cache"
import {redirect} from "next/navigation"
import {createClient} from "@/lib/supabase/server"
export async function login(formData){const supabase=await createClient();const credentials={email:formData.get("email"),password:formData.get("password")};const{error}=await supabase.auth.signInWithPassword(credentials);if(error)throw new Error("PixelFixer could not sign in with those credentials.");revalidatePath("/","layout");redirect("/")}
export async function signup(formData){const supabase=await createClient();const credentials={email:formData.get("email"),password:formData.get("password")};const{data,error}=await supabase.auth.signUp(credentials);if(error)throw new Error("PixelFixer could not create that account.");revalidatePath("/","layout");redirect(data.session?"/":"/login")}
How do the account actions work?
- The login action submits the email plus password to Supabase Auth.
- The signup action creates the shared PixelFixer identity.
- Each successful action refreshes the app layout before redirecting to the studio.
- Save src/app/login/actions.js using Acode's save control.
- Confirm that actions.js appears inside src/app/login in the Files panel.
Account actions showing an alias error?
Confirm that jsconfig.json still maps @/* to ./src/*. Restart the development server after correcting that file.
help me fix the path alias used by my login actions
- Create page.js inside src/app/login.
- Build the login page by pasting this code:
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import{login,signup}from "./actions"
export default function LoginPage(){return <main className="mx-auto flex min-h-screen max-w-5xl items-center px-5 py-12"><div className="grid w-full gap-8 md:grid-cols-[1.1fr_0.9fr] md:items-center"><section><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-3 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer private beta</p><h1 className="max-w-xl text-4xl font-semibold tracking-tight md:text-5xl">Restore a photo, then find it in every signed-in session.</h1><p className="mt-5 max-w-xl text-base leading-7 text-muted-foreground">Your regular and Incognito Chrome sessions keep separate cookies. Supabase connects both sessions to one user identity and one private history.</p></section><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Enter PixelFixer</CardTitle><CardDescription>Use at least eight characters. If email confirmation is enabled, confirm the message and then sign in.</CardDescription></CardHeader><CardContent><form className="grid gap-4"><label className="grid gap-2 text-sm font-medium">Email<input name="email" type="email" required autoComplete="email"/></label><label className="grid gap-2 text-sm font-medium">Password<input name="password" type="password" minLength="8" required autoComplete="current-password"/></label><div className="grid gap-3 sm:grid-cols-2"><button formAction={login}>Sign in</button><button formAction={signup}>Create account</button></div></form></CardContent></Card></div></main>}
What does the login page do?
The form sends the same credentials to either login or signup. Supabase returns a session after a successful authentication flow.
The responsive Card keeps the account screen consistent with the restoration studio. The same page fits regular and Incognito Chrome sessions on your OPPO A58.
- Save src/app/login/page.js.
- Enter http://localhost:3000/login in the Chrome address bar.
You should see the PixelFixer private beta introduction and logo placeholder beside an account Card. The Card contains email and password fields.
Login page not rendering?
Check that both login files are inside src/app/login. Confirm that the page imports its actions from ./actions.
help me debug the PixelFixer login route
- Create proxy.js inside src/lib/supabase.
- Add session refresh behavior by pasting this code:
import {createServerClient} from "@supabase/ssr"
import {NextResponse} from "next/server"
export async function updateSession(request){let supabaseResponse=NextResponse.next({request});const supabase=createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL,process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,{cookies:{getAll(){return request.cookies.getAll()},setAll(cookiesToSet,headers){cookiesToSet.forEach(({name,value})=>request.cookies.set(name,value));supabaseResponse=NextResponse.next({request});cookiesToSet.forEach(({name,value,options})=>supabaseResponse.cookies.set(name,value,options));Object.entries(headers).forEach(([key,value])=>supabaseResponse.headers.set(key,value))}}});const{data}=await supabase.auth.getClaims();if(!data?.claims&&!request.nextUrl.pathname.startsWith("/login")&&!request.nextUrl.pathname.startsWith("/auth")){const url=request.nextUrl.clone();url.pathname="/login";return NextResponse.redirect(url)}return supabaseResponse}
How does session refresh work?
The updateSession helper asks Supabase to validate the current authentication claims. It carries refreshed cookies into the response.
Requests without valid claims redirect to /login. Login plus authentication routes remain accessible without a session.
- Save src/lib/supabase/proxy.js.
- Confirm that both Supabase helper files appear inside src/lib/supabase.
Session helper reporting missing claims?
Confirm that .env.local contains the URL plus publishable key from the same Supabase project. Values from different projects cannot validate the same session.
help me debug Supabase session claims in PixelFixer
- Create src/proxy.js with Acode's new-file control.
- Paste the session connection code below:
import {NextResponse} from "next/server"
import {updateSession} from "./lib/supabase/proxy"
export async function proxy(request){if(request.nextUrl.pathname.startsWith("/_next")||request.nextUrl.pathname==="/favicon.ico")return NextResponse.next({request});return updateSession(request)}
What does the app proxy do?
The exported proxy function applies session handling to app requests. Framework assets plus the favicon bypass authentication work.
Every other request passes through updateSession. Private routes receive a current authentication state.
- Save src/proxy.js.
- Enter http://localhost:3000 in Chrome.
You should be redirected to /login. This proves the static studio now requires a valid session.
Root page still opens without login?
Check that the top-level file is exactly src/proxy.js. Restart the development server after saving it.
help me debug the PixelFixer authentication redirect
- Create src/lib/presets.js with Acode's new-file control.
- Paste the five preset choices below:
export const PRESETS={restore:{label:"Smart Restore",description:"Reduce compression damage, noise, and blur.",transformation:{effect:"gen_restore"}},upscale:{label:"Face Upscale",description:"Increase resolution with extra attention to faces.",transformation:{effect:"upscale"}},improve:{label:"Natural Enhance",description:"Balance color, contrast, brightness, and lighting.",transformation:{effect:"improve"}},sepia:{label:"Sepia",description:"Apply a warm historical tone.",transformation:{effect:"sepia"}},cartoon:{label:"Cartoon",description:"Create a clear illustrated finish.",transformation:{effect:"cartoonify"}}}
Why use a preset registry?
The PRESETS object keeps each displayed label beside its future transformation definition. The studio can build its menu from one source.
The next step sends the selected transformation to the image service. This step establishes the stable preset keys that history rows store.
- Save src/lib/presets.js.
- Confirm that presets.js appears inside src/lib.
Preset menu showing an import error?
Confirm that the file exports PRESETS with uppercase letters. Check that its path is exactly src/lib/presets.js.
help me fix the PixelFixer preset registry import
- Open src/app/page.js in Acode.
- Replace its static contents with the authenticated studio below:
import {redirect} from "next/navigation"
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
export default async function HomePage(){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();if(claimsError||!claimsData?.claims)redirect("/login");const{data:jobs,error:jobsError}=await supabase.from("restoration_jobs").select("id,preset,original_url,result_url,created_at");if(jobsError)throw new Error("PixelFixer could not load your restoration history.");const sortedJobs=[...(jobs??[])].sort((a,b)=>new Date(b.created_at)-new Date(a.created_at));const latestJob=sortedJobs[0];return <main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14"><header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Signed in as {claimsData.claims.email}. Results are AI-enhanced and may contain plausible details that were not present in the source.</p></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">{sortedJobs.length} saved result{sortedJobs.length===1?"":"s"}</p></header><section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]"><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</CardDescription></CardHeader><CardContent><form className="grid gap-5"><label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png" required/></label><label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore">{Object.entries(PRESETS).map(([key,preset])=><option key={key} value={key}>{preset.label}: {preset.description}</option>)}</select></label></form></CardContent></Card><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>{latestJob?"Latest before and after":"Your result will appear here"}</CardTitle><CardDescription>{latestJob?(PRESETS[latestJob.preset]?.label??latestJob.preset):"Process a photo to create your first private history entry."}</CardDescription></CardHeader><CardContent>{latestJob?<div className="grid gap-4 sm:grid-cols-2"><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.original_url} alt="Original uploaded photograph" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.result_url} alt="AI-enhanced PixelFixer result" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure></div>:<div className="grid min-h-72 place-items-center rounded-xl border border-dashed border-border bg-background/40 p-8 text-center text-muted-foreground">No restoration jobs yet.</div>}</CardContent></Card></section><section className="mt-8"><div className="mb-4"><h2 className="text-xl font-semibold">Private history</h2><p className="mt-1 text-sm text-muted-foreground">The same signed-in account can read this history from a separate browser session.</p></div>{sortedJobs.length?<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{sortedJobs.slice(0,6).map(job=><Card key={job.id} className="gap-4 overflow-hidden border-border/80 bg-card/90 py-0"><img src={job.result_url} alt={`${PRESETS[job.preset]?.label??job.preset} result`} className="aspect-[4/3] object-cover"/><CardContent className="pb-5"><p className="font-semibold">{PRESETS[job.preset]?.label??job.preset}</p><p className="mt-1 text-sm text-muted-foreground">{new Date(job.created_at).toLocaleString()}</p><a className="button-link mt-4 w-full" href={job.result_url} target="_blank" rel="noreferrer">Open full-size result</a></CardContent></Card>)}</div>:null}</section></main>}
How does the authenticated studio work?
- The page calls getClaims() before loading private content.
- The restoration_jobs query returns rows allowed by the user's RLS policy.
- The sortedJobs array keeps the newest restoration first.
- The empty-state Card proves the account has no saved results yet.
- Save src/app/page.js.
- Refresh http://localhost:3000 in Chrome.
You should remain on the PixelFixer login page because the browser does not have a valid account session yet.
Authenticated page showing an error?
Check the terminal for the first file path named in the error. Confirm that database.sql ran successfully before the page queries restoration_jobs.
help me debug the authenticated PixelFixer home page
✔️ Awesome, I've got everything!
Your Supabase project, private history schema, session helpers, login route, and authenticated studio are now in place. Double-check that every changed file is saved.
ⓧ I'd like to double check the full code
Compare each file below with your project. Keep your real Supabase values only in .env.local.
create table public.restoration_jobs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade default auth.uid(),
preset text not null,
source_public_id text not null,
original_url text not null,
result_url text not null,
created_at timestamptz not null default now()
);
alter table public.restoration_jobs enable row level security;
revoke all on table public.restoration_jobs from anon, authenticated;
grant select, insert, delete on table public.restoration_jobs to authenticated;
create policy "Users can view their own restoration jobs." on public.restoration_jobs for select to authenticated using ((select auth.uid()) = user_id);
create policy "Users can create their own restoration jobs." on public.restoration_jobs for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "Users can delete their own restoration jobs." on public.restoration_jobs for delete to authenticated using ((select auth.uid()) = user_id);
import {createServerClient} from "@supabase/ssr"
import {cookies} from "next/headers"
export async function createClient(){const cookieStore=await cookies();return createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL,process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,{cookies:{getAll(){return cookieStore.getAll()},setAll(cookiesToSet){try{cookiesToSet.forEach(({name,value,options})=>cookieStore.set(name,value,options))}catch{}}}})}
import {createServerClient} from "@supabase/ssr"
import {NextResponse} from "next/server"
export async function updateSession(request){let supabaseResponse=NextResponse.next({request});const supabase=createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL,process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,{cookies:{getAll(){return request.cookies.getAll()},setAll(cookiesToSet,headers){cookiesToSet.forEach(({name,value})=>request.cookies.set(name,value));supabaseResponse=NextResponse.next({request});cookiesToSet.forEach(({name,value,options})=>supabaseResponse.cookies.set(name,value,options));Object.entries(headers).forEach(([key,value])=>supabaseResponse.headers.set(key,value))}}});const{data}=await supabase.auth.getClaims();if(!data?.claims&&!request.nextUrl.pathname.startsWith("/login")&&!request.nextUrl.pathname.startsWith("/auth")){const url=request.nextUrl.clone();url.pathname="/login";return NextResponse.redirect(url)}return supabaseResponse}
import {NextResponse} from "next/server"
import {updateSession} from "./lib/supabase/proxy"
export async function proxy(request){if(request.nextUrl.pathname.startsWith("/_next")||request.nextUrl.pathname==="/favicon.ico")return NextResponse.next({request});return updateSession(request)}
"use server"
import {revalidatePath} from "next/cache"
import {redirect} from "next/navigation"
import {createClient} from "@/lib/supabase/server"
export async function login(formData){const supabase=await createClient();const credentials={email:formData.get("email"),password:formData.get("password")};const{error}=await supabase.auth.signInWithPassword(credentials);if(error)throw new Error("PixelFixer could not sign in with those credentials.");revalidatePath("/","layout");redirect("/")}
export async function signup(formData){const supabase=await createClient();const credentials={email:formData.get("email"),password:formData.get("password")};const{data,error}=await supabase.auth.signUp(credentials);if(error)throw new Error("PixelFixer could not create that account.");revalidatePath("/","layout");redirect(data.session?"/":"/login")}
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import{login,signup}from "./actions"
export default function LoginPage(){return <main className="mx-auto flex min-h-screen max-w-5xl items-center px-5 py-12"><div className="grid w-full gap-8 md:grid-cols-[1.1fr_0.9fr] md:items-center"><section><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-3 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer private beta</p><h1 className="max-w-xl text-4xl font-semibold tracking-tight md:text-5xl">Restore a photo, then find it in every signed-in session.</h1><p className="mt-5 max-w-xl text-base leading-7 text-muted-foreground">Your regular and Incognito Chrome sessions keep separate cookies. Supabase connects both sessions to one user identity and one private history.</p></section><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Enter PixelFixer</CardTitle><CardDescription>Use at least eight characters. If email confirmation is enabled, confirm the message and then sign in.</CardDescription></CardHeader><CardContent><form className="grid gap-4"><label className="grid gap-2 text-sm font-medium">Email<input name="email" type="email" required autoComplete="email"/></label><label className="grid gap-2 text-sm font-medium">Password<input name="password" type="password" minLength="8" required autoComplete="current-password"/></label><div className="grid gap-3 sm:grid-cols-2"><button formAction={login}>Sign in</button><button formAction={signup}>Create account</button></div></form></CardContent></Card></div></main>}
export const PRESETS={restore:{label:"Smart Restore",description:"Reduce compression damage, noise, and blur.",transformation:{effect:"gen_restore"}},upscale:{label:"Face Upscale",description:"Increase resolution with extra attention to faces.",transformation:{effect:"upscale"}},improve:{label:"Natural Enhance",description:"Balance color, contrast, brightness, and lighting.",transformation:{effect:"improve"}},sepia:{label:"Sepia",description:"Apply a warm historical tone.",transformation:{effect:"sepia"}},cartoon:{label:"Cartoon",description:"Create a clear illustrated finish.",transformation:{effect:"cartoonify"}}}
import {redirect} from "next/navigation"
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
export default async function HomePage(){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();if(claimsError||!claimsData?.claims)redirect("/login");const{data:jobs,error:jobsError}=await supabase.from("restoration_jobs").select("id,preset,original_url,result_url,created_at");if(jobsError)throw new Error("PixelFixer could not load your restoration history.");const sortedJobs=[...(jobs??[])].sort((a,b)=>new Date(b.created_at)-new Date(a.created_at));const latestJob=sortedJobs[0];return <main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14"><header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Signed in as {claimsData.claims.email}. Results are AI-enhanced and may contain plausible details that were not present in the source.</p></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">{sortedJobs.length} saved result{sortedJobs.length===1?"":"s"}</p></header><section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]"><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</CardDescription></CardHeader><CardContent><form className="grid gap-5"><label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png" required/></label><label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore">{Object.entries(PRESETS).map(([key,preset])=><option key={key} value={key}>{preset.label}: {preset.description}</option>)}</select></label></form></CardContent></Card><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>{latestJob?"Latest before and after":"Your result will appear here"}</CardTitle><CardDescription>{latestJob?(PRESETS[latestJob.preset]?.label??latestJob.preset):"Process a photo to create your first private history entry."}</CardDescription></CardHeader><CardContent>{latestJob?<div className="grid gap-4 sm:grid-cols-2"><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.original_url} alt="Original uploaded photograph" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.result_url} alt="AI-enhanced PixelFixer result" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure></div>:<div className="grid min-h-72 place-items-center rounded-xl border border-dashed border-border bg-background/40 p-8 text-center text-muted-foreground">No restoration jobs yet.</div>}</CardContent></Card></section><section className="mt-8"><div className="mb-4"><h2 className="text-xl font-semibold">Private history</h2><p className="mt-1 text-sm text-muted-foreground">The same signed-in account can read this history from a separate browser session.</p></div>{sortedJobs.length?<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{sortedJobs.slice(0,6).map(job=><Card key={job.id} className="gap-4 overflow-hidden border-border/80 bg-card/90 py-0"><img src={job.result_url} alt={`${PRESETS[job.preset]?.label??job.preset} result`} className="aspect-[4/3] object-cover"/><CardContent className="pb-5"><p className="font-semibold">{PRESETS[job.preset]?.label??job.preset}</p><p className="mt-1 text-sm text-muted-foreground">{new Date(job.created_at).toLocaleString()}</p><a className="button-link mt-4 w-full" href={job.result_url} target="_blank" rel="noreferrer">Open full-size result</a></CardContent></Card>)}</div>:null}</section></main>}
Using an account password can feel sensitive. Choose a unique test password that you do not use for another service.
Before you submit the account form, what should replace the old sign-in blocker once your session becomes valid?
- Enter your PixelFixer email in the Email field.
- Enter a unique password containing at least eight characters in the Password field.
- Tap Create account.
If email confirmation is disabled, PixelFixer redirects straight to the studio. If confirmation is enabled, Supabase sends a confirmation message before the first sign-in.
- Confirm the account from the email message if Supabase requests confirmation.
- Return to http://localhost:3000/login after confirming the account.
- Enter your PixelFixer email in the Email field.
- Enter the same password in the Password field.
- Tap Sign in.
You should see the restoration studio with your email in the header. The page should show 0 saved results plus the No restoration jobs yet. empty state.
That ownership gap is closed. PixelFixer can now identify a user and load only that user's private history.
Account does not reach the studio?
Check whether Supabase requires email confirmation. Confirm the message before using Sign in.
If the form reports a credential problem, use the same email plus password that created the account. Check the development terminal for server action errors.
help me debug PixelFixer account creation or sign-in
Your shared account foundation is working. Next up, you will connect the studio to real photo processing and save each successful result to this private history.
Connect Real Photo Restoration
Your Supabase account now gives every restoration a clear owner. The studio still cannot turn an uploaded photo into an image result.
In this step, you will connect Cloudinary to the five existing presets. A Server Action will validate each request before saving the successful result to private history.
In this step, get ready to:
- Configure authenticated Cloudinary processing on the server.
- Add the photo restoration Server Action with rollback protection.
- Connect the studio to real processing and verify a saved result.
Configure Cloudinary safely
The CLOUDINARY_URL connection string gives the Cloudinary SDK access to your product environment. Its API secret must stay inside .env.local so browser code never receives it.
Credentials deserve extra care. Your existing ignore rules already protect .env.local from source control.
- Create a free account from the Cloudinary website.
- Sign in to the Cloudinary Console.
- Open Console Settings.
- Select API Keys.
- Keep the page available while you configure the local connection.
Which values do you need?
- The Cloud Name identifies your Cloudinary product environment.
- The API Key identifies the calling application.
- The API Secret proves that the server is allowed to make signed requests.
- Use Android's Recent Apps view to return to Acode.
- Open .env.local from the Files panel.
- Add a completed Cloudinary connection line by adapting this template:
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
How does the connection string work?
- Replace your-api-key with the value beside API Key.
- Replace your-api-secret with the value beside API Secret.
- Replace your-cloud-name with the value beside Cloud Name.
- Save the completed .env.local file.
- Focus the terminal that is running the PixelFixer development server.
- Press Ctrl+C to stop the server.
- Restart the development server so it loads CLOUDINARY_URL by running:
npm run dev
Why restart the server?
The development server reads .env.local when it starts. Restarting makes the new Cloudinary connection available to server code.
You should see the local development URL in the terminal again. PixelFixer is running with the new environment configuration loaded.
- Open src/lib in Acode's Files panel.
- Create cloudinary.js inside src/lib.
- Paste the server configuration below:
import {v2 as cloudinary} from "cloudinary"
cloudinary.config({secure:true,signature_algorithm:"sha256"})
export{cloudinary}
What does this configuration do?
- The v2 import provides Cloudinary's current Node.js API.
- The secure:true option generates HTTPS delivery URLs.
- The signature_algorithm:"sha256" option selects the stronger signature algorithm for signed operations.
- The exported cloudinary client is used only by the restoration Server Action.
- Save src/lib/cloudinary.js using Acode's save control.
- Confirm that cloudinary.js appears inside src/lib in the Files panel.
Server not restarting?
Check that the CLOUDINARY_URL line has no spaces around the equals sign. Confirm that each placeholder was replaced with the matching value from Cloudinary.
If the terminal reports a syntax problem, compare src/lib/cloudinary.js with the three lines above.
help me troubleshoot my PixelFixer Cloudinary configuration
Create the restoration Server Action
The restoration action is the guarded path between the upload form and Cloudinary. It checks the user session before accepting a file.
The action also treats the image upload and database row as one workflow. A failed database insert triggers deletion of the uploaded asset.
- Open src/app in Acode's Files panel.
- Create actions.js inside src/app.
- Paste the restoration workflow below:
"use server"
import {revalidatePath} from "next/cache"
import {redirect} from "next/navigation"
import {cloudinary} from "@/lib/cloudinary"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
const MAX_FILE_BYTES=3*1024*1024
const ALLOWED_TYPES=new Set(["image/jpeg","image/png"])
export async function restorePhoto(formData){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();const userId=claimsData?.claims?.sub;if(claimsError||!userId)redirect("/login");const file=formData.get("photo");const presetKey=formData.get("preset");const preset=PRESETS[presetKey];if(!(file instanceof File)||file.size===0)throw new Error("Choose a JPG or PNG photo before processing.");if(!ALLOWED_TYPES.has(file.type))throw new Error("This demo accepts JPG and PNG images only.");if(file.size>MAX_FILE_BYTES)throw new Error("Use a demonstration image smaller than 3 MB.");if(!preset)throw new Error("Choose a valid PixelFixer preset.");const buffer=Buffer.from(await file.arrayBuffer());const uploadResult=await new Promise((resolve,reject)=>{cloudinary.uploader.upload_stream({resource_type:"image",type:"authenticated",folder:`pixelfixer/${userId}`,eager:[preset.transformation],eager_async:false},(uploadError,result)=>{if(uploadError||!result){reject(uploadError??new Error("Cloudinary returned no result."));return}resolve(result)}).end(buffer)});const resultUrl=uploadResult.eager?.[0]?.secure_url;const originalUrl=cloudinary.url(`${uploadResult.public_id}.${uploadResult.format}`,{resource_type:"image",type:"authenticated",sign_url:true,secure:true});if(!resultUrl){await cloudinary.uploader.destroy(uploadResult.public_id,{resource_type:"image",type:"authenticated",invalidate:true});throw new Error("The selected transformation did not finish successfully.")}const{data:insertedJobs,error:insertError}=await supabase.from("restoration_jobs").insert({user_id:userId,preset:presetKey,source_public_id:uploadResult.public_id,original_url:originalUrl,result_url:resultUrl}).select();if(insertError||!insertedJobs?.length){await cloudinary.uploader.destroy(uploadResult.public_id,{resource_type:"image",type:"authenticated",invalidate:true});throw new Error("The image was processed but its private history was not saved.")}revalidatePath("/");redirect("/")}
What does this action protect?
- The claims check extracts userId from the signed-in Supabase session.
- The file checks allow image/jpeg or image/png content up to 3 MB.
- The preset check accepts only keys already present in PRESETS.
- The upload uses the authenticated delivery type inside the signed-in user's folder.
- The eager option creates the selected transformation during the upload request.
- The signed original URL allows the authenticated asset to render in PixelFixer.
- The database insert records the original URL plus the result URL in restoration_jobs.
- The rollback deletes the Cloudinary asset when processing or history storage does not finish successfully.
- Save src/app/actions.js using Acode's save control.
- Confirm that actions.js appears inside src/app in the Files panel.
Action file showing a problem?
Confirm that the first line is "use server". Check that the imported paths use the existing @/* alias.
If the Cloudinary import cannot be resolved, confirm that src/lib/cloudinary.js appears in Acode's Files panel.
help me debug the PixelFixer restorePhoto Server Action
Connect the studio and verify a result
The Next.js form can send its file directly to restorePhoto through the form action. The refreshed page then reads the new user-owned row from Supabase.
- Create src/app/error.js with this safe request failure screen:
"use client"
export default function ErrorPage({error,reset}){return <main className="grid min-h-screen place-items-center px-5 py-12"><section className="max-w-lg rounded-xl border border-border bg-card p-7 text-center shadow-sm"><div aria-label="Logo placeholder" className="mx-auto mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer stopped safely</p><h1 className="mt-3 text-2xl font-semibold">The request did not finish.</h1><p className="mt-3 text-muted-foreground">{error?.message??"Check the selected file and service configuration."}</p><button className="mt-6" onClick={reset}>Try again</button></section></main>}
What does the error screen do?
- The "use client" directive allows the reset button to respond in the browser.
- The error message explains why a request stopped without exposing credentials.
- The reset callback gives the learner a direct way to try the request again.
- Save src/app/error.js using Acode's save control.
- Confirm that error.js appears inside src/app in the Files panel.
Error screen not compiling?
Make sure "use client" is the first line. Check the surrounding braces if Acode highlights the component.
help me fix the PixelFixer error boundary
- Open src/app/page.js in Acode.
- Replace its existing contents with the authenticated restoration studio below:
import {redirect} from "next/navigation"
import {restorePhoto} from "./actions"
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
export default async function HomePage(){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();if(claimsError||!claimsData?.claims)redirect("/login");const{data:jobs,error:jobsError}=await supabase.from("restoration_jobs").select("id,preset,original_url,result_url,created_at");if(jobsError)throw new Error("PixelFixer could not load your restoration history.");const sortedJobs=[...(jobs??[])].sort((a,b)=>new Date(b.created_at)-new Date(a.created_at));const latestJob=sortedJobs[0];return <main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14"><header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Signed in as {claimsData.claims.email}. Results are AI-enhanced and may contain plausible details that were not present in the source.</p></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">{sortedJobs.length} saved result{sortedJobs.length===1?"":"s"}</p></header><section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]"><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</CardDescription></CardHeader><CardContent><form action={restorePhoto} className="grid gap-5"><label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png" required/></label><label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore">{Object.entries(PRESETS).map(([key,preset])=><option key={key} value={key}>{preset.label}: {preset.description}</option>)}</select></label><button type="submit">Process Photo</button></form></CardContent></Card><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>{latestJob?"Latest before and after":"Your result will appear here"}</CardTitle><CardDescription>{latestJob?(PRESETS[latestJob.preset]?.label??latestJob.preset):"Process a photo to create your first private history entry."}</CardDescription></CardHeader><CardContent>{latestJob?<div className="grid gap-4 sm:grid-cols-2"><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.original_url} alt="Original uploaded photograph" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.result_url} alt="AI-enhanced PixelFixer result" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure></div>:<div className="grid min-h-72 place-items-center rounded-xl border border-dashed border-border bg-background/40 p-8 text-center text-muted-foreground">No restoration jobs yet.</div>}</CardContent></Card></section><section className="mt-8"><div className="mb-4"><h2 className="text-xl font-semibold">Private history</h2><p className="mt-1 text-sm text-muted-foreground">The same signed-in account can read this history from a separate browser session.</p></div>{sortedJobs.length?<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{sortedJobs.slice(0,6).map(job=><Card key={job.id} className="gap-4 overflow-hidden border-border/80 bg-card/90 py-0"><img src={job.result_url} alt={`${PRESETS[job.preset]?.label??job.preset} result`} className="aspect-[4/3] object-cover"/><CardContent className="pb-5"><p className="font-semibold">{PRESETS[job.preset]?.label??job.preset}</p><p className="mt-1 text-sm text-muted-foreground">{new Date(job.created_at).toLocaleString()}</p><a className="button-link mt-4 w-full" href={job.result_url} target="_blank" rel="noreferrer">Open full-size result</a></CardContent></Card>)}</div>:null}</section></main>}
How does the finished studio work?
- The page redirects visitors who do not have Supabase claims.
- The form sends photo plus preset to restorePhoto.
- The latest job displays the signed original beside the transformed result.
- The history section sorts saved jobs by date.
- The history grid displays up to six user-owned results.
- The interface labels generated output as AI-enhanced.
- Save src/app/page.js.
- Confirm that the development terminal finishes compiling the updated page.
Use a safe demonstration photo
Cloudinary Free includes 25 monthly credits. Generative Restore and Upscale have special transformation counts.
AI restoration can construct plausible details that were absent from the source. PixelFixer labels each result as AI-enhanced because the output is not historically exact.
- Use one small non-sensitive demonstration photo for this check.
- Avoid stress-testing the free account.
Before you submit the photo, do you expect the private history count to stay at zero or increase?
- Use Android's Recent Apps view to return to Chrome.
- Refresh http://localhost:3000.
- Tap the Photo control.
- Choose a non-sensitive JPG or PNG smaller than 3 MB from your OPPO A58.
- Select Smart Restore from the Output preset control.
- Tap Process Photo.
You should see the signed original beside an AI-enhanced result. The saved result count increases to 1.
That is the core workflow complete. PixelFixer now processes a real photo and stores the result in the signed-in user's private history.
Photo not processing?
Confirm that the selected file is a JPG or PNG smaller than 3 MB. Try a smaller demonstration image if the request stops during validation.
If the message mentions service configuration, check the CLOUDINARY_URL values in .env.local. Restart the development server after correcting the connection.
If the image processes without appearing in history, confirm that the existing restoration_jobs policies still permit the signed-in user to insert their own row.
help me debug a failed PixelFixer photo restoration
✔️ Awesome, I've got everything!
PixelFixer can now create a real AI-enhanced result and save it to private history.
ⓧ I'd like to double check the full code
Compare the files created or replaced in this step with these complete versions. Keep your real credentials only in .env.local.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
import {v2 as cloudinary} from "cloudinary"
cloudinary.config({secure:true,signature_algorithm:"sha256"})
export{cloudinary}
export const PRESETS={restore:{label:"Smart Restore",description:"Reduce compression damage, noise, and blur.",transformation:{effect:"gen_restore"}},upscale:{label:"Face Upscale",description:"Increase resolution with extra attention to faces.",transformation:{effect:"upscale"}},improve:{label:"Natural Enhance",description:"Balance color, contrast, brightness, and lighting.",transformation:{effect:"improve"}},sepia:{label:"Sepia",description:"Apply a warm historical tone.",transformation:{effect:"sepia"}},cartoon:{label:"Cartoon",description:"Create a clear illustrated finish.",transformation:{effect:"cartoonify"}}}
"use server"
import {revalidatePath} from "next/cache"
import {redirect} from "next/navigation"
import {cloudinary} from "@/lib/cloudinary"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
const MAX_FILE_BYTES=3*1024*1024
const ALLOWED_TYPES=new Set(["image/jpeg","image/png"])
export async function restorePhoto(formData){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();const userId=claimsData?.claims?.sub;if(claimsError||!userId)redirect("/login");const file=formData.get("photo");const presetKey=formData.get("preset");const preset=PRESETS[presetKey];if(!(file instanceof File)||file.size===0)throw new Error("Choose a JPG or PNG photo before processing.");if(!ALLOWED_TYPES.has(file.type))throw new Error("This demo accepts JPG and PNG images only.");if(file.size>MAX_FILE_BYTES)throw new Error("Use a demonstration image smaller than 3 MB.");if(!preset)throw new Error("Choose a valid PixelFixer preset.");const buffer=Buffer.from(await file.arrayBuffer());const uploadResult=await new Promise((resolve,reject)=>{cloudinary.uploader.upload_stream({resource_type:"image",type:"authenticated",folder:`pixelfixer/${userId}`,eager:[preset.transformation],eager_async:false},(uploadError,result)=>{if(uploadError||!result){reject(uploadError??new Error("Cloudinary returned no result."));return}resolve(result)}).end(buffer)});const resultUrl=uploadResult.eager?.[0]?.secure_url;const originalUrl=cloudinary.url(`${uploadResult.public_id}.${uploadResult.format}`,{resource_type:"image",type:"authenticated",sign_url:true,secure:true});if(!resultUrl){await cloudinary.uploader.destroy(uploadResult.public_id,{resource_type:"image",type:"authenticated",invalidate:true});throw new Error("The selected transformation did not finish successfully.")}const{data:insertedJobs,error:insertError}=await supabase.from("restoration_jobs").insert({user_id:userId,preset:presetKey,source_public_id:uploadResult.public_id,original_url:originalUrl,result_url:resultUrl}).select();if(insertError||!insertedJobs?.length){await cloudinary.uploader.destroy(uploadResult.public_id,{resource_type:"image",type:"authenticated",invalidate:true});throw new Error("The image was processed but its private history was not saved.")}revalidatePath("/");redirect("/")}
"use client"
export default function ErrorPage({error,reset}){return <main className="grid min-h-screen place-items-center px-5 py-12"><section className="max-w-lg rounded-xl border border-border bg-card p-7 text-center shadow-sm"><div aria-label="Logo placeholder" className="mx-auto mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer stopped safely</p><h1 className="mt-3 text-2xl font-semibold">The request did not finish.</h1><p className="mt-3 text-muted-foreground">{error?.message??"Check the selected file and service configuration."}</p><button className="mt-6" onClick={reset}>Try again</button></section></main>}
import {redirect} from "next/navigation"
import {restorePhoto} from "./actions"
import{Card,CardContent,CardDescription,CardHeader,CardTitle}from "@/components/ui/card"
import {PRESETS} from "@/lib/presets"
import {createClient} from "@/lib/supabase/server"
export default async function HomePage(){const supabase=await createClient();const{data:claimsData,error:claimsError}=await supabase.auth.getClaims();if(claimsError||!claimsData?.claims)redirect("/login");const{data:jobs,error:jobsError}=await supabase.from("restoration_jobs").select("id,preset,original_url,result_url,created_at");if(jobsError)throw new Error("PixelFixer could not load your restoration history.");const sortedJobs=[...(jobs??[])].sort((a,b)=>new Date(b.created_at)-new Date(a.created_at));const latestJob=sortedJobs[0];return <main className="mx-auto min-h-screen max-w-6xl px-5 py-10 md:py-14"><header className="mb-8 flex flex-col gap-4 border-b border-border pb-7 md:flex-row md:items-end md:justify-between"><div><div aria-label="Logo placeholder" className="mb-5 grid h-16 w-16 place-items-center rounded-xl border border-dashed border-primary px-2 text-xs text-primary">Your logo</div><p className="mb-2 text-sm font-semibold uppercase tracking-[0.2em] text-primary">PixelFixer restoration studio</p><h1 className="text-3xl font-semibold tracking-tight md:text-4xl">Repair the photo. Keep the memory.</h1><p className="mt-3 max-w-2xl leading-7 text-muted-foreground">Signed in as {claimsData.claims.email}. Results are AI-enhanced and may contain plausible details that were not present in the source.</p></div><p className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground">{sortedJobs.length} saved result{sortedJobs.length===1?"":"s"}</p></header><section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]"><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>Create a PixelFixer result</CardTitle><CardDescription>Use a non-sensitive JPG or PNG smaller than 3 MB while testing.</CardDescription></CardHeader><CardContent><form action={restorePhoto} className="grid gap-5"><label className="grid gap-2 text-sm font-medium">Photo<input name="photo" type="file" accept="image/jpeg,image/png" required/></label><label className="grid gap-2 text-sm font-medium">Output preset<select name="preset" defaultValue="restore">{Object.entries(PRESETS).map(([key,preset])=><option key={key} value={key}>{preset.label}: {preset.description}</option>)}</select></label><button type="submit">Process Photo</button></form></CardContent></Card><Card className="border-border/80 bg-card/90 backdrop-blur"><CardHeader><CardTitle>{latestJob?"Latest before and after":"Your result will appear here"}</CardTitle><CardDescription>{latestJob?(PRESETS[latestJob.preset]?.label??latestJob.preset):"Process a photo to create your first private history entry."}</CardDescription></CardHeader><CardContent>{latestJob?<div className="grid gap-4 sm:grid-cols-2"><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.original_url} alt="Original uploaded photograph" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">Original</figcaption></figure><figure className="overflow-hidden rounded-xl border border-border bg-background"><img src={latestJob.result_url} alt="AI-enhanced PixelFixer result" className="aspect-square object-contain"/><figcaption className="border-t border-border px-4 py-3 text-sm text-muted-foreground">AI-enhanced result</figcaption></figure></div>:<div className="grid min-h-72 place-items-center rounded-xl border border-dashed border-border bg-background/40 p-8 text-center text-muted-foreground">No restoration jobs yet.</div>}</CardContent></Card></section><section className="mt-8"><div className="mb-4"><h2 className="text-xl font-semibold">Private history</h2><p className="mt-1 text-sm text-muted-foreground">The same signed-in account can read this history from a separate browser session.</p></div>{sortedJobs.length?<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{sortedJobs.slice(0,6).map(job=><Card key={job.id} className="gap-4 overflow-hidden border-border/80 bg-card/90 py-0"><img src={job.result_url} alt={`${PRESETS[job.preset]?.label??job.preset} result`} className="aspect-[4/3] object-cover"/><CardContent className="pb-5"><p className="font-semibold">{PRESETS[job.preset]?.label??job.preset}</p><p className="mt-1 text-sm text-muted-foreground">{new Date(job.created_at).toLocaleString()}</p><a className="button-link mt-4 w-full" href={job.result_url} target="_blank" rel="noreferrer">Open full-size result</a></CardContent></Card>)}</div>:null}</section></main>}
Your local PixelFixer workflow now restores a photo and preserves its private result. Next, you will deploy the same account-backed experience and compare regular with Incognito Chrome on your OPPO A58.
Deploy and Test Cross-Device Access
Deploy and Test Separate Mobile Sessions
PixelFixer now restores photos for a signed-in user. However, localhost cannot prove that a separate browser session can reach the same private history.
You will deploy the app to Vercel. You will then use separate browser sessions to access the same Supabase account data protected by Row Level Security.
In this step, get ready to:
- Prepare Vercel CLI version 64.5.0 in Acode's terminal.
- Configure the three required Production environment variables.
- Deploy PixelFixer and prove that its private history works in regular and Incognito Chrome sessions.
Install Vercel CLI and link PixelFixer
Vercel CLI connects your local pixelfixer-web folder to a hosted Vercel project. Version 64.5.0 keeps this deployment workflow reproducible.
Why deploy from the CLI?
A Git-based deployment usually starts from a hosted repository. The CLI can deploy the folder already stored on your OPPO A58, which keeps the final proof focused on shared accounts and separate browser sessions.
- Open the pixelfixer-web folder in Acode.
- Open Acode's terminal tab.
- Confirm that the terminal is inside pixelfixer-web.
- Check the installed Vercel CLI version by running:
vercel --version
What does this command do?
The command prints the Vercel CLI release available in this terminal. Choose the tab that matches your output.
✔️ I see version 64.5.0
Your terminal has the pinned release. Vercel CLI is ready for authentication.
ⓧ I see an older version
The installed release is older than the project pin. Update the global package before linking PixelFixer.
- Update Vercel CLI by running these commands:
npm i -g vercel
vercel --version
What do these commands do?
- The first command installs the current Vercel CLI release globally.
- The second command confirms that your terminal now uses version 64.5.0.
Did the update fail?
A global installation can fail when npm cannot write to its package directory. Keep your existing Node.js installation method instead of changing system permissions.
Ask for help with your environment: help me update Vercel CLI safely.
ⓧ Command not found
Vercel CLI is not installed yet. Install it globally so the command is available inside the PixelFixer terminal.
- Install Vercel CLI and confirm its version by running these commands:
npm i -g vercel
vercel --version
What do these commands do?
- The first command makes Vercel CLI available from your terminal.
- The second command should print version 64.5.0.
Still cannot run Vercel CLI?
Close the current terminal after installation. Create a fresh terminal so it can load the updated command path.
Ask for environment-specific guidance: help me find the Vercel command.
Authentication gives this terminal permission to manage your Vercel projects. The interactive flow keeps your account password outside the PixelFixer codebase.
- Start the Vercel authentication flow by running this command:
vercel login
What does this command do?
The command starts Vercel's interactive login flow. Your terminal receives authorization after you approve the request.
- Complete the browser authentication flow using your preferred sign-in method.
- Create a Vercel account through the browser flow if you do not have one.
You should see confirmation that authentication completed. That Vercel account can now own the PixelFixer deployment.
Authentication did not finish?
Keep the terminal open while you complete the browser flow. Restart the command if the request expires.
Ask for help without sharing credentials: help me complete Vercel authentication.
The link flow connects the current folder to one Vercel project. It stores that connection inside the ignored .vercel directory.
- Choose your Vercel account when the link flow asks for a scope.
- Choose to create a new project when the link flow asks about an existing project.
- Keep pixelfixer-web as the project name.
- Link the current PixelFixer folder by running this command:
vercel link
What does this command do?
The command associates the current local directory with a Vercel project. Your existing .gitignore keeps the generated .vercel metadata out of shared source code.
You should see confirmation that pixelfixer-web is linked. The completed app now has a deployment destination.
Project linking failed?
Confirm that the terminal belongs to the existing pixelfixer-web folder. A terminal attached to another folder links the wrong codebase.
Ask for help without sharing account details: help me link PixelFixer to Vercel.
Configure the production environment and deploy
The hosted app needs the same Supabase connection as your local app. It also needs the server-only credentials for Cloudinary.
You are about to transfer configuration values into Vercel. Keep CLOUDINARY_URL out of screenshots because it contains credentials.
- Prepare the NEXT_PUBLIC_SUPABASE_URL value from .env.local.
- Paste that value when the CLI prompts for it.
- Keep Config if Vercel asks for the variable type.
- Add the Supabase project URL to Production by running this command:
vercel env add NEXT_PUBLIC_SUPABASE_URL production
What does this command do?
The command stores NEXT_PUBLIC_SUPABASE_URL for Production deployments. It points the hosted app to the Supabase project containing your account and restoration history.
You should see confirmation that the variable was added to Production. The first hosted connection value is ready.
- Prepare the NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY value from .env.local.
- Paste that value when the CLI prompts for it.
- Keep Config if Vercel asks for the variable type.
- Add the Supabase publishable key to Production by running this command:
vercel env add NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY production
What does this command do?
The command stores NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY for Production deployments. PixelFixer uses it to start user-scoped Supabase requests.
You should see another Production confirmation. The deployed login flow now has both public Supabase values.
- Prepare the CLOUDINARY_URL value from .env.local.
- Paste that value when the CLI prompts for it.
- Choose Secret if Vercel asks for the variable type.
- Add the Cloudinary credentials to Production by running this command:
vercel env add CLOUDINARY_URL production
What does this command do?
The command stores CLOUDINARY_URL for server-side Production code. The deployed restorePhoto action uses it to upload authenticated images and generate transformations.
You should see a third Production confirmation. Your local .env.local file remains excluded from the deployment source.
Could not add a variable?
Confirm that the project link succeeded before adding Production settings. Repeat the affected command if you submitted a blank value.
Ask for help without including any values: help me configure PixelFixer on Vercel.
The first Production build can take a few minutes while Vercel installs the pinned packages and compiles Next.js. A quiet build log during this stage does not mean the deployment has stopped.
Before you deploy, do you think a fresh browser session will open the studio or send you to the login page?
- Create the Production deployment by running this command:
vercel deploy --prod
What does this command do?
Vercel builds the linked PixelFixer project with its Production environment variables. The successful deployment is assigned to a Production URL.
The terminal should finish with a live URL. A fresh browser session reaches the PixelFixer login page because it does not have your localhost cookie.
- Record the live URL here: your Vercel production URL.
Did the Production build fail?
Read the first file or variable named in the build output. Confirm that all three environment-variable names match the commands above.
Ask for help using only non-sensitive output: help me debug the PixelFixer deployment.
Prove shared history across two mobile sessions
Each browser creates its own cookie-backed session after sign-in. Both sessions resolve to the same Supabase user when you enter the same account credentials.
- Open regular Chrome on your OPPO A58.
- Enter your Vercel production URL in the address bar.
- Sign in with the Supabase email account used for the local PixelFixer app.
- Confirm that the existing restoration appears under Private history.
The Production session can read the existing user-owned row. This proves that the deployment points to the correct Supabase project.
Why does the same history appear?
Supabase identifies both sessions as the same user after sign-in. The Row Level Security policies allow that user to read only rows carrying the matching user ID.
The saved signed URLs load the authenticated Cloudinary original and transformed result. The history belongs to the account instead of one browser.
- Tap Chrome's More menu.
- Tap Open in a new Incognito window.
- Enter your Vercel production URL in the Incognito address bar.
- Sign in with the same Supabase email account.
- Confirm that the regular-session restoration appears under Private history.
The same private history now works through two independent sessions. PixelFixer has moved beyond localhost.
- Tap the Photo control on the OPPO A58.
- Select a non-sensitive JPG or PNG smaller than 3 MB.
- Choose Smart Restore in the Output preset field.
- Tap Process Photo.
- Keep Chrome open until PixelFixer returns to the studio.
- Confirm that the signed original and AI-enhanced result appear on the phone.
That is the Incognito-session write working. The new restoration is now stored in the same user-owned history as the regular session.
Before you return to regular Chrome, do you expect the Incognito-created result to stay in Incognito or appear in the shared history?
- Switch from the Incognito window to regular Chrome.
- Refresh your Vercel production URL.
You should see the Incognito-created result at the top of the regular Chrome history. You should also see its signed original and AI-enhanced transformation.
Strong finish. Regular and Incognito Chrome now use separate sessions for one shared PixelFixer account on your OPPO A58.
Do the sessions show different histories?
Confirm that both sessions use the same Production URL. Check that regular and Incognito Chrome display the same signed-in email address.
help me debug shared history between Chrome sessions.
Secret mission
Add a Chained Restore + Balance Preset
Extend PixelFixer with a Restore + Balance preset that repairs damage before balancing color, contrast, and brightness. Prove it locally on your OPPO A58, then verify the deployed result across regular and Incognito Chrome sessions.
Clean Up Your Resources
Clean Up Your Resources
Your local workspace has no running cost. This small demonstration can remain within the hosted free allowances.
Choose whether to keep your resources running, pause them to return later, or delete them entirely.
Cost warning
The Cloudinary Free plan includes 25 monthly credits. Generative Restore and Upscale have special transformation counts.
The Vercel Hobby plan costs $0/mo. for non-commercial personal use. Production use can require paid capacity.
Resources you used:
- The pixelfixer-web Next.js project inside Acode's Alpine Linux home folder, including installed packages and .env.local.
- The dedicated PixelFixer project in Supabase, including Auth users and Row Level Security protected restoration_jobs rows.
- The Cloudinary product environment containing authenticated assets whose public IDs begin with pixelfixer/.
- The linked Vercel project containing the production deployment and its three Production environment variables.
Protect Your Test Photos
Authenticated Cloudinary assets require signed URLs. Anyone who receives an exposed signed URL can reuse it.
- Use non-sensitive demonstration photos while PixelFixer remains an MVP.
Keep everything running
No action is needed. Choose this if you want to keep testing PixelFixer on your OPPO A58.
- Keep ~/pixelfixer-web inside Acode's Alpine Linux home folder.
- Keep the local development server running only while you are actively testing.
- Keep the dedicated PixelFixer Supabase project available for account history.
- Keep the authenticated pixelfixer/ assets in Cloudinary.
- Keep the linked Vercel project available for regular and Incognito Chrome testing.
- Monitor Cloudinary transformation usage to stay within the free allowance.
Keep credentials private
Your .env.local file contains service credentials. Keep it out of any folder or archive you share.
Pause - I'll come back to this later
Stop the local server to free Acode's terminal session. Your project files and hosted resources remain available for later.
- Return to the Acode terminal running PixelFixer.
- Press Ctrl+C to stop the server.
- Keep ~/pixelfixer-web inside the terminal's home folder.
- Leave the Supabase project available for your shared account history.
- Leave the Cloudinary assets available for signed image delivery.
- Leave the Vercel deployment available for future Chrome session checks.
A free Supabase project can pause after inactivity. Your files remain stored in Acode's terminal environment even if the hosted project pauses.
Archive without credentials
- Delete .env.local from any copied folder before sharing or archiving it.
Delete - I don't want to use this again
Remove every PixelFixer resource if you no longer need the app. This clears its hosted data and Acode workspace.
Deletion is permanent. The steps below target only the PixelFixer resources you created.
- Download any result you want to keep through PixelFixer's Open full-size result link.
- Open Cloudinary's Media Library.
- Search for assets whose public IDs begin with pixelfixer/.
- Select every matching asset.
- Use the Media Library's delete control to remove the selected assets.
- Confirm that no matching pixelfixer/ assets remain.
- Open the PixelFixer project in the Vercel dashboard.
- Open Settings.
- Scroll to the project deletion area.
- Use the permanent project deletion control to delete PixelFixer.
- Open the former Production URL to confirm that PixelFixer no longer loads.
- Open the dedicated PixelFixer project in Supabase.
- Open Project Settings.
- Select General.
- Use the project deletion control in the danger zone.
- Confirm that PixelFixer no longer appears in your Supabase project list.
The last action removes the local folder and its .env.local credentials from your phone. Check the path carefully before you run it.
- Open Acode's terminal tab.
- Remove the local PixelFixer folder by running:
rm -rf ~/pixelfixer-web
- Confirm that the folder is gone by running:
ls ~
You should no longer see pixelfixer-web in the output.
Nice Work!
Nice Work!
Excellent work. PixelFixer is now a live photo-restoration app built and tested entirely on your OPPO A58. Regular and Incognito Chrome sessions can reach the same private account history.
You've learned how to:
- Build a responsive restoration studio on Android. Let a signed-in user choose one of five presets. Display the original beside the AI-enhanced result.
- Use a shared Supabase identity in regular and Incognito Chrome. Protect each user's restoration_jobs rows with PostgreSQL Row Level Security.
- Upload photos as authenticated Cloudinary assets. Save successful jobs to private history. Publish a live Vercel deployment that works in separate Chrome sessions on your OPPO A58.
- Secret Mission: Extend PRESETS with a Restore + Balance preset. Chain damage restoration with color balancing through e_gen_restore/e_improve. Verify the result across regular and Incognito Chrome without changing the database schema.
Ready to quiz yourself?