# Integrating Relay into an existing iOS app

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

For Android or web, see [`integration-android.md`](integration-android.md) /
[`integration-web.md`](integration-web.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 iOS 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. Owns
  chat storage, presence, the realtime gateway, and brokers WebRTC calls. You don't write any of
  this.
- **`RelayCore`/`RelayUI`/`RelayCall`** — Swift packages your existing app depends on. Talk to the
  Relay service directly (not through your backend) for everything after the initial token
  exchange.

Nothing here requires a rewrite. `RelayChatView` and `RelayCallOverlay` are ordinary SwiftUI views
you place inside a screen you already have; if you already have your own chat UI, skip to
Headless below and keep it.

## 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 inside your iOS app, 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 baked into your app — it grants nothing
by itself. `secretKey` mints user tokens for anyone in your project; it must never reach client
code, an app binary, 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 iOS 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. Tokens expire in ~15 minutes; `RelayCore` 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 (framework-agnostic — the shape is identical regardless of which client calls it).
Full details: [`service/README.md`](../service/README.md#how-a-host-backend-mints-tokens).

## Step 3 — Add the SDK to your app

**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 — it's a
separate product so a chat-only app skips the WebRTC binary). Requires iOS 17, Xcode 15+.

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

```swift
import RelayUI

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 (e.g. `scenePhase`, or wherever your app already reacts to
those) — the store resyncs by itself on every reconnect (the server never replays missed events),
so there's no missed-message bookkeeping to add yourself.

**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. It connects on appear and shows the conversation list → thread → composer
with presence, typing, read receipts, retries, edits, replies, and media.

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

```swift
try await relay.connect()
try await relay.chat.loadConversations()
let convo = try await relay.chat.openConversation(with: "user-42")   // your own user id
try await relay.chat.sendMessage(convo.id, text: "hello")
// relay.chat is @Observable — read relay.chat.conversations / .thread(id).messages directly in SwiftUI
```

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

Calling is a separate package (`RelayCall`) so a chat-only integration doesn't pull in WebRTC. 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:

```swift
import RelayCall

let calls = CallCenter(client: relay)               // once; uses CallKit on iOS
RelayChatView(client: relay)
    .overlay { RelayCallOverlay(center: calls) }     // full-screen call UI + errors, 1:1 or group automatically
// From a thread header:
calls.start(conversation: conversation, type: .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, without touching what anyone else
hears. Add the `voip` and `audio` background modes to your Info.plist. Incoming calls ring through
CallKit while the app is running.

### Ringing when the app is fully closed (PushKit)

Once your project has APNs credentials (.p8 key, team id, key id, bundle id — set from the admin
panel), the service sends a VoIP push for every incoming call and a cancel when the ring ends:

```swift
// AppDelegate.didFinishLaunching / @main App init
let push = RelayPushRegistry(client: relay, calls: calls)
push.start()                                   // registers the VoIP token with the service
// application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
push.registerAlertToken(deviceToken)           // chat alerts through your normal APNs token
// after sign-in: push.resync()     on sign-out: await push.unregisterAll()
```

`RelayPushRegistry` reports the call to CallKit synchronously inside the PushKit callback (Apple's
rule for VoIP pushes), starts the gateway connection so answering is instant, and ignores the
socket's duplicate `call_invite`. Chat alert pushes carry `relay = "chat_message"`,
`conversationId` and `messageId` for deep-linking.

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

```swift
MessageThreadView(conversationId: id, onPickGroupMembers: { await showMyOwnContactPicker() })
```

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

```swift
MessageThreadView(conversationId: id, onSearchPeople: { query in await myUserDirectory.search(query) })
```

`onSearchPeople: (String) async -> [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:

```swift
var theme = RelayTheme()
theme.accent = .myBrandBlue
theme.danger = .myBrandRed
theme.fontFamily = "MyBrandFont-Regular"
theme.fontScale = 1.1

RelayChatView(client: relay)
    .relayTheme(theme)
    .relayTypography(theme)
```

For icons:

```swift
var icons = RelayIcons()
icons.micOn = Image("my_mic_icon")
icons.callEnd = Image("my_hangup_icon")
RelayChatView(client: relay).relayIcons(icons)
```

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 regardless of your app's theme; it now reads from the same
`RelayTheme`/`RelayIcons` as the rest (apply the same modifiers around `RelayCallOverlay`). Every
property is independently optional — override one color without supplying the rest, and
everything else keeps Relay's default.

## Attachments & link previews

`MessageComposerView`'s attach button (Camera / Photo Library / File) needs **no permission at
all** for Photo Library or File — both are out-of-process pickers. **Camera is the one
exception**: add `NSCameraUsageDescription` to your Info.plist (the same key `RelayCall` already
needs for video calls). A message with an `http(s)://` link gets an automatic preview card — no
setup needed, fetched server-side.

## Security checklist before you ship

- `secretKey` lives only on your backend (Step 2), never in client code or git.
- The SDK sends `X-App-Bundle-Id` automatically on every request — no configuration needed. If
  you've configured an iOS bundle ID allowlist for your project in the admin panel, a request from
  an app not on that list is rejected; harmless to ignore otherwise.
- 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 your app's icon —
that comes from your own app target automatically, there's nothing to override there.

## Where to go next

- Full API reference: [`packages/ios/README.md`](../packages/ios/README.md).
- Backend/token details and every security note: [`service/README.md`](../service/README.md).
- Push notification credentials (APNs) setup: [`push-credentials.md`](push-credentials.md).
- There is no sample Xcode project — the snippets on this page and in
  [`packages/ios/README.md`](../packages/ios/README.md) are the integration.
