docs

Cheatsheet

Common patterns — Python backend and TypeScript frontend side by side

Quick reference for the most common things you'll do with uRun. Each pattern shows the Python GPU runtime and the TypeScript frontend together.

The product primitive is a Session: reference an App, invoke a deployed function by name to get a Session, then exchange media streams and a synced control document with the running GPU.

Streams carry media: ctx.stream("video") on Python, session.stream("video") on TypeScript. Same name, both sides.

Documents carry state: ctx.doc("control") on Python, session.doc("control") on TypeScript. CRDT-backed, synced to the GPU runtime over the session transport.

Store carries weight: store / @store.cache on Python is the content-addressed object store behind every function — model weights, caches, tensors.

The real React surface

The core of @urun-sh/react is useApp() — everything hangs off the Session it returns: session.stream(name) returns a SessionStream (read the inbound track via .track or .on('track', …), publish via .attach(track) / .detach()), and session.doc(key) returns the synced document. On top of that the package ships focused hooks (useRequest / useCompletion / useChat, useSessionTrack, useSessionDoc, useStreamMessages, useSessionEndsAt, useSessionIdle) and drop-in components (media: Camera, Voice, Audio, Video, Image, Mic under a Session; session honesty: UrunSessionClock, UrunIdleWarning) — see the hooks reference and Inputs & Outputs. Every snippet below uses only the real published API.


Setup

Python
import urun
from urun import App, store

app = App("my-app")
TypeScript (vanilla)
import { App } from "@urun-sh/core"

const app = App("my-app", {
  baseUrl: "https://urun.sh",
  orgId: "your-org-id",
})
TypeScript (React)
import { UrunProvider, useApp } from "@urun-sh/react"
import { useMemo } from "react"

function Root() {
  return (
    <UrunProvider
      baseUrl="https://urun.sh"
      orgId="your-org-id"
      appId="my-app"
      authProvider="workos"
    >
      <MyApp />
    </UrunProvider>
  )
}

function MyApp() {
  const app = useApp()                            // App("my-app") — same name as Python side
  const session = useMemo(() => app.generate(), [app]) // calls @app.function on the GPU
  const video = useMemo(() => session.stream("video"), [session]) // SessionStream
  // video.on("track", (t) => { ... })            // inbound track from ctx.stream("video")
  // session.doc("control").set({...})            // CRDT state → ctx.doc("control")
}

All examples below assume app / session are created as shown above.


Stream video from GPU to browser

Python
@app.function(gpus="h100:1")
async def generate(ctx: urun.Context):
    video = ctx.stream("video", codec="h264", fps=24)
    model = store.get("my-model")

    def produce(*, stop_event, config):
        while not stop_event.is_set():
            yield model.generate()

    pipeline = (
        ctx.source(produce, name="generate", device="cuda")
        | ctx.pace(fps=24)
        | video
    )
TypeScript
const session = app.generate()

const video = session.stream("video")       // SessionStream
video.on("track", (track) => {
  if (track) videoElement.srcObject = new MediaStream([track])
})
TypeScript (React)
function VideoPlayer() {
  const app = useApp()
  const session = useMemo(() => app.generate(), [app])
  const video = useMemo(() => session.stream("video"), [session])
  const videoRef = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    return video.on("track", (track) => {
      if (track && videoRef.current) {
        videoRef.current.srcObject = new MediaStream([track])
      }
    })
  }, [video])

  return <video ref={videoRef} autoPlay muted playsInline />
}

Send a prompt from browser to GPU

Python
@app.function(gpus="h100:1")
async def generate(ctx: urun.Context):
    control = ctx.doc("control")
    video = ctx.stream("video", codec="h264", fps=24)

    def produce(*, stop_event, config):
        while not stop_event.is_set():
            # live inline doc read — picks up browser writes each chunk
            prompt = control.get("prompt.text", "default")
            yield model.generate(prompt)

    pipeline = (
        ctx.source(produce, name="generate", device="cuda")
        | ctx.pace(fps=24)
        | video
    )
    await pipeline.run()
TypeScript
const session = app.generate()

// Mutate the control doc — CRDT syncs to GPU
session.doc("control").set({
  prompt: {
    text: "A sunset over the ocean",
    reset: true,
  },
})
TypeScript (React)
function PromptInput() {
  const app = useApp()
  const session = app.generate()

  return (
    <input
      placeholder="Enter a prompt"
      onKeyDown={(e) => {
        if (e.key === "Enter") {
          session.doc("control").set({
            prompt: { text: e.currentTarget.value, reset: true },
          })
        }
      }}
    />
  )
}

Build an SPMD pipeline across GPUs

Python — 2 GPUs
@app.function(gpus="b200:2")
async def generate(ctx: urun.Context):
    video = ctx.stream("video", codec="h264",
                       fps=24, rank=1)
    control = ctx.doc("control")

    pipeline = (
        ctx.source(denoise, name="denoise",
                   device="cuda", rank=0)
        | ctx.pipe(vae_decode, name="vae",
                   device="cuda", rank=1)
        | ctx.unbatch(rank=1)
        | ctx.pace(fps=24, rank=1)
        | video
    )
    # denoise reads control.get("prompt.text") live, inline
    await pipeline.run()
TypeScript
// Same as single-GPU — the pipeline
// is transparent to the frontend
const session = app.generate()

const video = session.stream("video")
video.on("track", (track) => {
  if (track) videoElement.srcObject = new MediaStream([track])
})

session.doc("control").set({
  prompt: { text: "A forest scene" },
})
TypeScript (React)
// Multi-GPU pipeline is transparent to React too —
// same Session API as the single-GPU case
function VideoPlayer() {
  const app = useApp()
  const session = useMemo(() => app.generate(), [app])
  const video = useMemo(() => session.stream("video"), [session])
  const videoRef = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    return video.on("track", (track) => {
      if (track && videoRef.current) videoRef.current.srcObject = new MediaStream([track])
    })
  }, [video])

  return (
    <>
      <video ref={videoRef} autoPlay muted playsInline />
      <button onClick={() => session.doc("control").set({ prompt: { text: "A forest scene" } })}>
        Generate
      </button>
    </>
  )
}

Cache a model across sessions

Python
@store.cache(prefix="model", gpu_pin=True)
def load_model(device: str):
    """First call loads from disk.
    All subsequent calls return the cached object."""
    return Model.from_pretrained("my-model").to(device)

@app.function(gpus="h100:1")
async def generate(ctx: urun.Context):
    model = load_model(str(ctx.device))
    # model stays in GPU memory between sessions
TypeScript
// No frontend code needed — caching is
// entirely server-side. The browser just
// connects and gets faster warm starts.
TypeScript (React)
// No React code needed — caching is server-side.
// Sessions just start faster on warm workers.
function VideoPlayer() {
  const app = useApp()
  const session = useMemo(() => app.generate(), [app]) // warm start if model is cached
  const video = useMemo(() => session.stream("video"), [session])
  const videoRef = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    return video.on("track", (track) => {
      if (track && videoRef.current) videoRef.current.srcObject = new MediaStream([track])
    })
  }, [video])

  return <video ref={videoRef} autoPlay muted playsInline />
}

Load a model from HuggingFace

Python
from urun import store

# Download, cache, and get the local path (for libraries that need it)
path = store.path("hf://meta-llama/Llama-3-70B")
TypeScript
// Model loading is server-side only.
// The frontend just connects — models are
// already loaded and cached on the GPU worker.
const session = app.generate()
const video = session.stream("video")
video.on("track", (track) => {
  if (track) videoElement.srcObject = new MediaStream([track])
})
TypeScript (React)
// Model loading is server-side only — React just connects
function VideoPlayer() {
  const app = useApp()
  const session = useMemo(() => app.generate(), [app])
  const video = useMemo(() => session.stream("video"), [session])
  const videoRef = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    return video.on("track", (track) => {
      if (track && videoRef.current) videoRef.current.srcObject = new MediaStream([track])
    })
  }, [video])

  return <video ref={videoRef} autoPlay muted playsInline />
}

Send settings from browser to GPU

Python
@app.function(gpus="h100:1")
async def generate(ctx: urun.Context):
    control = ctx.doc("control")
    video = ctx.stream("video", codec="h264", fps=24)

    def produce(*, stop_event, config):
        while not stop_event.is_set():
            # live inline doc reads — the latest merged settings
            yield model.generate(
                quality=control.get("settings.quality", "high"),
                fps=control.get("settings.fps", 24),
                superres=control.get("settings.superres", False),
            )

    pipeline = (
        ctx.source(produce, name="generate", device="cuda")
        | ctx.pace(fps=24)
        | video
    )
    await pipeline.run()
TypeScript
// Mutate settings — CRDT merges automatically
session.doc("control").set({
  settings: {
    quality: "high",
    fps: 30,
    superres: true,
  },
})
TypeScript (React)
function SettingsPanel() {
  const app = useApp()
  const session = app.generate()
  const doc = session.doc("control")

  return (
    <label>
      Super-resolution
      <input
        type="checkbox"
        onChange={(e) =>
          doc.set({ settings: { quality: "high", fps: 30, superres: e.target.checked } })
        }
      />
    </label>
  )
}

Handle keyboard/mouse input (interactive)

Continuous input (held keys, pointer motion at 30–60 Hz) rides awareness presence, not the control doc — presence is ephemeral by design, so a dropped tab can never leave a key "held down". Discrete state (a settings toggle, a placed object) still goes on the doc.

Python
@app.function(gpus="b200:1")
async def play(ctx: urun.Context):
    video = ctx.stream("video", codec="h264", fps=60)

    # ctx.input() samples the browser's awareness input
    # (held keys + pointer deltas) — a pipeline HEAD
    pipeline = (
        ctx.input(hz=60)
        | ctx.pipe(render_scene, name="render", device="cuda")
        | ctx.pace(fps=60)
        | video
    )
    await pipeline.run()
TypeScript
import { createInputPresencePublisher } from "@urun-sh/core"

const session = app.play()

// Publishes held keys + cumulative pointer counters
// into the session's awareness (throttled, default 30 Hz)
const input = createInputPresencePublisher({
  awareness: {
    setLocalStateField: (f, v) => session.presence.setField(f, v),
  },
})

window.addEventListener("keydown", (e) => input.keyDown(e.key))
window.addEventListener("keyup", (e) => input.keyUp(e.key))
window.addEventListener("mousemove", (e) =>
  input.movePointer(e.movementX, e.movementY))
TypeScript (React)
import { useApp, useInputPresence } from "@urun-sh/react"
import { useRef } from "react"

function GameSurface() {
  const app = useApp()
  const session = app.play()
  // Pointer-lock capture + throttled awareness publishing, handled for you
  const { engage, engaged, heldKeys } = useInputPresence(session)
  const surface = useRef<HTMLDivElement>(null)

  return (
    <div ref={surface} onClick={() => surface.current && engage(surface.current)}>
      {engaged ? `keys: ${heldKeys.join(" ")}` : "Click to play"}
    </div>
  )
}

Send voice/mic audio to GPU

Python
@app.function(gpus="h100:1")
async def voice_chat(ctx: urun.Context):
    mic = ctx.stream("microphone")

    # Option 1: consume directly
    async for chunk in mic:
        transcript = transcribe(chunk)

    # Option 2: pipe to OpenAI Realtime
    from openai import AsyncOpenAI
    client = AsyncOpenAI()

    async with client.realtime.connect(model="gpt-4o-realtime") as rt:
        mic.pipe(rt.input_audio)

        async for event in rt:
            if event.type == "response.audio.delta":
                await ctx.stream("audio").emit(event.delta)
            elif event.type == "response.text.done":
                ctx.doc("control").set({"transcript": event.text})
TypeScript
const session = app.voice_chat()
const mic = session.stream("microphone")  // SessionStream (mirrors ctx.stream)

// Send mic up by attaching a local track
const media = await navigator.mediaDevices.getUserMedia({
  audio: {
    channelCount: 1,
    sampleRate: 48000,
    echoCancellation: true,
    noiseSuppression: true,
  },
})

await mic.attach(media.getAudioTracks()[0])

// Stop sending
await mic.detach()
TypeScript (React)
function VoiceChat() {
  const app = useApp()
  const session = useMemo(() => app.voice_chat(), [app])
  const mic = useMemo(() => session.stream("microphone"), [session])
  const [active, setActive] = useState(false)

  async function start() {
    const media = await navigator.mediaDevices.getUserMedia({
      audio: { channelCount: 1, sampleRate: 48000, echoCancellation: true, noiseSuppression: true },
    })
    await mic.attach(media.getAudioTracks()[0])
    setActive(true)
  }

  async function stop() {
    await mic.detach()
    setActive(false)
  }

  return <button onClick={active ? stop : start}>{active ? "Stop" : "Start"} mic</button>
}

Deploy a CPU-only app

Python
app = urun.App("my-service")

@app.function(cpus=1)
async def process(ctx: urun.Context):
    control = ctx.doc("control")
    video = ctx.stream("video", format="raw", fps=10)

    def produce(*, stop_event, config):
        while not stop_event.is_set():
            # live inline doc read
            yield process_request(control.get("request"))

    pipeline = (
        ctx.source(produce, name="process")
        | ctx.pace(fps=10)
        | video
    )
    await pipeline.run()
Deploy
urun deploy app.py

Declare dependencies and credentials

Python
import urun
from urun import App, Dependencies, Credentials, store

app = App("my-app")

DEPS = Dependencies(
    python=["torch>=2.4", "transformers", "diffusers"],
    apt=["ffmpeg", "libopus0"],
)

CREDS = Credentials(
    optional_env=["HF_TOKEN", "OPENAI_API_KEY"],
)

@app.function(gpus="h100:1", deps=DEPS, credentials=CREDS)
async def generate(ctx: urun.Context):
    ...

React API quick reference

The anchor hook is useApp() — sessions, streams, and docs all hang off the Session you get from app.<fn>(). Purpose-built hooks (useRequest, useChat, useSessionTrack, useSessionIdle, …) and media components (Camera, Voice, Audio) are in the hooks reference and Inputs & Outputs.

TypeScript — React
import { UrunProvider, useApp } from "@urun-sh/react"
import { useEffect, useMemo, useRef } from "react"

function App() {
  return (
    <UrunProvider
      baseUrl="https://urun.sh"
      orgId="your-org-id"
      appId="my-app"
      authProvider="workos"
    >
      <VideoPlayer />
    </UrunProvider>
  )
}

function VideoPlayer() {
  const app = useApp()
  const session = useMemo(() => app.generate(), [app])      // invoke the deployed @app.function
  const video = useMemo(() => session.stream("video"), [session]) // SessionStream
  const ref = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    // Read the inbound track:
    return video.on("track", (track) => {
      if (track && ref.current) ref.current.srcObject = new MediaStream([track])
    })
    // Or read the latest synchronously: video.track
  }, [video])

  // session.doc("control").set({ prompt: { text: "..." } }) → ctx.doc("control")
  // video.attachVideo(localTrack) / video.detachVideo()     → push input up

  return <video ref={ref} autoPlay muted playsInline />
}

UrunProvider handles auth internally — it acquires a token from the auth provider (WorkOS by default) and uses the orgId to resolve the correct JWKS endpoint. No manual token wiring needed. For customer-JWT auth, bring your own token validated against the JWKS you registered with urun auth trust-jwk. See the TypeScript SDK reference for the full hook + SessionStream API.

On this page