LevelChatLevelChatDocs
10-line integration

Guides

10-line copy-paste integration

Four starters · server token in 10 lines · snippets written against the current SDK public API

10-line integration story

Each of the four starters below — Next.js, Vite + React, iOS, or Android — is a self-contained snippet: paste it, run the app, and two clients can connect to the same room.

Every snippet here is written against the current SDK public API — the same LevelChat, Room, and hook surfaces the packages export today.


The 10 lines (Next.js)

TypeScript (TSX)apppage.tsx
import { LevelChat } from '@levelchat/web';

export default function Page() {
  const join = async () => {
    const lc = new LevelChat({ logLevel: 'warn' });
    const { token } = await fetch('/api/lc-token', { method: 'POST' }).then((r) => r.json());
    const live = await lc.joinLive({ token, roomType: 'meeting' });
    await live.publishCamera();
    await live.publishMic();
  };
  return <button onClick={join}>Join</button>;
}

That's it. Nine of those lines are not boilerplate — every one earns its keep:

  1. import { LevelChat } — single named import, tree-shakes the rest.
  2. new LevelChat({ logLevel: 'warn' }) — production default. Use 'debug' while developing.
  3. fetch('/api/lc-token', { method: 'POST' }) — your server mints a short-lived JWT and returns { token }. 10-line server snippet below.
  4. lc.joinLive({ token, roomType: 'meeting' }) — same call shape for every topology (1to1, meeting, live, webinar).
  5. live.publishCamera() — kicks off getUserMedia and adds the track to the SFU.
  6. live.publishMic() — same, for audio.

That's the full developer surface area for the happy path. Reconnects, glare avoidance, codec negotiation, encryption keys — the SDK handles them.


The 10-line server (Node)

A standalone @levelchat/node package is on the roadmap; until it ships, fetch to the public token-mint endpoint with your project API key:

TypeScriptappapilc-tokenroute.ts (Next.js App Router)
export async function GET(req: Request) {
  const userId = await yourAuth(req); // your existing auth
  const roomId = new URL(req.url).searchParams.get('room') ?? 'demo-room';

  const identity = userId ?? `user-${crypto.randomUUID().slice(0, 8)}`;
  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: identity,
      displayName: identity,
      identity,
      roomType: 'meeting', // 'meeting' | 'live' | 'webinar' | '1to1'
      role: 'publisher',
      caps: ['publish:camera', 'publish:mic', 'subscribe:all'],
      ttlSeconds: 60,
    }),
  });

  if (!res.ok) return new Response('token mint failed', { status: 500 });
  return Response.json(await res.json()); // { token, expires_at }
}

Three things to note:

  • No external auth library. Call this after your own auth stack (Auth.js, Clerk, NextAuth, custom JWT, ...) has resolved who the user is — pass that identity into the body.
  • TTL is 60 seconds. The token only authorises the WebSocket upgrade — once the SDK is connected, the SFU has its own session. Short TTLs blow up clean if a token leaks.
  • role is 'publisher' for participants who need to publish camera/mic. Other values: 'viewer' (subscribe-only), 'co-host' (publisher + moderate), 'webinar-attendee' (subscribe-only with chat), 'webinar-panelist' (publish + chat). Full table on /api.

Vite + React

Same Vite/React stack — install the React bindings on top of the vanilla SDK:

~
npm install @levelchat/web @levelchat/web-react
TypeScript (TSX)srcApp.tsx
import { useEffect, useState } from 'react';
import { LevelChatProvider, ParticipantGrid, useLocalParticipant } from '@levelchat/web-react';

export default function App() {
  const [token, setToken] = useState<string>();
  useEffect(() => {
    fetch('/api/lc-token?room=r_demo', { method: 'POST' })
      .then((r) => r.json())
      .then((d) => setToken(d.token));
  }, []);

  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() {
  // `useLocalParticipant` exposes the room's publish helpers.
  const { publishCamera, publishMic } = useLocalParticipant();
  return (
    <>
      <button onClick={() => publishCamera()}>Share camera</button>
      <button onClick={() => publishMic()}>Share mic</button>
    </>
  );
}

You fetch the token from your server's /api/lc-token route (from §"The 10-line server" above) and hand it to LevelChatProvider via autoJoin — it opens the WS + ICE behind a single context. Under the hood it's the same vanilla @levelchat/web SDK — same call shape, same wire format, no second engine to debug.


iOS (Swift)

SwiftContentView.swift
import LevelChat
import SwiftUI

struct ContentView: View {
  var body: some View {
    Button("Join") {
      Task {
        let token = try await TokenAPI.fetch()
        let client = try await LevelChat(config: LevelChatConfig(logLevel: .info))
        let room = try await client.joinRoom(JoinRoomOptions(token: token))
        try await room.publishCamera()
        try await room.publishMic()
      }
    }
  }
}

room.events is an AsyncStream<RoomEvent> — iterate it with for await in a Task and drive your SwiftUI state from the typed events (participantJoined, connectionState, …).


Android (Kotlin)

KotlinMainActivity.kt
import ai.levelchat.sdk.LevelChat
import ai.levelchat.sdk.JoinRoomOptions

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // `LevelChat` is built via the `create` factory, not `new`.
    val lc = LevelChat.create(context = applicationContext)
    setContent {
      Button(onClick = {
        lifecycleScope.launch {
          val token = TokenAPI.fetch()
          val room = lc.joinRoom(JoinRoomOptions(token = token))
          room.publishCamera()
          room.publishMic()
        }
      }) { Text("Join") }
    }
  }
}

Declare CAMERA and RECORD_AUDIO in AndroidManifest.xml and request them at runtime (ActivityResultContracts.RequestMultiplePermissions) before you call publishCamera / publishMic — Android does not grant them implicitly.


What the SDK does NOT need from you

These are the things you do not have to write code for:

  • STUN / TURN configuration. Default STUN ships built-in; TURN credentials come down with the JWT when present.
  • ICE restart on network change. Built into the perfect-negotiation pipeline.
  • Glare avoidance. Polite/impolite roles + collision detection — exact pattern from W3C webrtc-pc spec example.
  • Bandwidth probing. We negotiate three simulcast layers (low / mid / high) automatically; the SFU drops the right layer per consumer.
  • Reconnect with backoff. Exponential + jitter, capped at 30s.

If you find yourself writing code for any of the above, stop and email [email protected] — that's a sign the SDK has a gap, not a sign you need extra glue.


Staying in sync

These snippets target the SDK public API as it ships today — LevelChat, Room, and the @levelchat/web-react hooks. They are not auto-generated or run by CI; they are written by hand against the packages' public exports and reviewed when the SDK surface changes. If a snippet ever drifts from the package it targets, email [email protected].


What's next

10-line copy-paste integration — LevelChat Docs