Getting Started
Render and steer a real-time session from a deployed uRun app
A uRun Python function runs on a remote GPU and produces media in real time. The TypeScript SDK is how you consume it in the browser: reference the deployed app, invoke a function, and render the session's streams. This guide builds a React video player that connects to a deployed function and steers it live.
Install
npm install @urun-sh/react @urun-sh/corenpm install @urun-sh/core@urun-sh/react supports React 18 and 19. You need your baseUrl (e.g. https://urun.sh) and orgId from your uRun console.
Package names
The browser client is @urun-sh/core (+ @urun-sh/react). It is distinct from the Python library urun and the CLI urun-cli. Do not confuse @urun-sh/core with the monorepo name.
Authentication
Browser clients never carry a server API key. They authenticate with your orgId plus a short-lived token, validated server-side.
Never ship secrets to the browser
Do not pass a long-lived API key or the urun_… deploy key to a browser client — every path below keeps it server-side.
The onramp: a token route in your app
The fastest way from zero to a live session: embed a token-vending endpoint in your Next.js app. Your org API key stays server-side in URUN_API_KEY; the route mints short-lived scoped client tokens with createClientToken (from @urun-sh/core — a server-side call, never imported from browser code); the SDK fetches them through the normal getAccessToken option. Minutes to a first session, no identity provider required.
// app/api/urun-token/route.ts — the onramp: the API key never leaves the server
import { createClientToken } from '@urun-sh/core'
export async function POST() {
const { token, expiresAt, orgId } = await createClientToken(process.env.URUN_API_KEY!, {
allowedFunctions: ['my-app/generate'], // only these app/functions
allowedOrigins: ['https://myapp.example'], // only presented from these Origins
expiresIn: 60, // seconds; gates STARTING sessions only
maxSessionS: 600, // per-session length cap
})
return Response.json({ token, expiresAt, orgId })
}// browser — the SDK fetches tokens from your route
const app = App('my-app', {
baseUrl: 'https://urun.sh',
orgId: 'your-org-id',
getAccessToken: async () => {
const res = await fetch('/api/urun-token', { method: 'POST' })
const body = (await res.json()) as { token: string }
return body.token
},
})Constraints are enforced server-side at session admission: the token can only start the allowlisted functions, from the allowlisted origins, within the session-length cap. Token expiry gates starting sessions only — a live session is never killed by its token expiring. Pass a stable subject (e.g. your user id) to coalesce one user's tabs into one session; by default every token gets a random subject, so distinct users never share a session.
What the onramp does and doesn't cover
Onramp tokens are org-scoped credentials your server mints for anyone it answers — right for prototypes, demos, and internal tools. If your product has real end users, put your identity provider in front: see Production auth best practices for WorkOS AuthKit and customer-JWT (trusted JWKS) setup, per-user identity, and key rotation.
Production auth (external identity provider)
In production, browser users authenticate against your identity provider and uRun verifies their short-lived JWTs directly:
- WorkOS AuthKit (default in production) — the browser obtains WorkOS access tokens; uRun validates them against your org's provisioned WorkOS JWKS. The React bridges (
UrunWorkOSProvider) wire it in a few lines. - Customer JWT — you bring your own JWTs, validated against a JWKS you register via
urun auth trust-jwk(or Settings → Frontend Auth in the console). Enable withNEXT_PUBLIC_AUTH_MODE=jwt.
Full setup — issuer/JWKS trust, per-user identity vs org tokens, key rotation: Production auth best practices.
React: render a stream
Wrap your tree in an auth bridge and UrunProvider, then call the function with useApp().
'use client'
import '@urun-sh/react/styles.css'
import { UrunProvider, useApp } from '@urun-sh/react'
import { UrunWorkOSProvider } from '@urun-sh/react/workos' // Next.js: '@urun-sh/react/next-workos'
import { useEffect, useRef, useState } from 'react'
function VideoPlayer() {
const app = useApp()
const videoRef = useRef<HTMLVideoElement>(null)
const [text, setText] = useState('a cat walking in a garden')
// Render-safe: the same args reuse the same session across re-renders.
const session = app.generate({ prompt: text })
const video = session.stream('video')
useEffect(() => {
if (video.track && videoRef.current) {
videoRef.current.srcObject = new MediaStream([video.track])
}
}, [video.track])
return (
<div>
<video ref={videoRef} autoPlay muted className="w-full max-w-2xl rounded-lg" />
<div className="flex gap-2 mt-4">
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => session.doc('control').set({ prompt: { text } })}>
Generate
</button>
</div>
</div>
)
}
export default function Home() {
return (
<UrunWorkOSProvider clientId="client_...">
<UrunProvider
baseUrl="https://urun.sh"
orgId="your-org-id"
authProvider="workos"
appId="my-app"
>
<VideoPlayer />
</UrunProvider>
</UrunWorkOSProvider>
)
}What happens:
UrunProviderconfigures the app proxy (baseUrl,orgId,authProvider,appId); the WorkOS bridge supplies access tokens.useApp()returns the proxy.app.generate({ prompt })invokes the deployed Pythongeneratefunction and returns a render-safeSession.session.stream('video')is the named media stream. Its.trackis the inboundMediaStreamTrack, which you assign to a<video>vianew MediaStream([track]).session.doc('control').set(...)writes the synced control doc; on the Python sidectx.doc("control")reads the same doc — a live inline read for cheap config,doc.bindfor structural change — and steers generation. No restart, no REST endpoint.
Render-safe sessions
useApp() memoizes by function name + args: calling app.generate(args) with the same args during re-renders reuses one session. session.stream(name) and session.doc(name) are likewise stable.
Vanilla: the same flow without React
import { App } from '@urun-sh/core'
const app = App('my-app', {
baseUrl: 'https://urun.sh',
orgId: 'your-org-id',
jwt: await getCurrentUserJwt(), // advanced/manual path; React WorkOS resolves this for you
authProvider: 'workos',
})
const session = app.generate({ prompt: 'a cat walking' })
const video = session.stream('video')
video.on('track', (track) => {
if (track) videoEl.srcObject = new MediaStream([track])
})
session.doc('control').set({ prompt: { text: 'a dog running' } })Session phases
A session moves through nine honest, renderable phases:
idle → queued | unavailable | provisioning → connecting → live → ended | expired
↘ errorWhile waiting, phase.queue carries { position, depth }, phase.runtime reports honest runtime readiness (unavailable | starting | busy + a reason), and phase.wakingSince timestamps the wait so you can render "waking (Ns)". On error, phase.error carries { reason, code?, kind? }.
import { describeSessionPhase, whenLive } from '@urun-sh/core'
session.onPhase((p) => statusEl.textContent = describeSessionPhase(p))
await whenLive(session) // resolve at 'live'; reject with a typed SessionFailedError
await video.attachVideo(localTrack) // only publish once liveSee Session lifecycle for the full phase model, scale-to-zero waking UX, session expiry, and idle handling; Transport for reconnection and viewer attach.
Media components
@urun-sh/react ships bare-name media components over the session's named streams so you rarely wire tracks by hand — send with <Camera> / <Mic>, show with <Video> / <Image> / <Audio>:
Session— provides one session to every media component beneath it, so each block needs nothing but a stream name (every component also takes an explicitsessionprop).Video— live video display for a named inbound stream (persistentsrcObject, iOS-safe attributes, honest empty state).Image— latest-image display over the platform image lane (H.264 intra frames on a video stream; default stream"image").Audio— gesture-safe playback of a session's inbound audio (handles the browser autoplay dance); sound-only unlesscontrols.Voice— a full-duplex voice loop: playback plus mic capture/attach over one named stream.Camera— camera capture and publish: viewless by default, self-view viavisible, declarativefront/backfacing with flip viareplaceTrack.Mic— viewless mic publish (the capture-only half ofVoice);visiblerenders a level meter.useSessionTrack(session, name)— the latest inboundMediaStreamTrack | nullfor a named stream, as React state.VideoPlayer— a video.js-backed player for VOD playback of a recording URL (src). video.js is an optional peer, so it lives behind a subpath: installvideo.jsand import from@urun-sh/react/video— it is deliberately not exported from the package root.
For live rendering, <Video> (or useSessionTrack + a <video> element) is all you need — VideoPlayer is only for apps that want player chrome over DVR recordings.
One copy-pasteable recipe for each of these — send camera (phone front/back), send mic, viewless sending, output video/audio/images — is on Inputs & outputs.
Next steps
- App reference —
App,app.<fn>(),Session, streams, docs. - Session lifecycle — phases, waking, expiry, idle, recordings.
- React providers — provider props, hooks, and auth bridges.
- Hooks reference — the full
@urun-sh/reacthook surface. - Python Getting Started — the backend that produces these streams.