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:
- Sign up + create a project → get an API key.
- Mint a short-lived room token on your server.
- Hand the token to the client SDK.
- 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:
- Open https://app.levelchat.io/console/signup and create your workspace.
- After sign-in you land in Studio. The signup flow auto-creates a
Defaultproject for you. - Open Projects → Default → API Keys.
- 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:
npm install @levelchat/webnpm install @levelchat/web @levelchat/web-reactnpm install @levelchat/react-native react-native-webrtc# Public Swift package + CocoaPods release land with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/ios# Maven Central publication lands with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/android# pub.dev publish lands with v0.3.
# Need the source today? Email [email protected].
# See: https://docs.levelchat.io/sdk/flutter2. 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:
/**
* 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:
'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:
'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
- Understand the system → How LevelChat works — the mental model behind rooms, tracks, the join lifecycle, and mesh vs SFU.
- Record the room →
room.record({ compose: 'tracks' }) - Self-host → see On-prem deployment
- Per-platform deep dives → Web SDK, iOS, Android, React Native, Flutter
- Licensing model → open source vs commercial — what's MIT, what's BUSL, where the production threshold is.
Where to find your API key: sign in to the LevelChat Studio and open Projects → <your project> → API Keys.