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:
- Deploy a Python function to a GPU.
- 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
-
Install the CLI.
terminal uv tool install urun-cli -
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.
-
Log in to the CLI with your API key.
terminal urun login --api-key urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uV
Deploy a GPU function
-
Declare a function with the
@app.functiondecorator. 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
Contextthat 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 tomy_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. - the Session id -
-
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
-
Retrieve your org ID. In the console, go to Settings → Organization and make note of your Organization ID.
-
Create a frontend that references the deployed App and invokes the function by name. Set
orgIdto 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
Sessionis invoked. -
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
videostream, 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() }, }) }) -
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 withcontrol.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() }, }) })
-
Retrieve your org ID. In the console, go to Settings → Organization and make note of your Organization ID.
-
Create a frontend that references the deployed App and invokes the function by name. Set
orgIdto 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
Sessionis invoked. -
Stream inputs and outputs. For convenience,
@urun-sh/reactships 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> ) } -
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) | Role | Python (GPU runtime) |
|---|---|---|
App / app.function_name() | Declare + own the Session | App / @app.function def function_name |
| (resolved server-side) | GPU shape (deploy-time config) | @app.function(gpus=...) |
session.stream("video") | Emit / receive media | ctx.stream("video") |
session.doc("control") | Read live control state | ctx.doc("control") |
| (transparent to frontend) | Multi-GPU coordination | ctx.rank / ctx.world_size |
Next steps
To learn more about working with uRun's SDKs, explore the following docs: