Skip to content

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

  1. Overview
  2. The Problem
  3. The Solution
  4. System Architecture
  5. API Reference
  6. Authentication & Authorization
  7. 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:

  1. Validates agent identities — accepts Entra-authenticated requests from agents and frontend clients.
  2. Issues scoped LiveKit JWT tokens — signed with credentials that never leave the platform.
  3. Creates LiveKit rooms with agent dispatch — rooms are pre-configured to automatically dispatch the correct AI agent.
  4. Tracks session lifecycle — processes LiveKit webhooks to record room creation, participant joins/leaves, and session completion in PostgreSQL.
  5. 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

LiveKit Platform Service — 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:

  1. Extract entra_app_id from the user-id or client-id header
  2. Generate a unique worker identity: agent-{entraAppId}-{uuid}
  3. Sign a LiveKit AccessToken with agent-scoped grants
  4. 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:

  1. Extract the client-id header (the calling client's Entra App ID)
  2. Acquire a client_credentials token from Entra ID for Microsoft Graph API access
  3. Call Microsoft Graph API to fetch the agent's preAuthorizedApplications list:
    GET /applications(appId='{agent_entra_app_id}')?$select=api
    
  4. Resolve the access_as_client scope name to its scope ID
  5. Verify the client is listed in preAuthorizedApplications with that scope ID
  6. 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.


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