LiveKit token server (Node)

Issue short-lived JWTs so browser/mobile clients can join a LiveKit room — the missing piece every voice / video MVP needs.

LiveKit token server (Node)

LiveKit clients need a signed JWT to join a room. Issuing one server-side is tiny but the docs spread it across pages — here is the entire endpoint.

Install

npm i livekit-server-sdk

Endpoint

import { AccessToken } from "livekit-server-sdk";

export async function POST(req: Request) {
  const { identity, room } = await req.json();
  if (!identity || !room) {
    return Response.json({ error: "identity and room required" }, { status: 400 });
  }

  const at = new AccessToken(
    process.env.LIVEKIT_API_KEY!,
    process.env.LIVEKIT_API_SECRET!,
    { identity, ttl: "15m" }
  );

  at.addGrant({
    room,
    roomJoin: true,
    canPublish: true,
    canSubscribe: true,
    canPublishData: true,
  });

  const token = await at.toJwt();
  return Response.json({ token, url: process.env.LIVEKIT_URL });
}

Client

import { Room } from "livekit-client";

export async function joinRoom(identity: string, room: string) {
  const res = await fetch("/api/livekit-token", {
    method: "POST",
    body: JSON.stringify({ identity, room }),
  });
  const { token, url } = await res.json();

  const r = new Room({ adaptiveStream: true, dynacast: true });
  await r.connect(url, token);
  await r.localParticipant.setMicrophoneEnabled(true);
  return r;
}

Things that bit me in production

  • TTL too long. Default is 6 hours. Set ttl: "15m" and refresh — leaked tokens stay valid forever otherwise.
  • canPublishData. Needed for the data channel that Pipecat agents use to ship intent / state events alongside audio.
  • CORS. Production-host the token server on the same origin as the page — don't punch wildcard CORS holes for it.
  • Identity. Use a stable user id, not a random UUID per session. participant.identity is what your agent's on_first_participant_joined event fires on.