Realtime
Audio, video, screen share, and participant state go through LiveKit. Light collaboration (chat, raise hand, reactions) uses a LiveKit data channel.
Token flow
text
POST /api/livekit/token
↓
Same-origin check + rate limit
↓
Authorize actor (member / guest cookie)
↓
Schedule + visibility + capacity
↓
Mint short-lived JWT (TTL capped by endTime)
↓
Return token + LIVEKIT_URL to clientIdentities are set server-side: user:{uuid} or guest:{roomId}:{guestId}. Clients cannot pick their own identity in the request body.
Long meetings soft-refresh the token before JWT expiry (about 90s early) so the connection can continue without a full remount when possible.
In the meeting UI
- Camera and microphone toggles
- Screen sharing
- Participant grid / screen-share stage
- Speaking indicators from LiveKit
- Chat, raise hand, emoji reactions (data channel)
- Connection banner for reconnect / disconnect
- Host/moderator mute, disable camera, remove
Why not Socket.IO?
Meeting-scoped signals already ride LiveKit’s data channel. Adding a second realtime bus would mean another service to run and secure. Application events that need durability (orders, memberships) stay in Postgres and Server Actions.