URL: /docs/your-app

---
title: Your app
description: Create a call, mint a token, connect the browser.
icon: code
---

Rill does not know your users. Your **server** creates a call and mints a token. The **browser** uses that token to join. Mute, hang-up, and “who is this person” stay in your app.

```sh
npm i @rill/server @rill/client
```

Until a `v*` tag exists, use this repo’s `sdk/` workspace. React: also `@rill/react`. This repo’s `apps/web` is the reference app: SvelteKit mints tokens with `@rill/server`; the browser joins with `@rill/client`. There is no `@rill/svelte` package.

The first-party demo is `apps/web` (`docker compose up`). To try the React SDK sample locally, run `examples/node-token-server/mint.ts` and paste a token into `examples/react` — that mint is a dev sample, not the product path.

```
Your server                            Browser
───────────                            ───────
POST /v1/calls → { id, recording }     Rill.connect({ url, token })
save id                                speaker plays remote.microphone
token({ id, publish })                 call.camera.enable()
end()                                  disconnect() leaves; end() kills the slot
```

Both people receive each other's media. Mute is `enable()` / `disable()`. A third person is `call_full`. If Rill restarts, calls are gone — create again and mint new tokens.

Reference: [`@rill/server`](/sdk-server) · [`@rill/client`](/sdk-client) · [`@rill/react`](/sdk-react)

## Server

Mint tokens only on the server. The API secret never goes to the browser.

{/* example:server-hello */}
```ts
import { Rill } from "@rill/server";

const rill = new Rill({
  url: "https://rill.example.com",
  apiKey: process.env.RILL_API_KEY!,
  apiSecret: process.env.RILL_API_SECRET!,
});

const call = await rill.calls.create();
const token = call.token({
  id: "alice",
  name: "Alice",
  publish: { microphone: true, camera: true, screen: false, data: true },
  ttl: "15m",
});
console.log(call.id, token);
```
{/* /example:server-hello */}

`publish` is microphone, camera, screen, and data — all four keys required. `data` gates **send**; receive always works. Persist `call.id` in your app. Mint later with the stored id: `rill.calls.token(id, opts)`.

JWT wire stays compact: `sub`, `rid`, `nam`, `can: { a, v, s, d }`. SDKs never expose those names. Default TTL 15 minutes, max 24 hours.

## Two tokens (join, then accept)

{/* example:two-token */}
```ts
import { Rill } from "@rill/server";

const rill = new Rill({
  url: "https://rill.example.com",
  apiKey: process.env.RILL_API_KEY!,
  apiSecret: process.env.RILL_API_SECRET!,
});

const call = await rill.calls.create();
// Persist call.id in your app. Mint the first token when they join.
const waiting = call.token({
  id: "alice",
  publish: { microphone: true, camera: true, screen: false, data: true },
});

// Mint the second token only after your app accepts.
const accepted = rill.calls.token(call.id, {
  id: "bob",
  publish: { microphone: true, camera: true, screen: true, data: true },
});
console.log(waiting, accepted);
```
{/* /example:two-token */}

## Browser

`examples/react` and `examples/vanilla` are SDK samples (not served by compose).

{/* example:client-hello */}
```ts
import { Rill } from "@rill/client";

const video = document.querySelector("video")!;

async function mintFreshToken(): Promise<string> {
  return "paste-jwt";
}

const call = await Rill.connect({
  url: "https://rill.example.com",
  token: "paste-jwt",
  onToken: () => mintFreshToken(),
});

call.on("remote", (remote) => {
  video.srcObject = remote?.camera ?? null;
});
call.on("error", ({ code }) => console.error(code));
call.on("ended", () => console.log("call ended"));

await call.microphone.enable();
await call.camera.enable();
```
{/* /example:client-hello */}

Do not attach `remote.microphone` to a second `<audio>` — the SDK speaker already plays it. Pass `speaker: false` to opt out. Do not stash join tokens across a destroyed page. Mint a fresh one. If the signaling socket drops but media is up, the SDK reconnects the socket only. The server holds media for `RILL_SIGNAL_RECONNECT_GRACE` (default 15s).

## Data

Labels are `^[a-z][a-z0-9._-]{0,31}$`. Max 8 open labels (not counting `_rill`). Max message **128 KiB** — str0m cannot live-relay more than that across all labels on the association. Chunk large rrweb full snapshots in your app (60 KiB is a safe size and matches the recorder’s UDP chunks). Incremental mutations of a few KiB stream fine. `send` queues while the local channel is connecting (cap 32 per label) and resolves at `dc.send()`. A missing peer is not an SDK error; the SFU drops live relay. The first message after a label opens can drop while the peer channel is pairing — retry or send a tiny ping. The app owns the schema.

Streaming cobrowse (rrweb) and recording it: use a label such as `rrweb`. Each complete `send` is one JSONL line `{ t, encoding, payload }` next to that participant’s MP4s. Rill does not parse rrweb. Keep chunks ≤128 KiB so live relay and the recording fork see the same messages. Labels starting with `_` are SDK-internal and are not recorded.

```ts
await call.send("chat", "hello");
call.on("data", (msg) => {
  if (msg.label === "chat" && typeof msg.data === "string") {
    console.log(msg.data);
  }
});
```

React: do not subscribe in `useCall`; call `call.on("data")` yourself.

## Hang up

| Call | Who | What |
|------|-----|------|
| `call.disconnect()` | Browser (`@rill/client`) | This page leaves. The other person stays. The slot remains. |
| `call.end()` | Server (`@rill/server`) | Destroys the call. Both browsers get `ended`. |

Kick one participant with `rill.calls.kick(callId, participantId)` without ending the slot.

## React

{/* example:react-hello */}
```ts
import { useCall } from "@rill/react";

export function Stage({ url, token }: { url: string; token: string }) {
  const call = useCall({ url, token });
  if (!call) {
    return null;
  }
  return call.state;
}
```
{/* /example:react-hello */}

Bind `<video>` to `call.camera.stream` and `call.remote?.camera`. There is no provider and no video component.

## HTTP

Prefer `@rill/server` for tokens. HTTP mint (`POST /v1/calls/:id/tokens`) is for CLI and curl. Full route list: [HTTP](/http/create-call).

`--dev` allows unauthenticated GET/DELETE. Production does not.

## Webhooks

Set `RILL_WEBHOOK_URL`. POST JSON with `X-Rill-Timestamp` (unix seconds) and `X-Rill-Signature` (HMAC of `timestamp.body` with the API secret). Payload catalog: [Webhooks](/webhooks).
