Communications · Convex component
convex-livekit
Sync LiveKit rooms, participants, tracks, egress, and ingress into Convex reactively, and manage them directly from Convex functions.
npm install convex-livekitDownloads
Download history
Build this
Build with convex-livekit
The install and setup steps from the package documentation.
- 1
Install
shnpm install convex-livekit - 2
Quick Start
1. Add the component
ts// convex/convex.config.ts import { defineApp } from "convex/server"; import convexLivekit from "convex-livekit/convex.config"; const app = defineApp(); app.use(convexLivekit); export default app;2. Set environment variables
shnpx convex env set LIVEKIT_API_KEY APIxxxxxxxx npx convex env set LIVEKIT_API_SECRET your-api-secret npx convex env set LIVEKIT_HOST https://your-project.livekit.cloudThese come from your LiveKit Cloud project settings (or your self-hosted server's configured key/secret pair).
3. Mount the webhook handler
ts// convex/http.ts import { httpRouter } from "convex/server"; import { components } from "./_generated/api"; import { LiveKit } from "convex-livekit"; const livekit = new LiveKit(components.convexLivekit, { apiKey: process.env.LIVEKIT_API_KEY!, apiSecret: process.env.LIVEKIT_API_SECRET!, host: process.env.LIVEKIT_HOST!, }); const http = httpRouter(); http.route({ path: "/webhooks/livekit", method: "POST", handler: livekit.webhookHandler, }); export default http;4. Register the webhook in your LiveKit project
In your LiveKit Cloud project settings (or your self-hosted server's
webhookconfig), set the webhook URL tohttps://<your-deployment>.convex.site/webhooks/livekit. LiveKit signs every webhook with your project's own API key/secret pair — there's no separate webhook secret to configure.5. Initialize the client
ts// convex/example.ts import { action, query } from "./_generated/server"; import { components } from "./_generated/api"; import { LiveKit } from "convex-livekit"; import { v } from "convex/values"; const livekit = new LiveKit(components.convexLivekit, { apiKey: process.env.LIVEKIT_API_KEY!, apiSecret: process.env.LIVEKIT_API_SECRET!, host: process.env.LIVEKIT_HOST!, }); export const listRooms = query({ args: {}, handler: async (ctx) => { return await livekit.listRooms(ctx, {}); }, }); - 3
Setup
The component needs no schema changes in your app — its tables (
rooms,participants,tracks,egress,webhookEvents) live entirely inside the component's own isolated schema. All you need is the webhook mounted (step 3) and aLiveKitclient instance wherever you call its methods.Unlike the other components in this series,
convex-livekitnever stores a long-lived credential in a header — every server API call and every room-join token is a fresh, short-lived JWT this component signs itself with your API key/secret, following the same convention as LiveKit's own server SDKs. - 4
Usage
Create a room
tsexport const openRoom = action({ args: { name: v.string(), maxParticipants: v.optional(v.number()) }, handler: async (ctx, args) => { return await livekit.createRoom(ctx, args); }, });Returns
{ sid, name }and immediately records the room in Convex — you don't have to wait for theroom_startedwebhook to see it in a query.Mint a join token for a client
tsexport const getJoinToken = action({ args: { roomName: v.string(), identity: v.string() }, handler: async (ctx, args) => { return await livekit.createRoomToken(args); }, });createRoomTokentouches no database — it's a plain signing operation, so it also works from a query if you'd rather not spend an action round-trip. The returned{ token }is what you pass to a LiveKit client SDK (room.connect(url, token)).Remove a participant
tsexport const kick = action({ args: { roomName: v.string(), identity: v.string() }, handler: async (ctx, args) => { await livekit.removeParticipant(ctx, args); return null; }, });Update a participant, or mute their track
tsexport const setAgentState = action({ args: { roomName: v.string(), identity: v.string(), state: v.string() }, handler: async (ctx, args) => { await livekit.updateParticipant(ctx, { roomName: args.roomName, identity: args.identity, attributes: { "lk.agent.state": args.state }, }); return null; }, }); export const muteMic = action({ args: { roomName: v.string(), identity: v.string(), trackSid: v.string() }, handler: async (ctx, args) => { await livekit.mutePublishedTrack(ctx, { ...args, muted: true }); return null; }, });Both call LiveKit's server API first, then patch the corresponding Convex row so the change is visible in queries immediately — no round trip through a webhook required.
Record or stream a room (egress)
tsexport const startRecording = action({ args: { roomName: v.string() }, handler: async (ctx, args) => { return await livekit.startRoomCompositeEgress(ctx, { roomName: args.roomName, filepath: `recordings/${args.roomName}-{time}.mp4`, }); }, }); export const stopRecording = action({ args: { egressId: v.string() }, handler: async (ctx, args) => { return await livekit.stopEgress(ctx, args); }, });startRoomCompositeEgressrecords or livestreams the whole room (mixed audio/video of every participant) to a file, one or more RTMP(S) URLs, or both. It uses LiveKit'sStartRoomCompositeEgressRPC — the file/stream actually lands wherever your LiveKit server's own storage config (S3/GCP/Azure/local) sends it. Both calls patch theegressrow immediately, same pattern as the room/participant/track actions above.Bring an external stream into a room (ingress)
tsexport const createStreamKey = action({ args: { roomName: v.string(), identity: v.string() }, handler: async (ctx, args) => { return await livekit.createIngress(ctx, { inputType: "rtmp", name: `${args.roomName}-obs`, roomName: args.roomName, participantIdentity: args.identity, participantName: args.identity, }); }, });Returns
{ ingressId, url, streamKey }— handurl/streamKeyto OBS or any RTMP encoder, and it joins the room as a regular participant.updateIngresschanges a reusable (RTMP/WHIP) ingress's target room or identity;deleteIngressremoves it permanently (unlike the other tables, the row is actually deleted, not kept as history — see ).Read rooms, participants, tracks, egress, and ingress reactively
tsxconst rooms = useQuery(api.example.listRooms, {}); const participants = useQuery(api.example.listParticipantsByRoom, { roomName: "standup", }); const tracks = useQuery(api.example.listTracksByRoom, { roomName: "standup" }); const ingress = useQuery(api.example.listIngressByRoom, { roomName: "standup" });Every
room_started/room_finished,participant_joined/participant_left/participant_connection_aborted,track_published/track_unpublished,egress_started/egress_updated/egress_ended, andingress_started/ingress_endedwebhook event patches or inserts a row, so these queries re-render live — no polling.