Realtime Mockup

WebSocket-based real-time mockup generation with live preview updates using useRealtimeMockup.

Interactive Demo

Try the interactive playground below - draw on the canvas and watch the product mockup update in real-time via WebSocket:

Loading playground...

Overview

The useRealtimeMockup hook connects to a WebSocket server for instant mockup generation as users make changes. Perfect for real-time product configurators and interactive design tools.

✨ Features

  • • WebSocket connection with auto-reconnect
  • • Real-time mockup generation (sub-second updates)
  • • WebP export format (~10x smaller than PNG) for fast uploads
  • • Event-driven auto-export with smart debouncing
  • • Multi-placement support (image + color placements)
  • • Fit mode with margins and alignment for precise artwork positioning
  • • Worker-based export (non-blocking UI)
  • • Session management and debug logging
  • • Automatic cleanup on unmount
This example uses SnowconeCanvas from @snowcone-app/canvas with exportImageFormat="webp" for ~10x smaller uploads compared to PNG. You can use any canvas library or drawing tool that can export image blobs. The hook only requires a blob to send via the sendCanvasBlob method.

Authorizing the session

The realtime socket requires a session token. Connecting without one is rejected with { "type": "error", "message": "A valid session token is required" } and closed with code 1008. Mint a short-lived grant from POST /realtime/grant, open the socket with ?token=, and renew before it expires. Renders are metered to the shop the token belongs to.

bash
# Mint a short-lived realtime session grant (TTL 60s).
# `shop` is your publishable key (= shop.id) — public, like Stripe's pk_.
curl -X POST https://api.snowcone.app/realtime/grant \
  -H "Content-Type: application/json" \
  -d '{ "shop": "dqMwU8Jct3" }'

# → { "token": "…", "expiresAt": 1733356800 }
#
# The client opens the socket with the token and renews before it expires:
#   wss://cdn.snowcone.app/realtime?token=<token>

In React you don't hand-roll this. Pass a getToken fetcher to the hook (it's called on connect and ~15s before expiry), or let RealtimeProvider do it for you by setting its shop and grantUrl props.

tsx
import { useRealtimeMockup } from '@snowcone-app/ui';

const { isConnected, sendConfig, sendCanvasBlob } = useRealtimeMockup({
  // wsUrl defaults to the public endpoint (wss://cdn.snowcone.app/realtime)
  // when getToken is set — pass it explicitly only to self-host.
  // Called on connect and ~15s before expiry to renew the session.
  // Point it at your own backend route that proxies POST /realtime/grant
  // (keeps any signing secret off the client).
  getToken: async () => {
    const res = await fetch('/api/backend/realtime/grant', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ shop: 'dqMwU8Jct3' }),
    });
    return res.json(); // { token, expiresAt }
  },
});
Signed shops (L3): shops with require_signed_urls must also send ts (epoch seconds, ±5 min skew) and signature — the hex HMAC-SHA256(signing_secret, "{shop}:{ts}"). The secret stays server-side; only the HMAC crosses the wire. For public shops, { shop } alone is enough.

Direct WebSocket protocol

The useRealtimeMockup hook speaks this for you. Driving the socket directly (a non-React or non-JS client) means three things: a JSON config message, one binary frame per placement, then a sequence of result messages.

json
// 1. Configure the session. Server replies with the placements it expects.
{ "type": "config", "config": {
    "productId": "BEEB77",
    "mockupIds": ["FV1qjO"],
    "variantId": "Pv1sLC",
    "shop": "dqMwU8Jct3",
    "width": 1000
} }

// ← { "type": "config_received", "requiredPlacements": ["Front"] }
//
// Optional: "ar" (aspect ratio, default "16:9"), "placementSettings".
// On the token path the billed shop comes from the session token, not "shop".

Artwork is sent as raw binary, not JSON — a UTF-8 header line naming the placement and version, then the image bytes. A JSON header is rejected with Invalid placement.

tsx
// 2. Send each placement's artwork as a BINARY frame (NOT JSON):
//    a UTF-8 header  "<placement>\n<version>\n"  then the raw image bytes.
const header = new TextEncoder().encode(`${placement}\n${version}\n`);
const bytes  = new Uint8Array(await blob.arrayBuffer());
const frame  = new Uint8Array(header.length + bytes.length);
frame.set(header, 0);
frame.set(bytes, header.length);
ws.send(frame);   // sending JSON here yields: Invalid placement "{…}"

Message lifecycle once connected:

bash
connected             { sessionId }
  → config_received     { requiredPlacements }
  → (client sends one binary blob per required placement)
  → have_all_blobs
  → rendering_started
  → mockup_rendered     { mockupId, imageUrl, placement, renderMs }   # one per mockup
  → all_mockups_rendered { mockups: [ … ] }
The returned imageUrl is an ephemeral, session-scoped render — download or copy it to your own storage if it needs to outlive the connection. Required config fields are productId, mockupIds, variantId, and width; mockup_rendered arrives once per mockup, then all_mockups_rendered closes the batch.

useRealtimeMockup Options

wsUrlstring
WebSocket server URL for real-time mockup generation.
getToken() => Promise<{ token: string; expiresAt: number }>
Optional grant fetcher. When provided, the session authorizes with a short-lived token (opened on the WS URL, renewed before expiry). Without it the socket is rejected with code 1008 — see Authorizing the session.

Hook Return Values

isConnectedboolean
Whether the WebSocket connection is active.
sessionIdstring | null
Current WebSocket session ID.
isConfiguredboolean
Whether the server has received and acknowledged the config.
mockupResultsMockupResult[]
Array of generated mockup results with imageUrl and mockupId.
statusstring
Connection status: "disconnected", "connecting", "connected", etc.
sendConfig(config: WebSocketConfig) => void
Send product configuration to server.
sendCanvasBlob(placement: string, blob: Blob, version: number, updateMs: number) => void
Send canvas artwork blob for a specific placement.

Installation

npm install @snowcone-app/ui

The hook and components ship in @snowcone-app/ui. Peers: react, react-dom, zod.

Styling — your Tailwind v4 build compiles it. In your global stylesheet, add @import '@snowcone-app/ui/styles/globals.css' (brings in Tailwind, the theme tokens, and an @source for the package) plus an @source for your own code. In Next.js, also add transpilePackages: ['@snowcone-app/ui']. Add className="dark" on an ancestor for dark mode.
Canvas Drawing: The SnowconeCanvas component used in the example is from @snowcone-app/canvas. You can use any canvas library that exports blobs - the key is implementing the onExport callback.