Skip to content

RESOURCES / BLOG

The Vibecoder Storefront: Automating Premium UGC With TanStack Start and Cloudinary AI

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:

  1. Ingestion. The user uploads a raw image via a secure, signed handshake.
  2. Restoration. The agent automatically detects blur and compression artifacts (e_gen_restore).
  3. Contextualization. The agent strips the messy background and generates a professional studio setting (e_gen_background_replace).
  4. 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_SECRET with VITE_. 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.

  1. Log in to your Cloudinary Console..
  2. Navigate to Settings (Gear Icon) > Upload.
  3. 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.ts server function.
  • 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.jpg at the same time.
  • 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.

  1. Request: The client asks: “May I upload?”
  2. Sign: The server verifies the request and uses the CLOUDINARY_API_SECRET to generate a time-limited signature.
  3. 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:

  1. The UploadZone calls getUploadSignature.
  2. The Server returns a valid token.
  3. The Widget uploads the file to the vibecoder_raw folder.
  4. The public_id is 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 with pad() 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 AIPreview component.
// 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).

  1. The asset is immutable. We never overwrite the raw upload.
  2. The recipe is dynamic. We save the Cloudinary transformation string (e.g., e_gen_restore/e_bg_replace:studio) to a custom context key called mastered_chain.
  3. 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.

  1. The grid sets a predictable 1:1 rhythm for the gallery.
  2. The asset uses max-w-full and max-h-full to fit perfectly without touching the edges.
  3. 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:

  1. 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.
  2. Self-healing. If the AI model improves next month, you don’t need to reupload files. Just rerun the saveMasteredImage function to update the metadata, and the frontend will instantly reflect the better quality.
  3. Governance-ready. Because the mastered state is just data, you can easily add a step for human oversight (e.g., adding an approved: true flag 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_chain to 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 saveMasteredImage function 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:

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free