RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
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.

Rork548LiveKitVoice AI AgentReact Native234WebRTC4Production10

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 $15 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-06-19
Before You Pay $200/mo for Rork Max, Map How Far Expo Reaches in Three Tiers
Wanting widgets or Live Activities makes Rork Max tempting, but most of those features are reachable from the Expo setup that standard Rork generates. Here is how I sort each Apple-native feature into three tiers—reachable in Expo, reachable with a custom module, or where Max is the pragmatic answer—and verify which tier my app is in before paying.
AI Models2026-06-14
Calling Apple Foundation Models from a Rork (Expo) App: Bridging On-Device AI Through a Native Module
Rork generates Expo (React Native) apps, but Apple Foundation Models ships as a Swift framework you can't touch from JavaScript. Here's how to write an Expo Modules API bridge, gate it by availability, and fall back to the cloud on unsupported devices.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →