- Live Demo: https://vibecoder-storefront.vercel.app
- GitHub Repository: https://github.com/musebe/vibecoder-storefront
The core problem with user-generated content (UGC) marketplaces is that they can look messy. An uploaded photo of an expensive shoe? Great! But the photo was taken on a dimly lit kitchen table with a cluttered background, and since the buyer can’t see all the intricate details, they’re more likely to click away.
For developers, bridging this gap at scale is a nightmare. There are, unfortunately, three bad options. You can force users to learn photography (impossible). Manually review and edit every asset (unscalable). Build brittle server-side processing queues (expensive and slow!).
A zero-touch “Art Director” can fix uploaded photos so you don’t have to. In this guide, I’ll walk you through how to create one.
Instead of building a traditional editing pipeline that generates duplicate files for every change, you’ll use a metadata-first architecture with:
- TanStack Start as the interface. It handles the secure, signed ingestion and enables type-safe server functions.
- Cloudinary Programmable Media as the intelligence. It restores, expands, and contextualizes the asset using generative AI.
- Virtual mastering as the state. Store the AI transformation recipe in the asset’s metadata, keeping the original file pristine while serving image worthy of being studio-grade.
By decoupling the raw asset from its mastered state, you’ll create a self-healing pipeline:
- Ingestion. The user uploads a raw image via a secure, signed handshake.
-
Restoration. The agent automatically detects blur and compression artifacts (
e_gen_restore). -
Contextualization. The agent strips the messy background and generates a professional studio setting (
e_gen_background_replace). - Hydration. The gallery fetches the raw asset and metadata recipe to reconstruct the “mastered” view instantly, without waiting for search indexing.
Before you build the intelligence, you’ll need a high-performance home for it. You’ll utilize TanStack Start because it bridges the gap between client-side interactivity and secure server-side operations without the bloat of traditional frameworks.
Run the following command in your terminal to create a fresh, type-safe workspace. This sets up the router, TypeScript, and Tailwind CSS automatically.
npm create @tanstack/start@latest vibecoder-storefront
Code language: CSS (css)
To keep your AI mastering logic isolated and scalable, enforce a domain-driven folder structure. Don’t scatter files into generic components or utils folders. Instead, group everything related to the “Art Director” agent into a single feature directory.
Create the following structure inside your src folder:
src/
├── features/
│ └── upload/
│ ├── components/ # The "Control Room" (Client-Side UI)
│ │ ├── AIControlPanel.tsx
│ │ ├── AIPreview.tsx
│ │ └── UploadZone.tsx
│ └── server/ # The "Engine Room" (Server Functions)
│ └── upload.fn.ts
└── lib/
└── cloudinary.ts # SDK Configuration
- Components. These are your visual controls. They never touch the database or API keys directly.
-
Server. Files (like
upload.fn.ts) run strictly on the server, keeping your API secrets safe from the browser.
The connection between your TanStack server and the Cloudinary media cloud requires a secure bridge.
Create a .env file in your root directory. Note the distinction between the Public key (safe for the browser) and the Secret key (server-only).
# Client-Side (Vite)
VITE_CLOUDINARY_CLOUD_NAME="your_cloud_name"
# Server-Side (TanStack Functions)
CLOUDINARY_API_KEY="your_api_key"
CLOUDINARY_API_SECRET="your_api_secret"
Code language: PHP (php)
Pro Tip: Never prefix your
API_SECRETwithVITE_. It must remain invisible to the client to prevent unauthorized access to your media library.
Finally, you’ll create a centralized instance of the Cloudinary SDK to avoid re-initializing it in every file.
File:
src/lib/cloudinary.ts
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config({
cloud_name: process.env.VITE_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
});
export { cloudinary };
Code language: JavaScript (javascript)
With the engine initialized, you’re ready to configure the security protocols that will protect your pipeline.
The AI Agent needs a sterile environment to operate. You don’t want it auditing every random upload in your cloud; you want a dedicated pipeline for the Storefront.
To achieve this, configure a signed upload [=preset. This acts as a strict “doorman” that enforces your governance rules at the edge.
In a production environment, “unsigned” presets are risky because they expose your cloud to unauthenticated uploads. For the Vibecoder Storefront, you’ll use signed mode.
- The logic. The frontend can’t upload a file unless it first requests a cryptographic signature from our TanStack server.
- The benefit. This allows us to rate-limit users, validate file types on the server, and enforce strict metadata rules before the upload even starts.
Follow these steps to configure the preset that guards your vibecoder_raw folder.
- Log in to your Cloudinary Console..
- Navigate to Settings (Gear Icon) > Upload.
- Scroll down to Upload presets and click Add Upload Preset.
Configure the preset with these exact settings to match your codebase:
- Preset Name:
vibecoder_signed-
Note: This key is hardcoded in our
upload.fn.tsserver function.
-
Note: This key is hardcoded in our
- Signing Mode: Select Signed.
- Folder: Type
vibecoder_raw.- The Quarantine: By hardcoding this folder, you’ll ensure that all raw user uploads land in one isolated location, keeping your main media library clean.
- Unique filename: On.
- Why? It prevents overwrites if two users upload
shoe.jpgat the same time.
- Why? It prevents overwrites if two users upload
- Delivery type: Upload.
Click Save. You should now see vibecoder_signed in your list with the mode set to “Signed.”
By configuring this preset, you’ve created a quarantine zone.
- In:
vibecoder_raw(raw, messy user uploads). - Out: The gallery (curated, AI-mastered assets).
The AI Agent will only listen to assets that land in this folder, ensuring it never accidentally modifies your other marketing assets.
Professional applications do not expose API secrets to the browser. Instead, you’ll implement a cryptographic handshake.
- Request: The client asks: “May I upload?”
-
Sign: The server verifies the request and uses the
CLOUDINARY_API_SECRETto generate a time-limited signature. - Upload: The client attaches this signature to the file and sends it directly to Cloudinary.
In TanStack Start, you’ll create a Server Function that runs strictly in the backend. This function uses the Cloudinary SDK to generate a signature without ever revealing the secret key to the client.
// src/features/upload/server/upload.fn.ts
export const getUploadSignature = createServerFn({ method: 'POST' })
.handler(async () => {
const timestamp = Math.round(new Date().getTime() / 1000);
// The "Engine": Generates the cryptographic token
const signature = cloudinary.utils.api_sign_request(
{
timestamp,
upload_preset: 'vibecoder_signed', // Matches our Cloudinary setting
},
process.env.CLOUDINARY_API_SECRET!
);
return { signature, timestamp, apiKey: process.env.CLOUDINARY_API_KEY! };
});
Code language: JavaScript (javascript)
View full source: src/features/upload/server/upload.fn.ts
Now, you’ll build the “control room” component. This React component handles the drag-and-drop interaction and initiates the handshake.
// src/features/upload/components/UploadZone.tsx
export function UploadZone({ onUploadComplete }: UploadZoneProps) {
return (
<CldUploadWidget
// The Handshake: We fetch the signature from our server function
signatureEndpoint={async () => {
const { signature, timestamp, apiKey } = await getUploadSignature();
return { signature, timestamp, apiKey };
}}
onSuccess={(result) => {
// The Handoff: Pass the publicId to the Art Director
if (result.info && typeof result.info === "object") {
onUploadComplete(result.info.public_id);
}
}}
>
{({ open }) => (
<button onClick={() => open()} className="...">
Upload Raw Asset
</button>
)}
</CldUploadWidget>
);
}
Code language: PHP (php)
View full source: src/features/upload/components/UploadZone.tsx
When the user drops a file:
- The
UploadZonecallsgetUploadSignature. - The Server returns a valid token.
- The Widget uploads the file to the
vibecoder_rawfolder. - The
public_idis passed to the next stage: The Art Director.
The first job of an Art Director isn’t to be creative; it’s to be corrective. User uploads are often blurry, compressed, or tightly cropped. You can fix these issues programmatically using a deterministic chain to repair and expand.
-
The restoration layer. Use
generativeRestore()as a “Digital Polish.” This two-pass restoration filter removes JPEG artifacts and sharpens blurry details, ensuring the base asset is high quality before we modify it. -
The expansion layer. Product photos often suffer from tight cropping, where the item touches the edges of the frame. You can use
generativeFill()combined withpad()to intelligently expand the canvas to a 1:1 square ratio. The AI invents realistic surroundings (e.g., extending a table surface) where none existed before. -
The repair chain. Combine these two powerful operations into a single, type-safe transformation chain within your
AIPreviewcomponent.
// src/features/upload/components/AIPreview.tsx
import { pad } from '@cloudinary/url-gen/actions/resize';
import { generativeFill } from '@cloudinary/url-gen/qualifiers/background';
import { generativeRestore } from '@cloudinary/url-gen/actions/effect';
// ... inside your component
if (config.fill) {
// 1. Expansion: Force 1:1 ratio and invent missing pixels
aiImage.resize(
pad()
.width(1000)
.aspectRatio("1:1")
.background(generativeFill())
);
}
if (config.restore) {
// 2. Restoration: Fix blur and compression
aiImage.effect(generativeRestore());
}
Code language: JavaScript (javascript)
View full source: src/features/upload/components/AIPreview.tsx
By chaining these methods, you’ll turn a low-res, rectangular phone photo into a sharp, square, studio-ready asset. This provides the perfect blank canvas for the next step of creative contextualization.
The second phase transforms the product from a snapshot into a studio shoot. You’ll use natural language prompts to generate photorealistic environments and create inventory variants without a reshoot.
Use generativeBackgroundReplace() to strip away the original messy environment (kitchen tables, cluttered desks) and generate a new one based on a text prompt.
- The logic: The AI automatically segments the foreground object, generates a new background, and blends the lighting and shadows to match.
- The prompt: “Studio lighting, minimalist concrete podium, soft shadows.”
To scale your catalog, you need to show different color options. Instead of photographing every SKU, we use generativeRecolor(). This allows us to target specific objects (e.g., “Sneakers”) and swap their color values instantly (e.g., to #FF0000).
We implement these creative directives as conditional layers in our transformation pipeline.
// src/features/upload/components/AIPreview.tsx
import {
generativeBackgroundReplace,
generativeRecolor,
} from "@cloudinary/url-gen/actions/effect";
// ... inside the transformation logic
if (config.bgReplace) {
// 1. Context: "Put it on a marble table"
aiImage.effect(generativeBackgroundReplace().prompt(config.bgReplace));
}
if (config.recolor?.item && config.recolor?.color) {
// 2. Variant: "Make the shoes red"
aiImage.effect(generativeRecolor(config.recolor.item, config.recolor.color));
}
Code language: JavaScript (javascript)
With just a few lines of code, a single uploaded image now serves as the source for unlimited marketing variations, different backgrounds for different campaigns, and different colors for different inventory SKUs.
Traditional editors create a mess of image_v1.jpg, image_v2.jpg. This kills storage and breaks the link to the original asset. We solve this by storing the AI transformation string in the asset’s metadata (context).
- The asset is immutable. We never overwrite the raw upload.
-
The recipe is dynamic. We save the Cloudinary transformation string (e.g.,
e_gen_restore/e_bg_replace:studio) to a custom context key calledmastered_chain. - The result. Zero storage cost for variations.
You’ll use the Cloudinary Admin API to patch the metadata without re-uploading the image.
// src/features/upload/server/upload.fn.ts
export const saveMasteredImage = createServerFn({ method: "POST" })
.inputValidator(
z.object({ publicId: z.string(), transformation: z.string() })
)
.handler(async ({ data }) => {
// The "Engine": Updates metadata, not the file
await cloudinary.uploader.explicit(data.publicId, {
type: "upload",
context: { mastered_chain: data.transformation },
});
return { success: true };
});
Code language: JavaScript (javascript)
View full source: src/features/upload/server/upload.fn.ts
When the gallery loads, it doesn’t look for a new file. It fetches the raw image and the mastered_chain string and reconstructs the “Mastered” view on the fly. This ensures the Art Director can always undo or remix the edit later.
A masterful asset looks amateur if the UI stretches it. A horizontal camera shot looks terrible when forced into a vertical card. To fix this, build a context-aware visualizer.
Start by enforcing a strict aspect-square grid for the container, but allow the asset to float freely inside it using object-contain.
- The grid sets a predictable 1:1 rhythm for the gallery.
- The asset uses
max-w-fullandmax-h-fullto fit perfectly without touching the edges. - The result is a product that centers itself as if it were shot professionally, whether it’s tall (bottle) or wide (shoes).
Next, implement a CompareSlider component that handles the before and after reveal without layout shifts.
// src/features/upload/components/CompareSlider.tsx
export function CompareSlider({ before, after }: CompareSliderProps) {
return (
<div className="relative w-full aspect-square rounded-3xl overflow-hidden bg-gray-50 border border-gray-100">
{/* The After Image (Base) */}
<AdvancedImage
cldImg={after}
className="absolute inset-0 w-full h-full object-contain p-8"
/>
{/* The Before Image (Overlay) */}
<div
className="absolute inset-0 w-1/2 overflow-hidden border-r-2 border-white/50"
style={{ width: `${sliderPosition}%` }}
>
<AdvancedImage
cldImg={before}
className="absolute inset-0 w-full h-full object-contain p-8"
// Crucial: Counter-translate to keep image static while container clips
style={{ width: '200%' }}
/>
</div>
</div>
);
}
Code language: JavaScript (javascript)
View full source: src/features/upload/components/CompareSlider.tsx
By adding p-8 (padding) and object-contain, you can create a “virtual matte” around the product. This ensures that even if the AI expands the background, the core product remains the visual anchor of the card.
When a user clicks Save, they expect the gallery to update immediately. However, standard Search APIs often have a two- to five-second indexing delay. This creates a phantom state during which the user wonders if their edit actually worked.
Solve this by switching to the Admin API to read directly from the folder source.
The Search API is fast for filtering millions of assets, but suffers from index lag. The Admin API, on the other hand, is slower for massive datasets, but offers real-time consistency for specific folders.
Since your vibecoder_raw folder is a curated workspace, you should prioritize consistency over raw query speed.
Implement a direct fetch using cloudinary.api.resources. This bypasses the search index and reads the file list straight from the disk.
// src/features/upload/server/upload.fn.ts
export const getGalleryImages = createServerFn({ method: "GET" }).handler(
async ({ data }) => {
// 1. Direct Fetch: Bypasses the Search Index
const result = await cloudinary.api.resources({
type: "upload",
prefix: "vibecoder_raw",
max_results: 50,
context: true, // Crucial: Fetches the 'mastered_chain' metadata
direction: "desc",
});
// 2. Client-Side Sort: Ensures "Recently Saved" pops to the top
const sorted = result.resources.sort((a, b) => {
const dateA = new Date(a.updated_at || a.created_at).getTime();
const dateB = new Date(b.updated_at || b.created_at).getTime();
return dateB - dateA;
});
return sorted.map((res) => ({
publicId: res.public_id,
masteredChain: res.context?.custom?.mastered_chain || null,
}));
}
);
Code language: JavaScript (javascript)
View full source: src/features/upload/server/upload.fn.ts
By manually sorting based on updated_at, the gallery becomes self-healing. As soon as the metadata patch is confirmed, the asset jumps to the top of the list with its new AI transformation applied, creating a seamless feedback loop for the Art Director.
You’ve successfully moved from an approach in which every new image requirement meant a new code deployment, to an AI Agent architecture. By decoupling the asset (the raw file) from the intent (the metadata recipe), you created a system that’s:
- Efficient for massive volumes of storage. One thousand product variants now cost 0 bytes of extra storage due to keeping 1,000 text strings in the metadata.
-
Self-healing. If the AI model improves next month, you don’t need to reupload files. Just rerun the
saveMasteredImagefunction to update the metadata, and the frontend will instantly reflect the better quality. -
Governance-ready. Because the mastered state is just data, you can easily add a step for human oversight (e.g., adding an
approved: trueflag to the metadata) before publishing to the live storefront.
The true breakthrough is the shift from imperative editing (e.g., crop to 500px, increase brightness by 10%) to declarative intent (e.g., make this look like a studio shoot). The old method is brittle, pixel-perfect fragility, while the vibecoder method is semantic, resilient, and adaptive.
This architecture is the foundation. To scale it into a full enterprise digital asset management (DAM) system, your next engineering steps would be:
-
Multi-channel variants. Add a new metadata key for
social_story_chainto automatically generate 9:16 vertical video assets from the same static product shot. - Webhooks. Use Cloudinary notification URLs to trigger a Slack alert whenever the AI Agent finishes a batch of 100 restorations.
-
Role-based access. Lock the
saveMasteredImagefunction behind an admin role so only approved Art Directors can commit changes to the public gallery.
You now have the blueprint! Sign up for a free Cloudinary account today and build the future of automated media.
Resources:
- Live Demo: https://vibecoder.vercel.app
- Source Code: https://github.com/musebe/vibecoder-storefront
- Cloudinary Docs: Generative AI Transformations