docs

Get started - deploy a custom App

Go from zero to a deployed GPU function and a live steerable stream in the browser

This page walks through the shape of a custom App on uRun. Example use cases for custom Apps include but are not limited to the following:

  • Live video annotation that re-steers the instant you change the prompt.
  • Virtual try-on that feels like looking in a mirror.
  • Interactive voice experiences where an avatar responds in real time to new user inputs.
  • Real-time speech translation.
  • Natural voice-driven interaction for hands-busy or accessibility-sensitive contexts.
  • Game worlds that generate as users move through them.

Want an off-the-shelf model?

uRun offers a catalog of popular models such as GLM-5.2, Qwen Coder, and Whisper Large V3. To open a Session with one of these models without coding a custom App, serve a templated App.

To create a custom App, you will do the following:

  1. Deploy a Python function to a GPU.
  2. Create a TypeScript frontend that calls the function by name from the browser.

When your function is called, you get a live Session that stays warm for the life of the interaction. Media flows both ways over QUIC/WebTransport (WebRTC fallback), and a synced control document carries the prompt.

New to the Session model?

Read Why uRun first. It lays out what a Session is and what it unlocks.

Prerequisites

  • Python 3.11 or newer.
  • uv package manager.
  • A uRun user account with Owner or Admin permissions so you can make an API key. Or, an API key previously generated by another member of your uRun organization.

Prepare the CLI

  1. Install the CLI.

    terminal
    uv tool install urun-cli
  2. Create an API key or get one from an organization Owner or Admin. To create a new API key, do the following:

    • In the console, go to Settings → API Keys and click + Create API key.
    • Enter a Label for the key that will help you identify where it is used later.
    • Select Create key.
    • Copy the generated secret and store it somewhere safe. It is not retrievable later.
  3. Log in to the CLI with your API key.

    terminal
    urun login --api-key urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uV

Deploy a GPU function

  1. Declare a function with the @app.function decorator. The GPU shape and dependencies you set here are fixed at deploy time. You can use credentials to pass secrets to your remote GPU functions securely.

    app.py
    from urun import App, Context
    from urun.core import Credentials, Dependencies
    
    app = App("my-app")
    
    @app.function(
        gpus="b200:1",
        deps=Dependencies(python=["torch", "diffusers"]),
        credentials=Credentials(optional_env=["HF_TOKEN"]),
    )
    def my_function(ctx: Context):
        # This function is the long-lived Session owner.
        # It holds the model warm and runs for the life of the connection.
        video = ctx.stream("video")
        control = ctx.doc("control")
        prompt = control.get("prompt.text", "initial prompt")
    
        # Custom business logic for your use case goes here.
        # For example, read inbound camera frames and the latest control state,
        # then restyle and emit restyled frames back out through `video`.

    The function receives a Context that carries the following, among other things:

    • the Session id - ctx.session_id
    • bidirectional media stream - ctx.stream(...)
    • the current Session's CRDT-synced control document- ctx.doc(...)

    Argument purity

    You can't change the GPU configuration at call time. The GPU specification on the decorator, for example gpus="b200:1", is resolved by the control plane when a Session is allocated. The args a client later passes to my_function(...) are delivered to your function verbatim. A Session invocation can never reconfigure the hardware a function runs on. See runtime configuration boundary for more information.

  2. Deploy your function with the CLI:

    Terminal
    urun deploy app.py

Connect from the browser

Use the uRun TypeScript SDK to create a frontend that calls your function.

  • For a framework-agnostic frontend, use @urun-sh/core
  • For a React frontend, use @urun-sh/react
  1. Retrieve your org ID. In the console, go to Settings → Organization and make note of your Organization ID.

  2. Create a frontend that references the deployed App and invokes the function by name. Set orgId to the value you noted in the previous step.

    TypeScript
    import { App } from '@urun-sh/core'
    
    const app = App('my-app', { 
      baseUrl: 'https://urun.sh', 
      orgId: 'your-org-id', 
      authProvider: 'workos', 
    }) 
    
    const session = app.my_function() 
    
    // Optional - log the Session lifecycle
    session.onPhase((p) => console.log(p.name)) // idle → … → live
    
    const camera = await navigator.mediaDevices.getUserMedia({ video: true })
    const video = session.stream('video')
    await video.attachVideo(camera.getVideoTracks()[0]) // push the webcam up (use attach() for mic/audio)
    video.on('track', (track) => {                      // restyled frames come back down
      if (track) videoEl.srcObject = new MediaStream([track])
    })
    
    const control = session.doc('control')
    
    promptInput.addEventListener('change', (e) => {
      control.set({
        prompt: { text: (e.target as HTMLInputElement).value, revision: Date.now() },
      })
    })

    When the frontend calls the function, a Session is invoked.

  3. Write custom business logic that streams inputs and outputs. The same stream name connects both ends. For example, to restyle a user's webcam, attach the local camera track to the video stream, and restyled frames will arrive on the same handle.

    TypeScript
    import { App } from '@urun-sh/core'
    
    const app = App('my-app', {
      baseUrl: 'https://urun.sh',
      orgId: 'your-org-id',
      authProvider: 'workos',
    })
    
    const session = app.my_function()
    
    // Optional - log the Session lifecycle
    session.onPhase((p) => console.log(p.name)) // idle → … → live
    
    const camera = await navigator.mediaDevices.getUserMedia({ video: true }) 
    const video = session.stream('video') 
    await video.attachVideo(camera.getVideoTracks()[0]) // push the webcam up (use attach() for mic/audio)
    video.on('track', (track) => {                      // restyled frames come back down
      if (track) videoEl.srcObject = new MediaStream([track]) 
    }) 
    
    const control = session.doc('control')
    
    promptInput.addEventListener('change', (e) => {
      control.set({
        prompt: { text: (e.target as HTMLInputElement).value, revision: Date.now() },
      })
    })
  4. Steer the live Session with the control doc. A write to the prompt deep-merges and syncs to the GPU runtime, which picks up the change on its next frame without a reconnect or new Session. The browser and the runtime agree on a single control-doc shape. In this example, it is { prompt: { text } }. The Python side reads it live inside the pipeline with control.get("prompt.text") at the chunk boundary. Use the same doc paths on both sides, or the runtime silently reads nothing.

    TypeScript
    import { App } from '@urun-sh/core'
    
    const app = App('my-app', {
      baseUrl: 'https://urun.sh',
      orgId: 'your-org-id',
      authProvider: 'workos',
    })
    
    const session = app.my_function()
    
    // Optional - log the Session lifecycle
    session.onPhase((p) => console.log(p.name)) // idle → … → live
    
    const camera = await navigator.mediaDevices.getUserMedia({ video: true })
    const video = session.stream('video')
    await video.attachVideo(camera.getVideoTracks()[0]) // push the webcam up (use attach() for mic/audio)
    video.on('track', (track) => {                      // restyled frames come back down
      if (track) videoEl.srcObject = new MediaStream([track])
    })
    
    const control = session.doc('control') 
    
    promptInput.addEventListener('change', (e) => { 
      control.set({ 
        prompt: { text: (e.target as HTMLInputElement).value, revision: Date.now() }, 
      }) 
    }) 
  1. Retrieve your org ID. In the console, go to Settings → Organization and make note of your Organization ID.

  2. Create a frontend that references the deployed App and invokes the function by name. Set orgId to the value you noted in the previous step.

    React — @urun-sh/react
    import '@urun-sh/react/styles.css'
    import { UrunProvider, useApp, Session, Camera, Video } from '@urun-sh/react'
    import { UrunWorkOSProvider } from '@urun-sh/react/workos'
    
    function MyReactFunction() {
      const app = useApp()
      const session = app.my_function({ prompt: 'A sunset' })
      return (
        <Session session={session}>
          <Camera stream="video" />  {/* webcam up */}
          <Video stream="video" />  {/* restyled frames back down, same stream name */}
          <input
            placeholder="A neon cyberpunk street"
            onChange={(e) => session.doc('control').set({ prompt: { text: e.target.value } })}
          />
        </Session>
      )
    }
    
    export default function App() {
      return (
        <UrunWorkOSProvider clientId="client_...">
          <UrunProvider baseUrl="https://urun.sh" orgId="your-org-id" authProvider="workos" appId="my-app">
            <MyReactFunction />
          </UrunProvider>
        </UrunWorkOSProvider>
      )
    }

    When the frontend calls the function, a Session is invoked.

  3. Stream inputs and outputs. For convenience, @urun-sh/react ships media components over the Session's named streams. For example, to restyle a user's webcam, <Camera> publishes input from the webcam and <Video> renders the restyled output.

    React — @urun-sh/react
    import '@urun-sh/react/styles.css'
    import { UrunProvider, useApp, Session, Camera, Video } from '@urun-sh/react'
    import { UrunWorkOSProvider } from '@urun-sh/react/workos'
    
    function MyReactFunction() {
      const app = useApp()
      const session = app.my_function({ prompt: 'A sunset' })
      return (
        <Session session={session}>
          <Camera stream="video" />  {/* webcam up */}
          <Video stream="video" />  {/* restyled frames back down, same stream name */}
          <input placeholder="A neon cyberpunk street"
          onChange={(e) => session.doc('control').set({ prompt: { text: e.target.value } })}
          />
        </Session>
      )
    }
    
    export default function App() {
      return (
        <UrunWorkOSProvider clientId="client_...">
          <UrunProvider
            baseUrl="https://urun.sh"
            orgId="your-org-id"
            authProvider="workos"
            appId="my-app"
          >
            <MyReactFunction />
          </UrunProvider>
        </UrunWorkOSProvider>
      )
    }
  4. Use a doc-bound input to steer the live Session with the control doc. A write to the prompt deep-merges and syncs to the GPU runtime, which picks up the change on its next frame without a reconnect or new Session. The Python side reads the prompt live inside the pipeline with control.get("prompt.text") at the chunk boundary. Use the same doc paths on both sides, or the runtime silently reads nothing.

    React — @urun-sh/react
    import '@urun-sh/react/styles.css'
    import { UrunProvider, useApp, Session, Camera, Video } from '@urun-sh/react'
    import { UrunWorkOSProvider } from '@urun-sh/react/workos'
    
    function MyReactFunction() {
      const app = useApp()
      const session = app.my_function({ prompt: 'A sunset' })
      return (
        <Session session={session}>
          <Camera stream="video" />  {/* webcam up */}
          <Video stream="video" />  {/* restyled frames back down, same stream name */}
          <input placeholder="A neon cyberpunk street"
          onChange={(e) => session.doc('control').set({ prompt: { text: e.target.value } })}
          />
        </Session>
      )
    }
    
    export default function App() {
      return (
        <UrunWorkOSProvider clientId="client_...">
          <UrunProvider
            baseUrl="https://urun.sh"
            orgId="your-org-id"
            authProvider="workos"
            appId="my-app"
          >
            <MyReactFunction />
          </UrunProvider>
        </UrunWorkOSProvider>
      )
    }

Data flow

A Session is a live bridge. When a browser invokes a function, the uRun control plane resolves the GPU shape from the deployed config, admits the request through the queue, provisions or warms the GPU, and establishes the media transport. Your code never touches scheduling or signaling. The Session lifecycle moves through idle → queued → provisioning → connecting → live. Media and steering state flow symmetrically between browser and runtime for as long as they're connected.

Concept map

To understand how concepts map between the TypeScript SDK and the Python SDK, refer to the following table:

TypeScript (browser)RolePython (GPU runtime)
App / app.function_name()Declare + own the SessionApp / @app.function def function_name
(resolved server-side)GPU shape (deploy-time config)@app.function(gpus=...)
session.stream("video")Emit / receive mediactx.stream("video")
session.doc("control")Read live control statectx.doc("control")
(transparent to frontend)Multi-GPU coordinationctx.rank / ctx.world_size

Next steps

To learn more about working with uRun's SDKs, explore the following docs:

On this page