# Integrating Relay into an existing app

This is the document for a team that already has an app — Android, iOS, or web — and wants to
add chat + calling to it, rather than build a new app around the SDK. It walks the whole path:
getting a project, wiring your backend, dropping the client into your existing screens, and
matching it to your app's own look. Each platform's package also has its own README with the full
API reference (linked at the bottom of each section here); this document is the order to read
things in, not a replacement for them.

If you want the fastest possible look at working code instead of reading this first, the
reference integration for each platform is a real, running app: `sample-apps/web-demo`,
`packages/ios/app-demo`, `packages/android/app-demo`.

## The mental model

Relay is a separate service your existing backend talks to, plus a client SDK your existing app
embeds. 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
  (self-hosted or the one your team runs). Owns chat storage, presence, the realtime gateway, and
  brokers WebRTC calls. You don't write any of this.
- **The Relay client SDK** — a package/library your existing app depends on. Talks to the Relay
  service directly (not through your backend) for everything after the initial token exchange.

Nothing here requires a rewrite. The chat/call screens are components/views/composables you mount
inside screens you already have; if you already have your own chat UI, skip straight to the
headless client 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`):

```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 client apps, 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 mobile app binary or 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 client
```

`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, so wiring a user picker or displaying a name is just reading a field
you already have. Tokens expire in ~15 minutes; every client SDK below calls this endpoint again
automatically when one expires, so this route just needs to exist and be fast — nothing to poll or
schedule. `sample-apps/web-demo/token-server.mjs` is a complete, 60-line reference implementation
of this exact endpoint if you want a working example to copy from. Full details:
[`service/README.md`](../service/README.md#how-a-host-backend-mints-tokens).

## Step 3 — Add the client to your app

Pick your platform. Each shows the drop-in path (their prebuilt chat/call UI, for when you don't
already have one) and the headless path (just the data layer, for when you do).

### Android

Full standalone guide (everything below, plus attachments, link previews, and more theming
detail): [`integration-android.md`](integration-android.md).

**Install** — until the modules are published, add them as a composite build or
`includeBuild("path/to/relay-sdk/packages/android")`, then in your existing app module:

```kotlin
implementation(project(":relay-ui"))     // chat UI — pulls in relay-core
implementation(project(":relay-call"))   // add only if you want calling too
```

**Initialize** — once per signed-in user, next to wherever your app already builds its other
network clients:

```kotlin
val relay = RelayClient(RelayConfig(
    baseUrl = "https://relay.example.com",
    publicKey = "pk_5f1e…",
    tokenProvider = { myExistingBackendClient.fetchRelayToken() },  // calls Step 2's endpoint
    packageId = BuildConfig.APPLICATION_ID,                        // optional, see Security below
))
```

Call `relay.connect()` where your app already handles "became foreground" / sign-in, and
`relay.goToBackground()` / `relay.disconnect()` at the matching background/sign-out points — the
store resyncs by itself on every reconnect, so there's no missed-message bookkeeping to add.

**Mount the UI** — inside whatever screen already hosts your chat/conversation list today:

```kotlin
RelayChat(client = relay)   // list → thread → composer, as one composable
```

Wrap it in your existing `MaterialTheme { … }` and it already inherits your colors and type scale
— see Theming below for the parts that need an explicit override.

**Headless**, if you already have chat UI and only want the data layer:

```kotlin
relay.chat.state.collect { snapshot -> /* your own UI reads conversations, threads, typing, presence */ }
relay.chat.sendMessage(convo.id, "hello")
```

Full reference: [`packages/android/README.md`](../packages/android/README.md).

### iOS

Full standalone guide (everything below, plus attachments, link previews, and more theming
detail): [`integration-ios.md`](integration-ios.md).

**Install** — Xcode → *File → Add Package Dependencies…* → this repo's URL (or a local path to
`packages/ios`) → add the `RelayUI` product (add `RelayCall` too if you want calling).

**Initialize** — once per signed-in user:

```swift
let relay = RelayClient(config: RelayConfig(
    baseURL: URL(string: "https://relay.example.com")!,
    publicKey: "pk_5f1e…",
    tokenProvider: { try await myExistingBackendClient.fetchRelayToken() }   // calls Step 2's endpoint
))
```

Call `try await relay.connect()` / `await relay.goToBackground()` at your existing
foreground/background transitions (`scenePhase`, or wherever your app already reacts to those).

**Mount the UI** — inside whatever screen/tab already hosts chat:

```swift
RelayChatView(client: relay)
```

It's a normal `View` — push it, present it, or embed it in a `TabView` the same as anything else
already in your app.

**Headless**, if you already have chat UI:

```swift
try await relay.chat.loadConversations()
// relay.chat is @Observable — read relay.chat.conversations / .thread(id).messages directly in SwiftUI
try await relay.chat.sendMessage(convo.id, text: "hello")
```

Full reference: [`packages/ios/README.md`](../packages/ios/README.md).

### Web

Full standalone guide (everything below, plus more theming detail):
[`integration-web.md`](integration-web.md).

**Install**:

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

**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 (
    <RelayProvider key={userId} config={{ baseUrl: 'https://relay.example.com', publicKey: 'pk_5f1e…', token: fetchToken }}>
      <div style={{ height: '100%' }}><ChatWindow /></div>
    </RelayProvider>
  );
}
```

`key={userId}` matters if your app can switch signed-in users without a full page reload — it
forces a fresh client instead of reusing a stale socket.

**Headless**, if you already have chat UI — use the hooks over your own markup instead of
`<ChatWindow>`:

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

Full reference: [`packages/web/react/README.md`](../packages/web/react/README.md).

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

Calling is a separate module on Android/iOS (so a chat-only integration doesn't pull in WebRTC) and
a flag on web. One call-start API handles both 1:1 and group — the SDK looks at the conversation
you pass it and shows the right layout itself, so there's no separate "group call" method to learn:

```kotlin
// Android — once
val calls = CallCenter(context, relay)
Box { RelayChat(relay); RelayCallOverlay(calls) }     // banner + full-screen UI, 1:1 or group automatically
// from a thread header:
calls.start(conversation, CallType.VIDEO)
```

```swift
// iOS — once
let calls = CallCenter(client: relay)
RelayChatView(client: relay).overlay { RelayCallOverlay(center: calls) }
calls.start(conversation: conversation, type: .video)
```

```tsx
// Web — CallOverlay + call buttons are already inside <ChatWindow /> by default; pass calls={false} to opt out
import { useCall, CallOverlay } from '@relay/react';
const { start } = useCall();
start(conversation, 'video');
```

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. Ringing while your app is fully
closed needs one more step per platform — FCM data messages on Android, PushKit VoIP pushes on
iOS — covered in each platform's README under "Ringing when the app is closed"; it needs APNs/FCM
credentials added to your project from the admin panel first.

## 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, from simplest to most integrated:

**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.

```kotlin
MessageThread(client = relay, conversationId = id, onPickGroupMembers = { showMyOwnContactPicker() })
```

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

```kotlin
MessageThread(client = relay, conversationId = id, onSearchPeople = { query -> myUserDirectory.search(query) })
```

`onSearchPeople` 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 existing app already has — an internal API, a cached employee list,
whatever you'd use to build a picker yourself. If you supply both, `onSearchPeople` wins; if you
supply neither, "Add people" is simply hidden. Same shape on iOS
(`onSearchPeople: (String) async -> [RelayUser]`) and web (`onSearchPeople` prop on `ChatWindow`).

## 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.

```kotlin
// Android — wrap anywhere above your Relay composables; every property you don't override keeps
// today's look, either via .copy() on the Material-derived default or by leaving the whole
// parameter null (colors/icons/typography are each independently optional)
RelayTheme(
    colors = RelayColors.fromMaterialTheme(MaterialTheme.colorScheme).copy(accent = MyBrandBlue, danger = MyBrandRed),
    icons = RelayIcons(micOn = myMicIcon),
    typography = RelayTypography(fontFamily = myBrandFont, fontScale = 1.1f),
) { RelayChat(relay); RelayCallOverlay(calls) }
```

```swift
// iOS
var theme = RelayTheme()
theme.accent = .myBrandBlue
theme.fontScale = 1.1
RelayChatView(client: relay).relayTheme(theme).relayTypography(theme)
```

```css
/* Web — plain CSS variables, no component API needed */
:root {
  --relay-accent: #7c3aed;
  --relay-bubble-me: #7c3aed;
  --relay-font: Inter, system-ui, sans-serif;
  --relay-font-scale: 1.1;
}
```

```tsx
// Web icons — a prop, since swapping a glyph is a render concern CSS can't reach
<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 look on every platform; it now reads from the same
colors/icons/typography as the rest. Every property is independently optional — override one
color without supplying the rest, and everything else keeps Relay's default.

## Security checklist before you ship

- `secretKey` lives only on your backend (Step 2), never in client code or git.
- Pass `packageId` (Android, your `applicationId`) — iOS sends its bundle id automatically — if
  you've configured a package/bundle allowlist for your project in the admin panel; harmless to
  omit otherwise.
- Set a CORS origin allowlist for your project if you're on web (`PATCH /projects/me/settings`);
  native apps aren't affected by this.
- 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), push
certificate/key management beyond what you configure once in the admin panel, or app icons/branding
on iOS/Android (those come from your app target automatically — there's nothing to override there).

## Where to go next

- Full API reference per platform: [`packages/android/README.md`](../packages/android/README.md),
  [`packages/ios/README.md`](../packages/ios/README.md),
  [`packages/web/react/README.md`](../packages/web/react/README.md).
- Backend/token details and every security note: [`service/README.md`](../service/README.md).
- Push notification credentials (FCM/APNs) setup: [`docs/push-credentials.md`](push-credentials.md).
- Protocol reference (if you're building a client this repo doesn't have yet):
  [`protocol/openapi.yaml`](../protocol/openapi.yaml), [`protocol/events.md`](../protocol/events.md).
