LiveKit Platform Service¶
- Version: 1.0.0
- Runtime: Node.js / Express
- Database: PostgreSQL (Sequelize ORM)
- Last Updated: April 2026
- Maintained by: Bayer LiveKit Platform Team ⧉
- Audience: Bayer internal developers — agent and client application teams
- Status: Active
Table of Contents¶
- Overview
- The Problem
- The Solution
- System Architecture
- API Reference
- Authentication & Authorization
- Related Resources
Overview¶
The LiveKit Platform Service (LKPS) is a trusted backend that authenticates callers via Microsoft Entra ID and uses the livekit-server-sdk to issue both participant tokens (for clients) and agent tokens (for AI agents) — so that neither side ever handles LiveKit credentials directly.
The service:
- Validates agent identities — accepts Entra-authenticated requests from agents and frontend clients.
- Issues scoped LiveKit JWT tokens — signed with credentials that never leave the platform.
- Creates LiveKit rooms with agent dispatch — rooms are pre-configured to automatically dispatch the correct AI agent.
- Tracks session lifecycle — processes LiveKit webhooks to record room creation, participant joins/leaves, and session completion in PostgreSQL.
- Audits every registration — persists a database record for each agent JWT issued, including who requested it, when it expires, and revocation status.
Key Principle
Your agents and clients never hold LiveKit credentials. They authenticate through the LiveKit Platform Service, receive a time-limited token, and connect.
The Problem¶
Standard LiveKit deployments distribute raw API credentials to every developer and service:
# Every developer has these — full admin access
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=devsecret
These credentials grant unrestricted access to the LiveKit server. In a shared enterprise platform, this creates serious security and operational risks:
| Problem | Impact |
|---|---|
| Unlimited access scope | Any holder can create/delete rooms, kick participants, access any session |
| Static secrets never expire | A leaked credential remains valid until manually rotated across all consumers |
| No audit trail | Impossible to trace which agent or user performed an action |
| No per-identity revocation | Rotating a shared secret impacts every consumer simultaneously |
| Compliance failure | Violates principle of least privilege required by enterprise security standards |
The Solution¶
LKPS replaces raw credential distribution with identity-based, service-mediated authentication:
| Without LKPS | With LKPS | |
|---|---|---|
| Developer holds | LIVEKIT_API_KEY + LIVEKIT_API_SECRET (full admin) |
Entra identity (scoped, revocable) |
| LiveKit JWT issued by | Developer's code | LKPS (secrets never leave the platform) |
| Access scope | Unlimited — any operation | Time-bound, role-checked |
| Audit trail | None | Every registration logged + persisted in DB |
| Revocability | Rotate shared secret for everyone | Remove Entra app registration → instant block |
System Architecture¶
What this means in practice:
- You never hold LiveKit credentials. The platform holds them. You hold an Entra identity.
- Every connection is time-bound. Tokens expire (default 1 hour). The SDK renews them automatically.
- Access is scoped per registration. Your token grants only what your role permits.
- Every registration is audited. Who registered, when, with what identity — all logged.
- Revocation is instant. Remove the Entra app role → the agent can no longer register. No secret rotation required.
Session Start Flow¶
sequenceDiagram
participant Client as Client App
participant Entra as Entra ID
participant Ocelot as Ocelot
participant LKPS as LKPS
participant LK as LiveKit Server
participant Agent as AI Agent
Client->>Entra: 1. Acquire access token (MSAL)
Entra-->>Client: Entra JWT
Client->>Ocelot: 2. POST /api/session/start (Bearer token)
Note over Ocelot: 3. Validate JWT
Note over Ocelot: 4. Inject identity headers
Ocelot->>LKPS: Forward with validated headers
Note over LKPS: 5. Create room
Note over LKPS: 6. Issue participant token<br/>with RoomAgentDispatch
Note over LKPS: 7. Audit log
LKPS-->>Client: { token, room, livekit_url }
Client->>LK: 8. Connect via WebRTC (scoped JWT)
LK->>Agent: 9. Dispatch agent job
Agent->>LK: 10. Join room via WebRTC
Note over Client, Agent: 11. Real-time interaction<br/>Voice · Video · Data via LiveKit SFU
LK-->>LKPS: 12. Webhooks (room_created, participant_joined, room_finished)
API Reference¶
LKPS exposes four endpoints. All /api/* routes sit behind Ocelot, which validates the Entra JWT and injects identity headers before the request reaches LKPS.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
/api/health |
Service health check | Ocelot (Entra JWT) |
POST |
/api/agent/register |
Agent registration — returns a LiveKit token | Ocelot (Entra JWT) |
POST |
/api/session/start |
Session creation — returns a room + participant token | Ocelot (Entra JWT) + optional client-to-agent authz |
POST |
/livekit/webhook |
LiveKit server event processing | HMAC-SHA256 (LiveKit API secret) |
GET /api/health¶
Combined liveness + readiness probe. Checks database connectivity and LiveKit server connection.
Response 200 OK
{
"status": "healthy",
"timestamp": "2026-03-10T10:45:13.607Z",
"version": "1.0.0"
}
Response 503 Service Unavailable
{
"status": "unhealthy",
"reason": "database",
"timestamp": "2026-03-10T10:45:13.607Z"
}
| Reason | Trigger |
|---|---|
database |
sequelize.authenticate() fails |
livekit |
Bayer LiveKit Infra connection is down |
Note
Health check requests are logged at DEBUG level to reduce noise from Kubernetes probes.
POST /api/agent/register¶
Registers an AI agent and returns a scoped LiveKit JWT token. The agent's identity is extracted from Ocelot-injected headers (client-id, user-id, x-bayer-user).
Note
This endpoint is called automatically by the Bayer LiveKit SDK. You do not call it directly if you are using the SDK.
Request Body (all fields optional)
{
"service_config": {
"enforce_client_authz": true
}
}
| Field | Type | Default | Description |
|---|---|---|---|
service_config.enforce_client_authz |
boolean |
true |
When true, clients calling /api/session/start for this agent must pass a Microsoft Graph API authorization check |
Response 200 OK
{
"livekit_token": "eyJhbGciOiJIUzI1NiIs...",
"livekit_url": "wss://eu.livekit-np.int.bayer.com",
"expires_in": 3600
}
| Field | Type | Description |
|---|---|---|
livekit_token |
string |
Signed LiveKit JWT with agent grants (agent, canPublish, canSubscribe, canPublishData) |
livekit_url |
string |
WebSocket URL to connect to Bayer LiveKit Infra |
expires_in |
number |
Token lifetime in seconds (default: 3600) |
How the token is built:
- Extract
entra_app_idfrom theuser-idorclient-idheader - Generate a unique worker identity:
agent-{entraAppId}-{uuid} - Sign a LiveKit
AccessTokenwith agent-scoped grants - Persist an audit record in
agent_registrations(non-blocking — the token is returned even if the DB write fails)
Error Responses
| Status | Error | Cause |
|---|---|---|
400 |
Bad Request | enforce_client_authz is not a boolean, or required identity headers (user-id / client-id) are missing |
500 |
Internal Server Error | JWT signing failure or unhandled exception |
Note
The database audit write is non-blocking by design. If the database is temporarily unreachable, the agent still receives its token and can connect to LiveKit.
POST /api/session/start¶
Creates a LiveKit room pre-configured to dispatch a specific AI agent, and returns a participant token for the calling client. The token includes a RoomAgentDispatch configuration — when the client connects, the LiveKit server automatically dispatches the named agent into the room.
Request Body
{
"agent_entra_app_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metadata": {
"language": "en",
"department": "sales"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
agent_entra_app_id |
string (UUID) |
Yes | The Entra App Registration ID of the target AI agent |
metadata |
object |
No | Custom key-value pairs forwarded to the agent as job metadata. Max 50 keys, 10 KB total |
Response 200 OK
{
"room_name": "jdoe1-a1b2c3d4-1705312200-4f3a",
"livekit_url": "wss://eu.livekit-np.int.bayer.com",
"participant_token": "eyJhbGciOiJIUzI1NiIs..."
}
| Field | Type | Description |
|---|---|---|
room_name |
string |
Generated room name: {cwid}-{agent_entra_app_id}-{timestamp}-{hex} |
livekit_url |
string |
WebSocket URL to connect to Bayer LiveKit Infra |
participant_token |
string |
Signed LiveKit JWT with participant grants and RoomAgentDispatch configuration |
Room configuration embedded in the token:
| Setting | Value | Purpose |
|---|---|---|
maxParticipants |
2 |
One client + one agent per room |
syncStreams |
true |
Synchronize audio/video streams |
departureTimeout |
30s |
Room stays alive 30 seconds after last participant leaves |
Session metadata forwarded to the agent:
Your metadata object is merged with participant identity fields and forwarded to the dispatched agent as job metadata:
{
"language": "en",
"department": "sales",
"participant_name": "John Doe",
"participant_identity": "john.doe@bayer.com",
"participant_cwid": "jdoe1"
}
The agent accesses this metadata via the JobContext provided by the Bayer LiveKit SDK.
Error Responses
| Status | Error | Cause |
|---|---|---|
400 |
Bad Request | agent_entra_app_id missing, not a valid UUID, metadata exceeds 50 keys or 10 KB |
400 |
Bad Request | client-id header missing when agent enforces client-to-agent authorization |
403 |
Forbidden | Client is not in the agent's preAuthorizedApplications or lacks the access_as_client scope |
404 |
Not Found | Agent with the given agent_entra_app_id has not called /api/agent/register |
500 |
Internal Server Error | Token generation failure |
Important
The agent_entra_app_id must belong to a previously registered agent. If the agent has not called /api/agent/register, this endpoint returns 404.
POST /livekit/webhook¶
Receives lifecycle events from the LiveKit server. This endpoint is not behind Ocelot — it is authenticated via HMAC-SHA256 signature verification using the LiveKit API key and secret.
Webhook Events Processed
| Event | Action | Status Set |
|---|---|---|
room_started |
Create a new agent_jobs record |
room_created |
participant_joined |
Update the record with participant or agent join time | participant_joined or active |
participant_left |
Record disconnect time and reason | completed (if participant) |
room_finished |
Compute session durations, finalize the record | completed, agent_never_joined, or failed |
Session Status Transitions
room_created → participant_joined → active → completed
↓
agent_never_joined / failed
Error Responses
| Status | Error | Cause |
|---|---|---|
401 |
Unauthorized | Missing or invalid HMAC-SHA256 signature |
500 |
Internal Server Error | Unhandled exception during event processing |
Authentication & Authorization¶
LKPS relies on a two-layer authentication model. Ocelot handles Entra JWT validation upstream; LKPS trusts the identity headers Ocelot injects and applies additional authorization logic.
Layer 1: Ocelot (Upstream)¶
Ocelot validates the Entra JWT before the request reaches LKPS:
- Verifies JWT signature and expiry against the Entra ID token endpoint
- Performs claims transformation
- Injects identity headers that LKPS trusts:
| Header | Source (Entra Claim) | Used For |
|---|---|---|
client-id |
appid |
Agent identity (register) / Client identity for authorization check (session start) |
user-id |
oid |
Participant CWID in room name generation |
x-bayer-user |
email |
Email fallback for logging and identity |
user-profile / x-bayer-user-profile |
Gateway-encoded object | Full participant identity: firstName, lastName, displayName, email, cwid |
Note
LKPS does not re-validate Entra JWTs. It trusts Ocelot's header injection. This is a deliberate design choice — Ocelot is a trusted infrastructure component within the same network boundary.
Layer 2: Client-to-Agent Authorization (Optional)¶
When an agent registers with enforce_client_authz: true (the default), LKPS performs an additional authorization check on every /api/session/start request for that agent:
- Extract the
client-idheader (the calling client's Entra App ID) - Acquire a client_credentials token from Entra ID for Microsoft Graph API access
- Call Microsoft Graph API to fetch the agent's
preAuthorizedApplicationslist:GET /applications(appId='{agent_entra_app_id}')?$select=api - Resolve the
access_as_clientscope name to its scope ID - Verify the client is listed in
preAuthorizedApplicationswith that scope ID - If not authorized → return
403 Forbidden
This ensures that only explicitly approved client applications can create sessions with a given agent.
Tip
See the Client-to-Agent Authorization guide for step-by-step Entra configuration instructions.
Related Resources¶
| Resource | Description |
|---|---|
| Getting Started Guide | Step-by-step guide for agent and client app developers |
| Agent Developer Guide | Build and deploy an AI agent using the SDK and LKPS |
| Client Developer Guide | Integrate a client application with the session start API |
| Client-to-Agent Authorization | Configure which client apps can access your agent |
| Bayer LiveKit SDK | The Python SDK that wraps LKPS authentication |
| Bayer LiveKit Infra | The underlying real-time media infrastructure |