◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/AI Models
◉ AI Models/2026-04-22Advanced

Rork × LiveKit: Production Voice Agent Infrastructure for AI-Powered Apps

Field notes on wiring LiveKit Agents into a Rork-generated React Native app: the agent server and token API, recovering from incoming calls and expired tokens, metered subscription billing, unit economics, and the monitoring queries that catch trouble before your users do.

Rork585LiveKitVoice AI AgentReact Native239WebRTC4Production10

✦ Premium Article

"I want a voice-first AI assistant baked into my app." I've been getting this question almost weekly since OpenAI opened up the Realtime API and voice-first platforms like Vapi and Retell dropped their entry price. Getting a demo to talk back is easy. Shipping something that survives a subway tunnel and a monthly billing cycle is an entirely different game.

My first voice chat app crashed every time a user rode under a bridge. Users don't type an angry review in that moment — they just leave. Reviewers who stuck around gave me one-star reviews with the subject "doesn't work on the train." The painful truth is that voice AI is one of those categories where anything above one second of latency makes users stop speaking, and a session that takes a while to come back almost never comes back at all.

This guide is for anyone stuck at that "demo works, production falls over" stage. We'll walk through the entire blueprint of a voice agent app using LiveKit Agents as the backend and a Rork-generated React Native client. Why LiveKit? Because writing your own WebRTC stack means you'll eventually face SRTP retransmission bugs and jitter-buffer issues that will eat weeks of your life. LiveKit Agents bundles the SFU and a Python/Node agent runtime together, and that one decision removes an entire category of production incidents from your roadmap.

Chapter 1: Designing the Overall Architecture

Before typing a single line, separate the system into four layers. If you start with "just make it work," you'll end up rewriting the whole thing when you add billing or logging later. I've done that three times already.

Four Responsibilities

  • Mobile client (Rork-generated React Native): microphone and speaker permissions, joining a LiveKit Room, UI state, emitting a call-end log
  • Token API (Hono on Cloudflare Workers): verifies the Supabase session, checks remaining call time against the plan, and issues a LiveKit JWT
  • LiveKit Agent (Python or Node.js): a long-running worker that joins rooms and runs the Whisper → GPT/Claude → TTS pipeline
  • Billing and log store (Stripe + Supabase Postgres): the source of truth for monthly quotas, call history, and error events

Why Four Layers Beat Two

Your first instinct might be to open a WebSocket from the app directly to OpenAI's Realtime API. I tried that. It fails on three fronts.

  1. API key exposure: the OpenAI key ends up in the binary. A weekend of reverse-engineering and your bill explodes.
  2. No enforceable quota: when the client talks to the provider directly, you can't stop a free user at 10 minutes from the server side.
  3. No observability: quality, error rates, user behavior — all live on the client. You can't improve what you can't see.

With LiveKit in the middle, keys stay on your server, quota decisions happen at token issuance, and per-room metrics come for free.

Directory Layout

The shape I've been running in production:

my-voice-app/
├── app/                    # Expo Router screens generated by Rork
│   ├── (tabs)/
│   │   └── voice.tsx       # the call screen
│   └── _layout.tsx
├── lib/
│   ├── livekit.ts          # LiveKit client wrapper
│   ├── billing.ts          # quota check
│   └── api.ts              # token API client
├── server/                 # backend (can live in a separate repo)
│   ├── token-api/          # Hono on Cloudflare Workers
│   └── agent/              # LiveKit Agent (Python)
└── app.config.ts

Chapter 2: Building the LiveKit Agent

Always build the backend first. If you start with the client, you'll end up with pretty UI that connects to nothing and a full day lost.

Environment Variables

Create a project on LiveKit Cloud to get three values:

  • LIVEKIT_URL: wss://your-project.livekit.cloud
  • LIVEKIT_API_KEY: APIxxxxxxxxxxxxxxxx
  • LIVEKIT_API_SECRET: the secret

You'll also need an AI provider key. I'll use OpenAI here.

# .env.agent
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=APIxxxxxxxxxxxxxxxx
LIVEKIT_API_SECRET=your-secret
OPENAI_API_KEY=YOUR_OPENAI_API_KEY

The Agent Itself (Python)

LiveKit's Python Agents SDK compresses the VAD → STT → LLM → TTS pipeline into about thirty lines. Here's a minimal friendly assistant.

# server/agent/main.py
import asyncio
import logging
from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli, llm
from livekit.agents.voice_assistant import VoiceAssistant
from livekit.plugins import openai, silero
 
logger = logging.getLogger("voice-agent")
 
async def entrypoint(ctx: JobContext):
    # Initial prompt: defines personality and hard limits.
    initial_ctx = llm.ChatContext().append(
        role="system",
        text=(
            "You are a friendly assistant. Keep responses to 1-2 sentences, "
            "with natural pauses. Do not give medical, legal, or investment "
            "advice. Suggest consulting a professional instead."
        ),
    )
 
    # Subscribe only to audio tracks - skipping video saves bandwidth and cost.
    await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
 
    assistant = VoiceAssistant(
        vad=silero.VAD.load(),
        stt=openai.STT(model="whisper-1"),
        llm=openai.LLM(model="gpt-4o-mini"),
        tts=openai.TTS(voice="nova"),
        chat_ctx=initial_ctx,
    )
    assistant.start(ctx.room)
 
    # Greeting on connection
    await asyncio.sleep(0.5)
    await assistant.say("Hi there! How can I help you today?", allow_interruptions=True)
 
if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))

Run it and the worker stays resident on LiveKit Cloud, spinning up on every incoming room join. Expected log:

voice-agent - INFO - worker started, id=AW_xxxxxxx
voice-agent - INFO - accepting job, room=voice-user-123
voice-agent - INFO - agent connected to room

Why the VoiceAssistant Class Earns Its Keep

Wiring STT → LLM → TTS by hand means you also have to write "don't fire the LLM while the user is still speaking" and "stop TTS if the user interrupts." VoiceAssistant handles all that, and with allow_interruptions=True the back-and-forth feels natural.

My first attempt wired LangChain agents straight through and I couldn't understand why the bot refused to respond until I finished speaking for two full seconds. Switching to VoiceAssistant made that symptom vanish in one line.

Where to Deploy

Agents are long-lived processes, so Cloudflare Workers is out. Three options I trust:

  • Fly.io: Python containers, $5/month to start. My default for solo projects.
  • LiveKit Cloud Agents (paid add-on): no ops, but pricey.
  • Railway / Render: comparable to Fly.io in DX.

I run Fly.io with fly launch --dockerfile and fly scale count 2 for redundancy. One agent container handles 10–20 concurrent calls, so scaling is mostly "add more machines when MRR grows."

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Turn a voice agent that only works in demos into one that survives incoming calls, backgrounding, and expired tokens
✦Get the full three-layer stack — LiveKit Agents server, Rork-generated client, and billing and permission wiring — as code you can paste and run
✦Learn how to meter call minutes against subscription tiers, read your per-minute cost, and monitor the three signals that tell you something broke
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◉ AI Models2026-08-07
Cutting MCP Tools Didn't Make Anything Lighter — 2,377 Bytes of Definitions vs 110,298 Bytes of Response
I wrote a minimal MCP server for a wallpaper catalog and measured the byte cost of tool definitions against the byte cost of responses. Here is which side actually matters, and the real reason to merge tools.
◉ AI Models2026-10-14
Take the Prompt Out of the App: An Edge Function Gateway With the Guard on the Output Side
A system prompt shipped inside your app is readable from the day it ships. This walkthrough moves the prompt into a Supabase Edge Function, lets the client name only a task, and puts the guard on an output allowlist, with tested code.
◉ AI Models2026-10-05
Sonnet 5.5, GPT-6.1 Sol, or Opus 5.5? I Decide by the Kind of Fix Before I Pick the Model. A Week of Logging Where My Rork Credits Went
After Rork's model menu settled into Sonnet 5.5, GPT-6.1 Sol, and Opus 5.5, I sorted my fixes into three kinds and assigned a model and effort level to each. Here is how I read the changelog, the tiny ledger that tracks where each credit goes, and the rule I drew from a week of entries.