Integrating Relay into an existing Android app
This is the complete path for a team that already has an Android 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/android/README.md has the full API reference if you
need more than what's shown here.
For iOS or web, see integration-ios.md /
integration-web.md instead. A shorter, cross-platform overview lives at
integration.md.
The mental model
Relay is a separate service your existing backend talks to, plus a client library your Android 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.
relay-core/relay-ui/relay-call— Kotlin/Compose modules 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. RelayChat and RelayCallOverlay are ordinary composables 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):
curl -s -X POST https://relay.example.com/projects \
-H 'content-type: application/json' \
-d '{"name":"My App"}'
# {
# "publicKey": "pk_5f1e…", ← ships inside your Android 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's APK — it grants
nothing by itself. secretKey mints user tokens for anyone in your project; it must never reach
client code, an APK, 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 Android 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; 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 (framework-agnostic — the shape is identical regardless of which client calls it).
Full details: service/README.md.
Step 3 — Add the SDK to your app
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's
build.gradle.kts:
implementation(project(":relay-ui")) // chat UI — pulls in relay-core
implementation(project(":relay-call")) // add only if you want calling too
Requires minSdk 26, Kotlin 2.1, Compose BOM 2025.01.
Initialize — once per signed-in user, next to wherever your app already builds its other network clients:
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
(onStart/after login), and relay.goToBackground() / relay.disconnect() at the matching
background/sign-out points — 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 already hosts your chat/conversation list today:
setContent {
MaterialTheme { // your app's existing theme — RelayChat inherits its colors/type
RelayChat(client = relay) // list → thread → composer, as one composable
}
}
RelayChat reads the ambient MaterialTheme.colorScheme/.typography your app already installs,
so dropping it into an existing themed app already gets you most of the way to matching your
brand — see Theming (Step 6) for the parts that still need an explicit override.
Headless, if you already have chat UI and only want the data layer:
relay.connect()
relay.chat.loadConversations()
val convo = relay.chat.openConversation("user-42") // your own user id
relay.chat.sendMessage(convo.id, "hello")
relay.chat.state.collect { snapshot -> /* your own UI reads conversations, threads, typing, presence */ }
Step 4 — Calling (1:1 and group, one API)
Calling is a separate module (relay-call) 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:
val calls = CallCenter(context, relay) // once per client
Box { RelayChat(relay); RelayCallOverlay(calls) } // banner + full-screen UI, 1:1 or group automatically
// in a thread header:
CallButtons(calls, conversation)
// or trigger it yourself:
calls.start(conversation, CallType.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. Request RECORD_AUDIO (and CAMERA for video) before starting a call.
Ringing when the app is fully closed (FCM)
Once your project has FCM credentials (set from the admin panel), the service sends a high-priority FCM data message for incoming calls (and a cancel when the ring ends):
// Application.onCreate()
RelayCallHost.configure(this, MainActivity::class.java) { RelayClient(relayConfig) }
// use RelayCallHost.client() / RelayCallHost.callCenter() in your UI so state is shared
// your FirebaseMessagingService
override fun onMessageReceived(m: RemoteMessage) {
if (RelayCallPush.onMessage(this, m.data)) return // Relay handled a call push
// ... your own notifications
}
override fun onNewToken(token: String) = RelayCallPush.onNewToken(token)
// after sign-in: RelayCallPush.registerCurrentToken(); on sign-out: RelayCallPush.unregisterCurrentToken()
RelayCallService rings, vibrates, shows a full-screen incoming-call notification with
Answer/Decline, and keeps the process alive for the length of the call. Ask for
POST_NOTIFICATIONS on API 33+. Chat messages arrive as ordinary FCM notification messages with
data.relay = "chat_message" and conversationId 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:
MessageThread(client = relay, conversationId = id, onPickGroupMembers = { showMyOwnContactPicker() })
Relay's built-in picker, backed by your directory — you supply a search function, Relay renders the search/list/selection UI itself:
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 RelayUsers (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 (or just wrapping in your existing MaterialTheme,
per Step 3) is a fully supported choice.
RelayTheme(
// .copy() on the Material-derived default lets you override just the colors you care about
colors = RelayColors.fromMaterialTheme(MaterialTheme.colorScheme).copy(
accent = MyBrandBlue,
danger = MyBrandRed,
),
icons = RelayIcons(micOn = myMicIcon, callEnd = myHangUpIcon),
typography = RelayTypography(fontFamily = myBrandFont, fontScale = 1.1f),
) {
RelayChat(relay)
RelayCallOverlay(calls)
}
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
colors/icons/typography as the rest. colors, icons, and typography are each
independently optional — override one color without supplying the rest, and everything else keeps
Relay's default (which itself already derives from your app's ambient MaterialTheme if you don't
touch it at all).
Attachments & link previews
MessageComposer's attach button (Camera / Gallery / File) needs no permission at all for
Gallery or File — both are out-of-process pickers. Camera is the one exception: add
<uses-permission android:name="android.permission.CAMERA"/> to your own manifest if you want it;
MessageComposer requests it at runtime the first time it's tapped. A message with an http(s)://
link gets an automatic preview card — no setup needed, fetched server-side.
Security checklist before you ship
secretKeylives only on your backend (Step 2), never in client code or git.- Pass
packageId(BuildConfig.APPLICATION_ID) inRelayConfigif you've configured an Android package allowlist for your project in the admin panel; harmless to omit otherwise — the SDK sends it asX-App-Package-Idon every request. - 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 module automatically, there's nothing to override there.
Where to go next
- Full API reference:
packages/android/README.md. - Backend/token details and every security note:
service/README.md. - Push notification credentials (FCM) setup:
push-credentials.md. - The reference integration is a real, running app:
packages/android/app-demo.