← All @molecule/* packages · App templates

@molecule/api-video-rooms-livekit

Provider bond · video-rooms · API (Node) · v1.0.1 · Apache-2.0

LiveKit video rooms provider for molecule.dev. Implements @molecule/api-video-rooms via the LiveKit Server API (Twirp transport) and HS256 JWT meeting tokens via livekit-server-sdk. Self-hostable + LiveKit Cloud.

npm install @molecule/api-video-rooms-livekit

npm · Source on GitHub · Implements @molecule/api-video-rooms

How it works

@molecule/api-video-rooms-livekit is a provider bond on the API (Node) side: it implements the video-rooms core interface (@molecule/api-video-rooms) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider } from '@molecule/api-video-rooms'
import { createProvider } from '@molecule/api-video-rooms-livekit'

// Bond at startup (reads LIVEKIT_URL / LIVEKIT_API_KEY /
// LIVEKIT_API_SECRET by default)
setProvider(createProvider())

// Explicit config. For real cloud recording, also pass `recordingEgress` — a
// LiveKit `RoomEgress` (built with RoomCompositeEgressRequest + EncodedFileOutput
// + your S3/GCP/Azure upload), or a `(roomName) => RoomEgress` factory; see the
// `recordingEgress` docs in @remarks. With it, `createRoom({ recording: true })`
// starts a real room-composite egress; WITHOUT it, that call throws rather than
// silently not recording.
setProvider(
  createProvider({
    host: 'https://livekit.example.com',
    apiKey: 'APIxxx',
    apiSecret: 'secretxxx',
  }),
)

Works with: @molecule/api-secrets, @molecule/api-video-rooms

Secrets: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

LiveKit video rooms provider for molecule.dev.

Implements the @molecule/api-video-rooms interface against the LiveKit Server API (Twirp transport) and the livekit-server-sdk AccessToken HS256 JWT signer. Self-hostable + LiveKit Cloud — the recommended provider for users who need to keep media on their own infrastructure.

Quick Start

import { setProvider } from '@molecule/api-video-rooms'
import { createProvider } from '@molecule/api-video-rooms-livekit'

// Bond at startup (reads LIVEKIT_URL / LIVEKIT_API_KEY /
// LIVEKIT_API_SECRET by default)
setProvider(createProvider())

// Explicit config. For real cloud recording, also pass `recordingEgress` — a
// LiveKit `RoomEgress` (built with RoomCompositeEgressRequest + EncodedFileOutput
// + your S3/GCP/Azure upload), or a `(roomName) => RoomEgress` factory; see the
// `recordingEgress` docs in @remarks. With it, `createRoom({ recording: true })`
// starts a real room-composite egress; WITHOUT it, that call throws rather than
// silently not recording.
setProvider(
  createProvider({
    host: 'https://livekit.example.com',
    apiKey: 'APIxxx',
    apiSecret: 'secretxxx',
  }),
)

Type

provider

Installation

npm install @molecule/api-video-rooms-livekit @molecule/api-secrets @molecule/api-video-rooms livekit-server-sdk

API

Interfaces

LiveKitEgressClient

Minimal EgressClient surface used by the provider for listing recordings.

interface LiveKitEgressClient {
  /** Lists egress (recording) jobs, optionally filtered by room. */
  listEgress(options?: { roomName?: string }): Promise<LiveKitEgressInfo[]>
}

LiveKitEgressInfo

Minimal LiveKit egress (recording job) shape. Status is the numeric EgressStatus enum value from @livekit/protocol.

interface LiveKitEgressInfo {
  /** Egress identifier. */
  egressId: string

  /** Name of the room the egress belongs to. */
  roomName: string

  /** Numeric `EgressStatus` enum (0=starting … 6=limit_reached). */
  status?: number

  /** Unix-seconds (or bigint) start time. */
  startedAt?: number | bigint

  /** Unix-seconds (or bigint) end time. */
  endedAt?: number | bigint

  /** Recorded file results, when present. */
  fileResults?: Array<{
    location?: string
    duration?: number | bigint
  }>
}

LiveKitRoom

Minimal LiveKit Room shape consumed by the provider. The real protobuf class exposes additional fields we don't depend on.

interface LiveKitRoom {
  /** Room name (stable identifier). */
  name: string

  /** Room sid (unique per-instance id). */
  sid?: string

  /** Maximum simultaneous participants. */
  maxParticipants?: number

  /** Empty-room TTL in seconds. */
  emptyTimeout?: number

  /** Departure-room TTL in seconds. */
  departureTimeout?: number

  /** Provider-side opaque metadata blob. */
  metadata?: string

  /** Unix-seconds creation timestamp (the protobuf field is `bigint`). */
  creationTime?: number | bigint

  /** Whether a recording is currently active on the room. */
  activeRecording?: boolean
}

LiveKitRoomServiceClient

Minimal RoomServiceClient surface used by the provider. Tests inject a stub here to avoid HTTP traffic. The real livekit-server-sdk.RoomServiceClient satisfies this shape.

interface LiveKitRoomServiceClient {
  /** Creates a new LiveKit room. */
  createRoom(options: {
    name: string
    emptyTimeout?: number
    departureTimeout?: number
    maxParticipants?: number
    metadata?: string
    /**
     * Auto-egress specification. When set, LiveKit automatically starts a
     * room-composite (or participant/track) egress once the room becomes
     * active — this is how the bond wires `CreateRoomOptions.recording`.
     */
    egress?: RoomEgress
  }): Promise<LiveKitRoom>

  /** Lists active rooms, optionally filtered by name. */
  listRooms(names?: string[]): Promise<LiveKitRoom[]>

  /** Deletes a room by name. */
  deleteRoom(room: string): Promise<void>
}

LiveKitVideoRoomsConfig

Configuration for the LiveKit video rooms provider.

interface LiveKitVideoRoomsConfig {
  /**
   * LiveKit host (including protocol), e.g.
   * `https://my-project.livekit.cloud` or `https://livekit.example.com`.
   * Defaults to `process.env.LIVEKIT_URL` (which may use `wss://` — the
   * provider rewrites it to `https://` for HTTP/Twirp calls).
   */
  host?: string

  /**
   * LiveKit API key. Defaults to `process.env.LIVEKIT_API_KEY`.
   */
  apiKey?: string

  /**
   * LiveKit API secret. Defaults to `process.env.LIVEKIT_API_SECRET`.
   * Used to sign JWT meeting tokens via HS256.
   */
  apiSecret?: string

  /**
   * Optional default token TTL in seconds. Used when
   * `createMeetingToken()` is called without an `expiresAt`. Defaults to
   * `21600` (6 hours), matching the `livekit-server-sdk` default.
   */
  defaultTokenTtl?: number

  /**
   * Auto-egress specification used to satisfy `createRoom({ recording:
   * true })`. LiveKit records via Egress, which **requires** a storage
   * destination (S3 / GCP / Azure / AliOSS via `EncodedFileOutput`, or a
   * stream/segment output) — a destination the core `recording: boolean`
   * flag does not carry. Supply the LiveKit `RoomEgress` (with your
   * storage output) here, either as a fixed object or a factory that
   * receives the room name and returns a per-room `RoomEgress`. When it is
   * set, `createRoom({ recording: true })` attaches it to the room so
   * LiveKit auto-starts a room-composite egress once the room is active.
   *
   * When this is **not** configured, `createRoom({ recording: true })`
   * throws rather than silently returning a room with no recording — LiveKit
   * cannot record without a storage output.
   */
  recordingEgress?: RoomEgress | ((roomName: string) => RoomEgress)

  /**
   * Optional pre-built `RoomServiceClient` (or compatible stub). Tests
   * inject a stub here. When omitted, the provider lazily constructs a
   * real `RoomServiceClient` from the SDK on first use.
   */
  roomServiceClient?: LiveKitRoomServiceClient

  /**
   * Optional pre-built `EgressClient` (or compatible stub). Tests inject
   * a stub here. When omitted, the provider lazily constructs a real
   * `EgressClient` from the SDK on first use.
   */
  egressClient?: LiveKitEgressClient

  /**
   * Optional `AccessToken` constructor override. Tests inject a fake
   * builder to avoid the real `jose` HS256 signer. Defaults to the
   * `AccessToken` class exported by `livekit-server-sdk`.
   */
  accessTokenCtor?: AccessTokenCtor
}

Types

AccessTokenCtor

Constructor for livekit-server-sdk's AccessToken class. Exposed so tests can inject a fake builder without spinning up the real jose signer.

type AccessTokenCtor = new (
  apiKey?: string,
  apiSecret?: string,
  options?: ConstructorParameters<typeof AccessTokenClass>[2],
) => AccessTokenClass

Functions

createProvider(config)

Creates a LiveKit-backed {@link VideoRoomsProvider}.

Self-hostable: pass a host of e.g. https://livekit.example.com or set LIVEKIT_URL to the same value (or a wss:// URL — the provider rewrites the protocol for the HTTP/Twirp endpoints).

The API secret is never included in any error thrown by this provider — both the missing-secret error and SDK-error wrappers scrub it out.

function createProvider(config?: LiveKitVideoRoomsConfig): VideoRoomsProvider
  • config — LiveKit provider configuration. Falls back to the LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET env vars when the corresponding fields are omitted.

Returns: A fully initialised VideoRoomsProvider backed by LiveKit.

Constants

videoRoomsLivekitSecretDefinitions

Secret definitions required by the LiveKit video-rooms bond.

const videoRoomsLivekitSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-video-rooms interface.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-secrets ^1.0.1
  • @molecule/api-video-rooms ^1.0.1

Environment Variables

  • LIVEKIT_URL (required) — LiveKit server URL
    • Setup: Your LiveKit Cloud project URL (or self-hosted wss:// URL).
    • Get it here: https://cloud.livekit.io/
    • Example: wss://your-app.livekit.cloud
  • LIVEKIT_API_KEY (required) — LiveKit API key
  • LIVEKIT_API_SECRET (required) — LiveKit API secret

Runtime Dependencies

  • @molecule/api-secrets

  • @molecule/api-video-rooms

  • livekit-server-sdk

  • LiveKit rooms are always token-gated — there is no public mode. Every join needs a token from createMeetingToken(), and the returned Room.url is the server's wss:// endpoint (not a click-to-join link). A private room is therefore LiveKit's native, enforced behaviour and is reported truthfully. Requesting createRoom({ privacy: 'public' }) throws — this bond will not return a room falsely labelled public.

  • recording: true requires a configured egress output. LiveKit records via Egress, which needs a storage destination (S3 / GCP / Azure / AliOSS file output, or a stream/segment output) — a destination the core recording flag does not carry. Supply it via config.recordingEgress (a LiveKit RoomEgress, or a (roomName) => RoomEgress factory); the bond attaches it to the room so LiveKit auto-starts a room-composite egress once the room is active. createRoom({ recording: true }) throws when recordingEgress is not configured — it never returns a room that silently isn't recording.

  • listRecordings() reflects the real LiveKit Egress state (it calls EgressClient.listEgress): an empty result means no egress ran, not a swallowed failure.

  • expiresAt on createRoom maps to LiveKit's emptyTimeout (how long an empty room survives), not an absolute expiry.

  • All meeting tokens grant canPublish + canSubscribe; isOwner adds roomAdmin — there is no subscribe-only token option in this revision.

  • createProvider() throws at bond time when LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET are unset.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. CAVEAT: the live A/V call and the in-room video UI run inside the provider's client and CANNOT be driven in the sandbox — verify the room LIFECYCLE and the per-participant join TOKENS you mint server-side, never the media itself:

  • Creating a room returns a usable handle: createRoom(...) yields a RoomCreated with a stable name and a joinable url, and the app persists that name on its own record — not a throwaway URL it can never resolve again.
  • Each participant gets its OWN token: the app calls createMeetingToken(room.name, { userName, expiresAt, isOwner }) per user, so two joiners receive two DISTINCT, short-lived credentials — never one static shared secret reused for everyone.
  • The token honors role + scope + expiry: an owner/moderator token (isOwner: true) differs from a plain-participant token, each is scoped to the single room.name it was minted for (it admits no other room), and it carries the requested expiresAt — inspect the minted token's claims; don't assume it, and don't hand an owner token to an ordinary participant.
  • getRoom(name) reflects real state: a created room resolves with its configured privacy/maxParticipants/recording, and after deleteRoom(name) it returns null — ending a room actually removes it, so its old URL/tokens no longer admit a join. (Live participant count is NOT in the core Room type — don't assert on it.)
  • Size/quota holds: if the app sets maxParticipants, the created room carries that cap (the provider enforces it at join) — it isn't silently dropped.
  • SECURITY — the provider API key (e.g. DAILY_CO_API_KEY) stays server-side; the browser only ever receives a token/URL your endpoint returned, never the key or a direct provider call. Private rooms are un-guessable: only an authorized user's request mints a token, and no unauthenticated caller joins a private room by guessing its name/URL without one.