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 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.identityis what your agent'son_first_participant_joinedevent fires on.