03 โ€” Node.js backend integration

Your Node backend is responsible for authenticating your users and then minting LiveKit tokens for them via the arzbridge API. Below is a clean, production-ready setup (no extra SDK needed โ€” just fetch, built into Node 18+).

1. Configuration

Keep credentials in environment variables โ€” never in code or the frontend.

# .env
ARZBRIDGE_BASE_URL=https://calls.arzbridge.com/api/v1
ARZBRIDGE_API_KEY=YOUR_API_KEY
ARZBRIDGE_API_SECRET=YOUR_API_SECRET

2. A reusable client (arzbridge.js)

// arzbridge.js  โ€” a tiny, dependency-free client for the arzbridge calls API
const BASE = process.env.ARZBRIDGE_BASE_URL;
const AUTH = `Bearer ${process.env.ARZBRIDGE_API_KEY}:${process.env.ARZBRIDGE_API_SECRET}`;

async function call(method, path, body) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    const msg = json?.error?.message || `arzbridge ${res.status}`;
    const err = new Error(msg);
    err.status = res.status;
    err.body = json;
    throw err;
  }
  return json.data;
}

export const arzbridge = {
  // Mint a join token for a participant.
  createToken: (opts) => call('POST', '/tokens', opts),
  // Rooms
  listRooms: () => call('GET', '/rooms'),
  createRoom: (opts) => call('POST', '/rooms', opts),
  getRoom: (name) => call('GET', `/rooms/${encodeURIComponent(name)}`),
  deleteRoom: (name) => call('DELETE', `/rooms/${encodeURIComponent(name)}`),
  // Participants
  listParticipants: (room) => call('GET', `/rooms/${encodeURIComponent(room)}/participants`),
  removeParticipant: (room, identity) =>
    call('DELETE', `/rooms/${encodeURIComponent(room)}/participants/${encodeURIComponent(identity)}`),
  // Data message
  sendData: (room, data, opts = {}) =>
    call('POST', `/rooms/${encodeURIComponent(room)}/data`, { data, ...opts }),
};

3. Expose a token endpoint to your app (Express)

// server.js
import express from 'express';
import { arzbridge } from './arzbridge.js';

const app = express();
app.use(express.json());

// Your app calls THIS endpoint (with your own auth) to join a call.
app.post('/api/calls/join', async (req, res) => {
  try {
    // 1) Authenticate the user with YOUR system (session, JWT, etc.)
    const user = req.user;                       // however you auth
    if (!user) return res.status(401).json({ error: 'unauthenticated' });

    // 2) Decide which room they may join + their role (your business rules)
    const { roomId } = req.body;

    // 3) Mint a token scoped to that user + room
    const { token, ws_url } = await arzbridge.createToken({
      room: `room-${roomId}`,
      identity: `user-${user.id}`,
      name: user.name,
      ttl: 3600,
      // role example: a viewer in a webinar
      // can_publish: false,
      metadata: { role: user.role },
    });

    // 4) Hand the token to your app
    res.json({ token, wsUrl: ws_url });
  } catch (e) {
    res.status(e.status || 500).json({ error: e.message });
  }
});

app.listen(3000, () => console.log('listening on :3000'));

Your Flutter/web app calls POST /api/calls/join and gets back { token, wsUrl }.

4. Server-side controls

// Kick a user
await arzbridge.removeParticipant('room-42', 'user-99');

// End a call for everyone
await arzbridge.deleteRoom('room-42');

// Broadcast a signal (e.g. "quiz started") to all participants
await arzbridge.sendData('room-42', JSON.stringify({ type: 'quiz_started' }), { topic: 'signals' });

// See who's connected
const people = await arzbridge.listParticipants('room-42');

5. Error handling & retries

  • 401/403 โ†’ fix credentials/scopes (config issue, not transient).
  • 422 โ†’ bad input; inspect err.body.error.fields.
  • 429 โ†’ you're rate-limited; honor Retry-After and back off.
  • 5xx โ†’ retry with exponential backoff (2โ€“3 tries).
async function withRetry(fn, tries = 3) {
  for (let i = 0; i < tries; i++) {
    try { return await fn(); }
    catch (e) {
      if (e.status && e.status < 500 && e.status !== 429) throw e; // don't retry client errors
      if (i === tries - 1) throw e;
      await new Promise(r => setTimeout(r, 300 * 2 ** i));
    }
  }
}

(Optional) Node as a call client

If you also have a web frontend, use the browser SDK livekit-client with the token from your backend โ€” see the snippet in the main README and LiveKit JS docs. For mobile, see Flutter.