# Integrating Relay into an existing web app

This is the complete path for a team that already has a web app and wants to add chat + calling to
it — not a new demo app. Everything you need is on this page; the package's own
[`packages/web/react/README.md`](../packages/web/react/README.md) has the full API reference if
you need more than what's shown here.

For Android or iOS, see [`integration-android.md`](integration-android.md) /
[`integration-ios.md`](integration-ios.md) instead. A shorter, cross-platform overview lives at
[`integration.md`](integration.md).

## The mental model

Relay is a separate service your existing backend talks to, plus a client library your web app
depends on. It does **not** replace your login system, your user database, or your backend — it
has no idea who your users are until you tell it, on every request, via a short-lived token your
own backend mints. Three actors:

- **Your backend** — already exists, already authenticates your users. Gains one new endpoint:
  mint a Relay user token for whoever is signed in.
- **The Relay service** — one shared Communication Service, reachable at a URL you configure. Owns
  chat storage, presence, the realtime gateway, and brokers WebRTC calls. You don't write any of
  this.
- **`@relay/core` / `@relay/react`** — an npm package your existing app depends on. Talks to the
  Relay service directly (not through your backend, and not through your existing API) for
  everything after the initial token exchange.

Nothing here requires a rewrite. `<ChatWindow />` is a normal React component you place inside a
screen you already have; if you're on Vue/Svelte/vanilla JS, or already have your own chat UI, use
`@relay/core` directly and keep your UI.

## Step 1 — Create a project and get your keys

Once, against the Relay service (ask whoever runs it for the URL, or run `service/` yourself — see
[`service/README.md`](../service/README.md)):

```bash
curl -s -X POST https://relay.example.com/projects \
  -H 'content-type: application/json' \
  -d '{"name":"My App"}'
# {
#   "publicKey": "pk_5f1e…",   ← ships in your browser bundle, safe to embed
#   "secretKey": "sk_9c2a…"    ← backend only, shown exactly once — store it in your secrets manager now
# }
```

`publicKey` identifies your project to Relay and is fine in a browser bundle — it grants nothing
by itself. `secretKey` mints user tokens for anyone in your project; it must never reach client
code, a browser, or git. If you lose it, rotate it (`POST /projects/me/keys`) rather than trying to
recover it — it's stored as a hash.

## Step 2 — Add a token endpoint to your existing backend

This is the one piece of backend work, and the only place Relay touches your existing auth. Add a
route your already-signed-in users can call:

```
POST /api/relay-token          (your backend, behind your existing session/auth middleware)
  → looks up the current user from YOUR session (however you already do that)
  → calls Relay:  POST /users/token  { "externalId": "<your user id>", "displayName": "...", "avatarUrl": "..." }
                  Authorization: Bearer sk_9c2a…
  → returns { "userToken": "...", "expiresAt": "..." } to your browser
```

`externalId` is **your** user id — whatever you already use as a primary key. Relay never invents
its own identity; every `userId` your app sees on the wire (message senders, call participants,
presence) is that same id. Tokens expire in ~15 minutes; `@relay/core` calls this endpoint again
automatically when one expires, so this route just needs to exist and be fast.
`sample-apps/web-demo/token-server.mjs` is a complete, 60-line reference implementation of this
exact endpoint. Full details:
[`service/README.md`](../service/README.md#how-a-host-backend-mints-tokens).

## Step 3 — Add the SDK to your app

**Install**:

```bash
npm install @relay/react @relay/core react react-dom
```

`react` / `react-dom` 18.2+ or 19 are peer dependencies. On a non-React stack, install
`@relay/core` alone.

**Initialize + mount** — `RelayProvider` fits around whatever part of your existing component tree
should have chat, the same as any other context provider you already use (a router outlet, a
dashboard panel, a modal):

```tsx
import { RelayProvider, ChatWindow } from '@relay/react';
import '@relay/react/styles.css';

const fetchToken = () =>
  fetch('/api/relay-token', { method: 'POST' }).then((r) => r.json()).then((j) => j.userToken); // Step 2

function MyExistingChatPanel({ userId }: { userId: string }) {
  return (
    // key={userId}: switching users must create a fresh client, not reuse the old socket.
    <RelayProvider key={userId} config={{ baseUrl: 'https://relay.example.com', publicKey: 'pk_5f1e…', token: fetchToken }}>
      <div style={{ height: '100%' }}><ChatWindow /></div>
    </RelayProvider>
  );
}
```

`ChatWindow` fills its container, so give the parent a height.

**Headless**, if you already have chat UI, or you're on Vue/Svelte/vanilla — use the hooks (React)
or `@relay/core`'s store directly (anything else) over your own markup:

```tsx
// React
const { messages, send } = useMessages(conversationId);
```

```ts
// Framework-agnostic
import { createRelayClient } from '@relay/core';
const relay = createRelayClient({ baseUrl: '...', publicKey: 'pk_5f1e…', token: fetchToken });
await relay.connect();
relay.chat.subscribe(() => render(relay.chat.getSnapshot()));
```

## Step 4 — Calling (1:1 and group, one API)

`<ChatWindow />` already shows call buttons in every thread header and mounts the call overlay
(incoming banner, full-screen view, errors) by default — pass `calls={false}` to turn that off. One
call-start API handles both 1:1 and group calls — the SDK looks at the conversation you pass it and
shows the right layout itself:

```tsx
import { useCall, CallOverlay } from '@relay/react';

function Header({ conversation }) {
  const { call, start } = useCall();            // call: null | { phase, callId, peerId, type, … }
  return <button disabled={!!call} onClick={() => start(conversation, 'video')}>Video call</button>;
}
// somewhere once, inside <RelayProvider> — only needed if you set calls={false} above and want your own overlay:
<CallOverlay />
```

Group calls (up to 6 participants) render as a grid — 2 stacked, 3 as 2-over-1, up to 6 as three
rows of 2 — with per-tile name badges, mute indicators, tap-to-fullscreen, and a per-participant
"mute for me" control that only affects what you hear locally, without touching what anyone else
hears. Media is peer-to-peer WebRTC; the service hands out TURN credentials automatically once
configured for your project.

## Step 5 — Wire your own "add participants" picker

Group chats need a way to pick who to add. Relay does **not** ship a directory of your users — it
has no idea who they are beyond the ids that show up in tokens — so this is always a callback into
your own app. Two ways to hook it in:

**Your own picker UI entirely** (fastest to wire, if you already have a contacts/user list
screen) — return the ids yourself, in whatever UI you want:

```tsx
<ChatWindow onPickGroupMembers={async () => await showMyOwnContactPicker()} />
```

**Relay's built-in picker**, backed by your directory — you supply a search function, Relay
renders the search/list/selection UI itself:

```tsx
<ChatWindow onSearchPeople={async (query) => myUserDirectory.search(query)} />
```

`onSearchPeople?: (query: string) => Promise<RelayUser[]>` is called once with an empty query when
the screen opens (a starting list) and again as the user types; return `RelayUser`s (id, display
name, avatar, online status) from whatever directory your app already has. If you supply both,
`onSearchPeople` wins; if you supply neither, "Add people" is simply hidden.

## Step 6 — Make it look like your app

Every color, most icons, and text sizing are overridable, with defaults equal to Relay's own look
— so this step is optional, and doing nothing is a supported choice. Colors and fonts are plain CSS
variables; icons are a prop, since swapping which glyph renders is something CSS can't do.

```css
:root {
  --relay-accent: #7c3aed;
  --relay-bubble-me: #7c3aed;
  --relay-font: Inter, system-ui, sans-serif;
  --relay-font-scale: 1.1;
}
```

```tsx
<RelayProvider config={config} icons={{ micOn: <MyMicIcon />, callEnd: <MyHangUpIcon /> }}>
```

This covers chat *and* call screens uniformly — the call screen used to be the one place still
hardcoded to Relay's own dark-navy palette; its colors now read from the same `--relay-*` variables
as the rest (`--relay-call-scrim-start`/`-end` for the backdrop, `--relay-online`/`--relay-danger`
for answer/decline). Full variable list, dark mode (`data-theme="dark"`), and every `relay-*` class
name for deeper CSS overrides: [`packages/web/react/README.md#theming`](../packages/web/react/README.md#theming).

## Security checklist before you ship

- `secretKey` lives only on your backend (Step 2), never in client code or git.
- Set a CORS origin allowlist for your project (`PATCH /projects/me/settings`) so only your own
  origins can open a connection with your public key.
- Decide who can message whom in **your own backend**, before minting a token or before your UI
  offers a conversation — Relay has no friend graph or block list of its own, so any two users in
  the same project can otherwise open a chat with each other.

## What you still own

Relay doesn't do: authentication (you already have this — it just needs a way to mint tokens for
whoever is signed in), a user directory or search (Step 5 is how your existing one plugs in), or
push certificate/key management beyond what you configure once in the admin panel (web calling
works while a tab is open; there is no web-push wake-up for calls, unlike iOS/Android).

## Where to go next

- Full API reference: [`packages/web/react/README.md`](../packages/web/react/README.md) (React) /
  headless `@relay/core` reference in [`packages/web/README.md`](../packages/web/README.md).
- Backend/token details and every security note: [`service/README.md`](../service/README.md).
- The reference integration is a real, running app: `sample-apps/web-demo`.
