LevelChatLevelChatDocs
Quickstart

Getting started

Quickstart

From zero to 'I am in a room' in five minutes.

LevelChat is a video / voice / live-streaming platform you drop into any web, mobile, or native app. This guide gets you to a working room as fast as possible.

The flow is the same on every platform:

  1. Sign up + create a project → get an API key.
  2. Mint a short-lived room token on your server.
  3. Hand the token to the client SDK.
  4. Publish camera / mic, render incoming tiles.

Total: ~50 lines of code.

0. Sign up + grab an API key

The SDKs talk to a LevelChat server. You can either point them at the managed cloud (fastest) or run the server yourself. For the managed cloud:

  1. Open https://app.levelchat.io/console/signup and create your workspace.
  2. After sign-in you land in Studio. The signup flow auto-creates a Default project for you.
  3. Open Projects → Default → API Keys.
  4. Click + New key. Pick the scopes (publish, subscribe, record — or all three for an admin key). Copy the plaintext key — it's shown once, never again.

Your key looks like lc_pk_<keyId>.<secret>. Keep secret server-side; never bake it into the browser bundle.

For self-host, the same flow works at https://<your-domain>/console/signup once your stack is up — see On-prem deployment.

1. Install the SDK

Pick the SDK for your platform:

Web (vanilla Next.js Vite)
npm install @levelchat/web
React (hooks + components)
npm install @levelchat/web @levelchat/web-react
React Native
npm install @levelchat/react-native react-native-webrtc
iOS (Swift Package Manager) — preview
# Public Swift package + CocoaPods release land with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/ios
Android — preview
# Maven Central publication lands with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/android
Flutter — preview
# pub.dev publish lands with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/flutter

2. Mint a token on your server

Tokens are minted on your server — never in the browser. Issue a POST /v1/auth/tokens/room to LevelChat with your project API key (lc_pk_*); the response includes a short-lived JWT the user joins one specific room with.

A standalone @levelchat/node SDK is on the roadmap; until it lands, use plain fetch — the request shape is small and the SDK won't change it:

TypeScriptappapilc-tokenroute.ts (Next.js App Router)
/**
 * Mint a LevelChat room JWT on the server. The browser MUST call this
 * route with POST — the request never carries a body but the method is
 * what distinguishes a token-mint call from any other side-effect-free
 * GET we expose (caching proxies and edge runtimes treat them
 * differently). The route below ignores the body; it reads the room id
 * from the query string + the user id from your existing session.
 *
 * Trust boundary: `userId`, `identity`, `role`, and `caps` are derived
 * SERVER-side from the authenticated session — never from the request
 * body. A signed-in viewer cannot promote themselves to publisher by
 * lying about caps in the browser; the values your route sends to
 * LevelChat are the source of truth.
 */
export async function POST(req: Request) {
  const userId = await yourAuth(req); // your existing session/cookie auth
  if (!userId) return new Response('unauthorized', { status: 401 });
  const roomId = new URL(req.url).searchParams.get('room');
  if (!roomId) return new Response('room is required', { status: 400 });

  const res = await fetch(`${process.env.LEVELCHAT_API_URL}/v1/auth/tokens/room`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.LEVELCHAT_API_KEY}`, // lc_pk_xxx.yyy
    },
    body: JSON.stringify({
      roomId,
      userId,
      displayName: 'Alice Example',
      identity: userId,
      // Canonical RoomType values supported by the backend today:
      //   'meeting' | 'live' | 'webinar' | '1to1'
      // Use 'live' for one-to-many broadcasts (the Studio UI labels it
      // "Broadcast"). 'classroom' is reserved in the DB enum but has
      // no capability matrix yet — treat it as Preview.
      roomType: 'meeting',
      // 'publisher' grants publish + subscribe on meeting/webinar.
      // 'broadcaster' is the live-room author. 'viewer' is subscribe-only.
      role: 'publisher',
      // Caps are an INTERSECT-DOWN list — they can NEVER elevate above
      // what (roomType, role) allows. Sending an empty array gets you
      // the full canonical cap set for the (roomType, role) pair.
      caps: ['publish:camera', 'publish:mic', 'subscribe:all'],
      ttlSeconds: 600, // 10 minutes
    }),
  });

  if (!res.ok) {
    const detail = await res.text().catch(() => '');
    return new Response(`token mint failed: ${res.status} ${detail}`, { status: 502 });
  }
  const { token, expires_at } = await res.json();
  return Response.json({ token, expires_at });
}

The mint response is { token, expires_at }. The client SDK reads the room id straight out of the JWT and connects to the LevelChat managed cloud with no extra configuration — you don't need to pass a signaling URL unless you self-host (see step 3).

Or for prototyping, curl the same endpoint directly:

~
curl -X POST "$LC_API_URL/v1/auth/tokens/room" \
  -H "Authorization: Bearer lc_pk_xxx.yyy" \
  -H "Content-Type: application/json" \
  -d '{
    "roomId": "r_demo",
    "userId": "u_alice",
    "displayName": "Alice",
    "identity": "alice",
    "roomType": "meeting",
    "role": "publisher",
    "caps": ["publish:camera","publish:mic","subscribe:all"],
    "ttlSeconds": 600
  }'

3. Connect from the browser

The vanilla @levelchat/web SDK is one named import:

TypeScriptapppage.tsx
'use client';
import { LevelChat } from '@levelchat/web';
import { useEffect, useRef } from 'react';

export default function Page() {
  const localVideo = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    (async () => {
      // Always POST — the route handler above only accepts POST.
      const res = await fetch('/api/lc-token?room=r_demo', { method: 'POST' });
      if (!res.ok) {
        console.error('token mint failed:', res.status);
        return;
      }
      const { token } = await res.json();

      // On the managed cloud the SDK connects with no extra config.
      // Self-hosting? Pass your signaling URL, e.g.
      // `new LevelChat({ signalingUrl: 'wss://ws.your-domain.com/v1/rtc/signal' })`.
      // `joinLive` is the high-level helper for meeting + live + webinar
      // + 1to1 rooms — the topology lives in the JWT (set server-side at
      // mint time), so we don't pass `roomType` here. Use `client.joinRoom`
      // when you want the low-level Room object without the helper
      // (publish/subscribe APIs work the same, but you wire the camera +
      // mic yourself instead of via `live.publishCamera()`).
      const lc = new LevelChat({ logLevel: 'info' });
      const live = await lc.joinLive({ token, role: 'broadcaster' });

      const cam = await live.publishCamera({ resolution: '720p' });
      await live.publishMic();

      // Bind the local preview.
      if (cam.mediaStreamTrack && localVideo.current) {
        localVideo.current.srcObject = new MediaStream([cam.mediaStreamTrack]);
        await localVideo.current.play();
      }

      // Render every remote track that arrives.
      live.room.on('track-subscribed', (t) => {
        if (!t.mediaStreamTrack) return;
        const el = document.createElement('video');
        el.srcObject = new MediaStream([t.mediaStreamTrack]);
        el.autoplay = true;
        el.playsInline = true;
        document.getElementById('remote')!.appendChild(el);
      });
    })();
  }, []);

  return (
    <div>
      <video ref={localVideo} autoPlay playsInline muted style={{ width: 320 }} />
      <div id="remote" style={{ display: 'flex', gap: 12 }} />
    </div>
  );
}

That's it. Open the page in two tabs — they join the same r_demo room and see each other.

4. (Optional) Use the React bindings

If you want hooks + ready-made components, @levelchat/web-react wraps the vanilla SDK:

TypeScript (TSX)apppage.tsx (with @levelchatweb-react)
'use client';
import { useEffect, useState } from 'react';
import { LevelChatProvider, useLocalParticipant, ParticipantGrid } from '@levelchat/web-react';

export default function Page() {
  const [token, setToken] = useState<string>();
  const [error, setError] = useState<string>();
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch('/api/lc-token?room=r_demo', { method: 'POST' });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const { token } = (await res.json()) as { token: string };
        if (!cancelled) setToken(token);
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err.message : 'token mint failed');
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  if (error) return <p>Could not join: {error}</p>;

  if (!token) return <p>Connecting…</p>;
  // `autoJoin` takes the same options as `client.joinRoom`. The provider
  // joins on mount and tears the room down on unmount.
  return (
    <LevelChatProvider autoJoin={{ token }}>
      <ParticipantGrid />
      <Controls />
    </LevelChatProvider>
  );
}

function Controls() {
  const { publishCamera, publishMic } = useLocalParticipant();
  return (
    <div>
      <button onClick={() => publishCamera()}>Share camera</button>
      <button onClick={() => publishMic()}>Share mic</button>
    </div>
  );
}

What's next

Where to find your API key: sign in to the LevelChat Studio and open Projects → <your project> → API Keys.

Quickstart — install, mint a token, go live — LevelChat Docs