Rheo documentation
Developer Guide

Overview

Shared runtime model for Rheo mobile SDKs — channel resolve, events, terminal snapshots, and platform guides.

Purpose

Rheo ships four first-party mobile SDKs (Expo, bare React Native, Flutter, and SwiftUI — Flutter and SwiftUI coming soon). Each resolves flows by channel, runs the same flow state machine from @getrheo/flow-runtime, renders dashboard-authored screens, batches analytics, and emits terminal snapshots when a run completes or is abandoned.

Install exactly one React Native flavor — never both @getrheo/react-native-expo and @getrheo/react-native-bare.

Mental model

ConceptWhat it means
Publishable keyob_pk_test_* or ob_pk_live_* — identifies your app and environment. Create keys in App settings → SDK credentials.
ChannelPublic id (ch_test_… / ch_live_…) — pass to <Flow channelId={…} /> or FlowView(channelId:). Rheo decides which flow revision (or experiment arm) to serve.
ResolvePOST /v1/sdk/resolve with Authorization: Bearer <key> and X-Rheo-Channel: <channelId>. Returns the manifest, branding, mediaMap, and assignment metadata.
ManifestJSON flow definition — screens, layers, decisions, external surfaces.
EventsAutomatic analytics (flow_started, step_viewed, …) batched to POST /v1/sdk/events. See Event catalog.
Terminal snapshotVersioned FlowTerminalSnapshot on completion or abandonment — JSON.stringify for your API, CRM, or LLM.

How resolve works

  1. The SDK sends your publishable key and channel id (plus identity: userId, locale, etc.).
  2. Rheo returns the manifest — either the channel's pinned version or an experiment arm bucketed by stable userId.
  3. Response includes flowId, versionId, assignmentVersion, experimentId / variantId when applicable — attached to every analytics event.
  4. mediaMap maps mediaAssetId → CDN URL for image and Lottie layers.

Caching

Resolve responses carry an ETag ("{assignmentVersion}-{versionId}"). The SDK caches per channel + locale on device (AsyncStorage on React Native). If-None-Match can return 304 and reuse the cached manifest until the assignment changes.

Prefetch

Warm the cache before showing a flow to skip the loading spinner. All SDKs support:

  1. Provider prefetch on RheoProvider mount ("all" or specific channel ids).
  2. Imperative prefetch from navigation listeners or button handlers.
  3. On-demand resolve when a flow mounts — owns error and retry UI.

Prefetch is best-effort and silent — it never emits flow_started or experiment exposure.

Platform-specific APIs: Expo, bare React Native.

Diagnostics and logging

All mobile SDKs default to silent console output in production. Opt in with the top-level logLevel prop on RheoProvider:

logLevelEmits
silent (default)Nothing
warnEvent transport failures, RevenueCat integration warnings
debugAbove plus dev-only diagnostics (manifest dump, prefetch misuse hints) — requires a dev build

React Native: logLevel="warn" on <RheoProvider />. Kotlin will use the same levels from @getrheo/contracts.

Production apps should omit logLevel unless you are actively debugging transport issues.

Events and batching

Events queue in memory and POST to /v1/sdk/events every ~5 seconds, when the buffer hits 500 events, or immediately on flow_completed / flow_abandoned. Multi-channel batches split into separate POSTs (one X-Rheo-Channel per batch).

The queue is in-memory only — force-quit or offline periods can drop non-terminal events. Terminal events prioritize immediate flush.

Full event reference: Event catalog.

Terminal snapshot

Callbacks:

SDKCompletedAbandoned
React NativeonFlowCompleted on <Flow /> / useFlowonFlowAbandoned

FlowTerminalSnapshot (schemaVersion: 1) fields:

FieldContents
terminalcompleted | abandoned
occurredAtISO timestamp
correlationchannelId, flowId, versionId, assignmentVersion, environment, experimentId, variantId
subjectappUserId, optional customUserId, sessionId
deviceplatform, locale, optional appVersion, customProperties
answersNormalized field map (auth secrets omitted)
traitsMerged sdkAttributes + attribution at terminal time

Optional flags: includeManifestInTerminalPayload, includePathInTerminalPayload, includeAnswerDetailInTerminalPayload.

Call abandon() (RN useFlow) to transition to abandoned explicitly. Unmounting mid-flow also enqueues flow_abandoned when not already terminal.

Layer kinds

All SDKs render these manifest layer kinds:

stack, text, image, lottie, video, icon, button, back_button, progress, loader, counter, single_choice, multiple_choice, text_input, scale_input, oauth_login, email_password_auth, carousel, hyperlink, checkbox

Auth layers require host handlers — Authentication layers.

External surfaces (RevenueCat paywalls) are graph nodes, not layers — RevenueCat integration.

Production defaults

Default API host: https://api.getrheo.io (RHEO_DEFAULT_SDK_API_BASE_URL in @getrheo/contracts). Omit apiBaseUrl / apiBaseURL in production unless you self-host.

Match key environment to channel environmentob_pk_test_* with ch_test_*, ob_pk_live_* with ch_live_*.

Troubleshooting

Blank or stale flow

  • Validate publishable key, channel public id, and environment alignment.
  • Confirm the channel has an assigned published flow.
  • Use a stable userId for experiment bucketing.
  • After publish, cached manifests persist until assignmentVersion changes.

Resolve errors

SituationMeaning
Channel missingNo X-Rheo-Channel header
Channel not foundUnknown id or key/channel environment mismatch
Channel unassignedNo flow or experiment pinned
Channel archivedUnarchive in dashboard or switch channel

Use fallback on <Flow /> / FlowView for host-owned recovery UI.

Indie MAU cap / billing suspension

Indie workspaces pause SDK traffic above 400 live MAU per UTC month. Past-due paid subscriptions block after grace. See Billing & subscriptions.

Local development API URL

RuntimeHost loopback
iOS Simulatorhttp://localhost:4000
Android Emulatorhttp://10.0.2.2:4000

Set apiBaseUrl / apiBaseURL in provider config during local dev.