← Technical journal

Product engineering · TypeScript · 5 min read

The API contract is the same. The result isn’t.

A source-level look at UGC Forge’s live and demo modes—and why a believable preview needs an unmistakable boundary around simulated work.

Anudeep Adiraju · September 2026

01 / System at a glance
  1. 01UI action

    Create a campaign or inspect a render

  2. 02API adapter

    One set of typed endpoint methods

  3. 03Live request

    Configured API URL → actual response

  4. 04Demo fallback

    Explicit demo mode or network TypeError

  5. 05View state

    Shared shapes; different provenance

The live request and demo fallback are alternative branches. HTTP error responses remain errors. Demo data is held in memory and its render progress is simulated.

A convincing interface creates an obligation

UGC Forge explores a workflow from campaign brief to generated scripts and video renders. Its public interface lets someone try that workflow with demo campaigns. The engineering question is what that experience promises when the video backend is absent.

An interface can look complete while its output is entirely simulated. That is useful for developing interactions, but only if the person using it knows what they are seeing. Here, the source exposes a clear adapter boundary worth examining: endpoint methods use a live API or fall back to a demo implementation with the same response types.

Centralize the choice of data source

The request helper derives its base URL from VITE_API_URL, with a localhost default. VITE_DEMO_MODE can bypass live requests. A module-level apiUnavailable flag also stops subsequent network attempts after a network TypeError. Endpoint wrappers catch the API_UNAVAILABLE sentinel and call the corresponding demo method.

This keeps campaign creation, statistics, scripts, and renders behind a consistent interface. Components do not each invent a different fallback. But the decision is sticky for the lifetime of the loaded module: one network failure can move later requests into demo mode until reload. A recovery control would make that behavior easier to understand.

UI → endpoint wrapper → request()
  response OK       → live data
  HTTP error        → surface error
  API_UNAVAILABLE   → demoApi

An HTTP error should stay an error

The helper throws an explicit error when a response is not OK. That error is not converted into demo data. The fallback instead activates for the sentinel or a network TypeError. This is a valuable distinction: an authorization failure or a rejected request should not quietly become a successful fake campaign.

A TypeError still covers more than “the backend is down.” A blocked cross-origin request or other fetch failure can look similar to the application. A production version should explain the transition and offer a retry, rather than suggesting it has diagnosed the infrastructure.

Shared types do not establish shared meaning

The demo implementation stores campaigns in an array and scripts and renders in maps. Delays make actions feel asynchronous. A timer advances render progress by ten points every half-second. Completion supplies an example.com video URL; generated scores are random values. None of those values demonstrates an actual rendered asset or a measured likelihood of success.

The state disappears when the page’s module is recreated. Calling this a database, persistent campaign history, or a working video-generation pipeline would overstate what the demo does. The strongest presentation is to label it as an interactive prototype and explain the real engineering beneath that prototype.

A change I would make at the contract level is to include provenance in the result: a discriminated live/demo field, rather than relying on surrounding UI text alone. That makes the distinction available to exports, analytics, and any component that consumes the response.

type Result<T> =
  | { mode: "live"; data: T }
  | { mode: "demo"; data: T; simulated: true }

// Proposed contract, not the existing implementation.

What I would finish before calling it live

The live render watcher currently points at a localhost WebSocket URL. Deployment needs an environment-aware WebSocket endpoint and secure transport. It also needs a real job lifecycle that survives reconnects and returns an actual downloadable asset.

I would test the boundary with four cases: explicit demo mode, an unavailable network, a rejected HTTP request, and a completed real render. Each case should have a distinct user-visible outcome. Tests that only prove the demo progress bar reaches 100 percent miss the most important behavior.

The point of a prototype is to make a product decision easier. In this case, it can test whether the campaign-to-script-to-render workflow makes sense. It cannot yet tell us about output quality, generation latency, or campaign performance. Making that distinction part of the design is itself product work.