Skip to main content
A guided tour of the open-source Visko Orbis Stable reference app, which demonstrates every important pattern in the typed SDK. By the end you’ll know how to start scenes from prompts or images, steer a scene live with per-chunk prompts, snap clips, and surface model errors.

Installation and setup

Get the example running locally before reading further. Every section below points back at code in the repo you just cloned. You will need:
  • Node.js 18+.
  • pnpm (the example pins lockfiles to pnpm; npm or yarn will work but you’ll regenerate the lockfile).
  • A Reactor API key (starts with rk_).
  • Familiarity with the Next.js App Router.
1

Clone the example

The example lives alongside the other reference apps in reactor-team/js-sdk under examples/.
2

Add your API key

Your rk_… key must never reach the browser; the example reads it server-side and mints a short-lived JWT for the client. We’ll cover the broker pattern below; for now, drop the key into .env:
See a “Setup Required” screen? Your REACTOR_API_KEY isn’t loaded. The check lives in app/page.tsxapp/SetupRequired.tsx.
3

Install dependencies and run

Open http://localhost:3000, click Connect, and pick a starting point: a curated prompt, an example image, or your own text.

How Visko Orbis Stable works

Building with Visko Orbis Stable is different from calling a typical generative API. There’s no prompt-in / video-out request. You open a long-lived connection, send a prompt, and the model begins producing a continuous stream of 33-frame chunks (~1.833 s each). You steer it by sending set_prompt while it runs, and the picture morphs at the next chunk boundary. Opening the connection isn’t instant — placing a Visko Orbis Stable session takes longer than a typical stateless API call, and the client moves through four states before media starts flowing:
Connection lifecycle: disconnected → connecting → waiting → ready
The waiting state is when the session is being placed. The reference app’s StatusBadge labels it honestly rather than spinning a mystery. Once the status reaches ready, commands take effect and chunks start arriving. See Sessions for the full breakdown. At ready the model is connected but idle; it won’t produce frames until you set a prompt and call start. From there a small set of SDK methods (setPrompt, start, pause / resume, reset) drives it through the rest of its lifecycle. Two things to note about those methods:
  • They’re asynchronous. Awaiting setPrompt registers the prompt with the model and confirms it landed; errors surface as command_error rather than thrown exceptions.
  • Errors arrive out-of-band. A broken precondition like start with no prompt surfaces later as a command_error event, not as a thrown exception.

Authentication

Visko Orbis Stable opens a long-lived WebRTC connection that the server needs to trust for hours. Shipping a raw rk_… key to the browser would hand full account access to anyone with devtools open, so instead the Reactor SDK presents a JWT minted server-side from your API key. Mint it session-scoped (authorization_details with type: "session"): the token can only start and operate its own Visko Orbis Stable sessions, it is short-lived (1 hour by default, capped at 6), and it is safe to hand to the client. Your rk_… key stays on the server. That means every Visko Orbis Stable frontend needs one server-side route that mints JWTs. In the example, that route is app/api/reactor/token/route.ts, a Next.js route handler that exchanges your rk_… key for a JWT and hands the JWT back alongside its server-reported expiry so the client can memoize it:
ViskoOrbisStableApp.tsx passes a memoizing resolver to <ViskoOrbisStableProvider>. The SDK re-invokes the resolver on every Coordinator HTTP hop — uploads, clip manifests, ICE refreshes — so the resolver has to return the same token for the whole life of a session rather than mint a fresh one per hop. It memoizes { jwt, expires_at } in module scope and only re-fetches when the current token is about to expire:
app/ViskoOrbisStableApp.tsx
The browser HTTP cache is not the place to memoize this. A Cache-Control: max-age strategy misses under DevTools “Disable cache”, under cache eviction, and behind a shared proxy, and the miss mints a fresh token with no sessions bound — which the next upload or clip request then rejects with a 403 naming authorization_details.resources.sessions.bind. The in-memory memoize above is the only cache that holds the token stable for a session’s whole life.
The broker pattern (server mints, client consumes) is the standard for any browser-side Reactor app, not just Visko Orbis Stable. See Authentication for the full concept page. jwtToken accepts either a static string or a resolver; pass the resolver form so the SDK never holds a stale string.

Starting from a prompt

Generation kicks off in two SDK calls: setPrompt registers the prompt, then start begins producing chunks. PromptComposer.tsx exposes a grid of curated presets and a free-text input, but every button routes through the same send function:
app/components/PromptComposer.tsx
The preset text comes from app/lib/prompts.ts, a curated scene library that’s the single source of truth for both these prompts and the mid-stream evolutions covered later in this page. Its header comment is worth reading: each prompt is a full paragraph for a reason, and that style is what lets the model hot-swap prompts smoothly later.
start requires a prompt. Skip the setPrompt call and you’ll get a command_error event back. See start for the full precondition list; how the example surfaces those errors is covered later in this article.

Starting from an image

Visko Orbis Stable can start from text alone — the model invents the opening frame from the prompt. But when you have a reference image, setImage matters. Image-to-video adds a second piece of conditioning, and chaining setImage → setPrompt → start directly hides a subtle race. setImage carries an upload that the runtime has to decode before the model dispatches it; start carries nothing and sails past on the same data channel. The first chunk is then generated from the prompt alone, no image conditioning at all. The image lands a tick later and only applies from chunk 1 onward, so the user sees the scene “correct itself” at the first chunk boundary. There is no combined image+prompt command at all. The safe order is just await uploadFileawait setImageawait setPromptawait start — the resolution of setImage carries the decode, so no broadcast to wait on:
app/components/ImageStarter.tsx
If anything in the chain fails (missing piece, non-image MIME, undecodable bytes), the model emits command_error and mutates nothing. The example’s second image path is for custom uploads, where the user’s prompt arrives later from a separate action. It’s shorter still: uploadFile, then setImage. The user types their own prompt in the composer above and clicks Start, at which point PromptComposer fires setPrompt + start. No ack wait is needed: by the time the human has typed and clicked, the upload has long since been decoded.
Non-16:9 starting images squash. The model resizes the reference to 832×480 with no crop — use a 16:9 frame (every curated image in the library is).

Going live

Once generation starts, the UI flips from the setup panel to its Live phase. The example wires three small components into the right-hand sidebar and main pane: a status badge that tracks the connection lifecycle, a “now playing” panel that mirrors the state snapshot and exposes transport controls, and the video pane itself. StatusBadge.tsx is the user’s window into the four-state connection machine. Every state — including the waiting step where the session is being placed — gets a visible label and color. It also holds a separate “priming” state that fires on generation_started and clears the moment the video track actually delivers:
app/components/StatusBadge.tsx
NowPlaying.tsx is the canonical example of how the rest of the app reads model state: subscribe once with useViskoOrbisStableState, hold the latest snapshot in useState, read fields off it. No event aggregation, no derived booleans, no useReducer over chunk_complete events.
app/components/NowPlaying.tsx
The panel also renders a “Run finished” call-to-action when generation_complete fires, because the model does NOT auto-restart after a run reaches max_chunks:
app/components/NowPlaying.tsx
The video pane is one typed component:
app/components/Video.tsx
<ViskoOrbisStableMainVideoView /> is a typed wrapper around <ReactorView track="main_video"> that handles <video> element setup, srcObject binding, and browser autoplay policy quirks. Style the outer container; never reach for the underlying element.

Hot-swapping prompts mid-stream

This is the model’s hero feature. Once started === true, calling setPrompt({ prompt }) is a morph on the next chunk boundary (~1.8 s) — no restart, no start() again, no cut, no ack wait. From the user’s perspective the scene just keeps going; from the model’s perspective the prompt schedule was updated for the next chunk boundary. EvolveScene.tsx matches the active prompt against the scene library and offers one-click continuations:
app/components/EvolveScene.tsx
Each button is a single setPrompt call. No start, no reset, no acknowledgment wait. The model is already generating, and the next 33-frame chunk picks up the new prompt automatically.

Snapping a clip

The SDK ships recording primitives so you don’t have to wire up MediaRecorder yourself. The example’s SnapClip.tsx captures the last 10 seconds of the live stream and opens a modal with the SDK’s built-in preview player and a download button.
app/components/SnapClip.tsx
Notice how the imports are from @reactor-team/js-sdk, not @reactor-models/visko-orbis-stable. Recording is a base-SDK feature: it works the same way for every Reactor model, and the typed model packages don’t re-export the recording surface. So direct base-SDK imports are idiomatic in this one place, and you can drop the file into any other Reactor example unchanged. The clip components inherit the jwtToken resolver from the provider via React context, so no auth plumbing is needed at the component level.
Clip preview in Chromium and Firefox requires hls.js, already in the example’s package.json. See Recordings for the full feature page, including continuous recording, programmatic capture, and retention policies.

Surfacing command_error

Every Visko Orbis Stable command can fail a precondition check (e.g. start before a prompt, a set_resolution value not on available_resolutions). The example never lets these fail silently.
app/components/CommandError.tsx
useViskoOrbisStableCommandError is the typed wrapper for the command_error message: it fires when the model rejects a command, carrying the failing command name and a human-readable reason. The component sits in the sidebar, renders nothing until an error arrives, and clears itself when the next state snapshot lands so a stale banner can’t pile up.
command_error is one of several messages the model emits. See the Messages table for the full list, including chunk_complete, conditions_ready, image_accepted, and state.

Going further

Not every Visko Orbis Stable feature is surfaced in this demo. See below for a list of what’s missing and the one-line addition that wires each one in.