Use this module after intent classification, product selection (Chat / Video / Feeds / any combination), and the Project signals probe from SKILL.md. Run credentials.md before writing connected Chat, Video, or Feeds code or creating requested demo data.
Start by understanding what kind of React Native project is in front of you:
EMPTY_CWD-> valid Track A target; scaffold in the current directory or a named child directory- no
package.json, no Expo config, and non-empty directory -> ask before creating a child app package.jsonwithexpoorapp.json/app.config.*-> Expo lanepackage.jsonwithreact-nativeandios/+android/-> RN CLI laneapp/_layout.*orexpo-router-> Expo Router@react-navigation/*-> React Navigationbabel.config.js-> required place for the Reanimated/Worklets plugin on RN CLI, and on Expo below SDK 54. On Expo SDK 54+ its absence is correct — do not create one (§5 > Babel plugin)
Also note any installed Stream packages:
stream-chat-react-nativeorstream-chat-expo-> Chat already present@stream-io/video-react-native-sdk-> Video already present@stream-io/react-native-callingx,@stream-io/react-native-webrtc-> Video peers already present@stream-io/feeds-react-native-sdk-> Feeds already present- two or more Stream RN packages present -> nest the providers; see
RULES.mdandsdk.md> Provider tree
For Track A, default to Expo if the user did not specify Expo vs RN CLI. Keep the new-app guidance minimal: app creation, Stream package install, root providers, auth/token flow, first Chat / Video / Feeds screen, and verification. Do not explain full React Native, Expo, Xcode, Android Studio, simulator, device, or account setup.
Use this when the user asks for a brand-new Chat, Video, or Feeds RN app (or any combination), or the workspace is empty and Track A applies.
- If the user provided an app name, use it as the directory name.
- If the current directory is empty and the user asked to use it, scaffold into
.. - If the current directory is non-empty, create a child directory from the requested app name.
- If no app name can be inferred in a non-empty directory, ask one short question for the app directory name.
Expo default lane (replace MyApp with the target directory):
npx create-expo-app@latest MyApp
cd MyAppMandatory reanimated/worklets floor (Expo SDK 57). create-expo-app on SDK 57 pins react-native-reanimated@4.5.0 + react-native-worklets@0.10.0, a known-crash pair (crash is on worklet/animation paths, not at boot — a clean launch does NOT prove it's fixed). The Chat-Expo install block below ends with an explicit bump past that pin; it is not optional. After install, assert the versions (see §9) — never rely on remembering to bump, because npx expo install re-pins the crash version.
RN CLI lane (only when the user asks for RN CLI or requirements point there):
npx @react-native-community/cli@latest init MyApp
cd MyAppPick the product(s) confirmed in Step 0 of SKILL.md. Install one block per product in scope (Chat, Video, Feeds, or any combination).
Chat - Expo:
npm view stream-chat-expo version dist-tags --json
npx expo install stream-chat-expo@latest @react-native-community/netinfo expo-dev-client expo-image-manipulator react-native-gesture-handler react-native-reanimated react-native-svg react-native-teleport
npx expo install react-native-safe-area-context
# MANDATORY on Expo SDK 57: bump past the crash-prone bundled pin (4.5.0/0.10.0).
# expo install re-pins the bad version above, so this override must come AFTER it.
npx expo install react-native-reanimated@4.5.2 react-native-worklets@0.10.2
npx expo prebuildChat - RN CLI:
npm view stream-chat-react-native version dist-tags --json
npm install stream-chat-react-native@latest @react-native-community/netinfo react-native-gesture-handler react-native-reanimated react-native-teleport react-native-worklets react-native-svg
npm install react-native-safe-area-context
npx pod-installVideo - Expo:
npm view @stream-io/video-react-native-sdk version dist-tags --json
npx expo install @stream-io/video-react-native-sdk \
@stream-io/react-native-webrtc \
@config-plugins/react-native-webrtc \
react-native-svg \
@react-native-community/netinfo \
react-native-safe-area-context \
expo-build-properties
# recommended (animated floating-participant tile; matches Stream's sample apps):
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handlerAdd @stream-io/video-react-native-sdk and @config-plugins/react-native-webrtc to app.json plugins. On Expo SDK 54+ do not add a Babel plugin or a babel.config.js — babel-preset-expo appends react-native-worklets/plugin itself (§5 > Babel plugin). Also enable Android edge-to-edge under android in app.json ("edgeToEdgeEnabled": true; default-on Expo SDK 54+). Then npx expo prebuild --clean.
Video - RN CLI:
npm view @stream-io/video-react-native-sdk version dist-tags --json
npm install @stream-io/video-react-native-sdk
npm install @stream-io/react-native-webrtc react-native-svg @react-native-community/netinfo
npm install react-native-safe-area-context
# recommended (animated floating-participant tile; matches Stream's sample apps):
npm install react-native-reanimated react-native-worklets react-native-gesture-handler
npx pod-installAndroid edge-to-edge (pick the branch that matches the host RN version):
- RN 0.81+: set
edgeToEdgeEnabled=trueinandroid/gradle.properties. The RN Gradle plugin handles the rest - noreact-native-edge-to-edgeinstall, nostyles.xmledit. - Older RN CLI:
npm install react-native-edge-to-edgeand inherit aTheme.EdgeToEdgevariant (e.g.Theme.EdgeToEdge.Material3) inandroid/app/src/main/res/values/styles.xml. Add<item name="enforceNavigationBarContrast">false</item>for a fully transparent nav bar.
If you installed the animation peers, add react-native-worklets/plugin as the last Babel plugin. Set minSdkVersion = 24 in android/build.gradle and add Java 8 source compatibility in android/app/build.gradle. Add camera/microphone usage descriptions to Info.plist and camera/audio permissions to AndroidManifest.xml. In android/app/src/main/res/values/styles.xml, set the app theme parent to a Theme.EdgeToEdge variant (e.g. Theme.EdgeToEdge.Material3) so Android draws under the system bars.
Feeds - Expo:
npm view @stream-io/feeds-react-native-sdk version dist-tags --json
npx expo install @stream-io/feeds-react-native-sdk @react-native-community/netinfo
npx expo install react-native-safe-area-contextFeeds has no Reanimated, gesture-handler, SVG, or worklets requirement. No Expo config plugin entries are needed. If the app is already in the Expo dev-client lane (because Chat or Video is also installed), keep that lane; an Expo Feeds-only app can stay on the managed workflow.
Feeds - RN CLI:
npm view @stream-io/feeds-react-native-sdk version dist-tags --json
npm install @stream-io/feeds-react-native-sdk @react-native-community/netinfo
npm install react-native-safe-area-context
npx pod-installIf the new app uses yarn or pnpm, translate package-manager commands without changing package names. Run pods after native dependency changes in RN CLI apps. Use npx expo install for Expo dependencies so versions match the Expo SDK.
The bundled blueprints (App Provider, Navigation Shell, Channel List, Channel Screen, Home/Join-or-Start, Active Call, Ringing) import from @react-navigation/* or expo-router. Install the matching stack before generating screens or imports will break the first build:
- RN CLI (no navigation by default) - install React Navigation explicitly:
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screens
# react-native-safe-area-context is already installed above; reuse it
npx pod-install-
Expo, default template (recommended) -
npx create-expo-app@latestalready scaffolds Expo Router underapp/. Skip the React Navigation install and use the Expo Router branch of the Navigation Shell blueprint (andapp/*.tsxfiles for routes). Both React Navigation and Expo Router branches are documented in the blueprints - pick the one that matches the chosen template. -
Expo Router on SDK 56+ — never install
@react-navigation/*. Expo Router ships its own navigation runtime from SDK 56 onward and Metro fails to bundle if any@react-navigation/*package is present (seeRULES.md> Expo Router SDK 56+ — no React Navigation). The Chat blueprints'useHeaderHeight()pattern from@react-navigation/elementsdoes not apply on this lane — use the Expo-Router-SDK-56 swap documented inreferences/CHAT-REACT-NATIVE-blueprints.md> Channel Screen. -
Expo opting into React Navigation instead (SDK ≤ 55 only) -
npx expo install @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screensand follow the React Navigation branch. Not applicable on Expo Router SDK 56+.
On RN CLI and Expo Router SDK <= 55, Chat blueprints read useHeaderHeight() from @react-navigation/elements; that's why elements is in the React Navigation install line above. On Expo Router SDK 56+, do not install or import it - see the Channel Screen blueprint for the Platform-based swap.
For Feeds apps that use a comments modal (the typical activity-details flow), register the route with presentation: "modal":
- Expo Router: add
<Stack.Screen name="comments-modal" options={{ presentation: "modal", title: "Comments" }} />in the parent_layout.tsxand createapp/comments-modal.tsxwith the blueprint code. - React Navigation: add
<Stack.Screen name="CommentsModal" component={CommentsModal} options={{ presentation: "modal" }} />and navigate withnavigation.navigate("CommentsModal", { activityId }).
Pass only the activityId (string) as a navigation param. The modal screen creates client.activityWithStateUpdates(activityId) and disposes it on unmount.
After scaffold and packages:
- Use
references/DOCS.mdto fetch the appropriate manifest (Chat, Video, or Feeds) and selectedInstallationmarkdown page. - Confirm the installed Stream package matches the selected docs and npm dist-tag.
- Run
credentials.mdor wire the app's token provider plan. - Configure Babel (Chat: Reanimated/Worklets plugin) and root providers.
- Implement the first screen set:
- Chat:
references/CHAT-REACT-NATIVE-blueprints.md-> App Provider and Auth Gate, Navigation Shell, Channel List Screen, Channel Screen. - Video:
references/VIDEO-REACT-NATIVE-blueprints.md-> App Provider and Auth Gate, Navigation Shell, Home / Join-or-Start Call, Active Call Screen. - Feeds:
references/FEEDS-REACT-NATIVE-blueprints.md-> App Provider and Auth Gate, Own Feeds Context, Activity List Screen, Activity Composer, Comments Modal.
- Chat:
- Start the dev server only when useful and feasible for the environment (
npx expo start --dev-client,npm run ios, ornpm run android).
Resolve five things before editing an existing app:
- Runtime: Expo or RN CLI
- Product: Chat, Video, Feeds, or any combination (from Step 0 of
SKILL.md) - Navigation: React Navigation, Expo Router, existing custom navigation, or no navigation
- Scope: setup only, core Chat / Video / Feeds screens, optional native capability, or customization
- Auth model: backend token endpoint, CLI-generated local token, or pasted static token
If the user only asked for setup, stop after the shared wiring in sdk.md.
Use references/DOCS.md first: fetch the appropriate manifest (Chat, Video, or Feeds), select Installation, then fetch that markdown page.
Preserve the project's package manager. Use npx expo install for Expo packages so versions match the Expo SDK.
npm view stream-chat-react-native version dist-tags --json
npm install stream-chat-react-native@latest @react-native-community/netinfo react-native-gesture-handler react-native-reanimated react-native-teleport react-native-worklets react-native-svgIf the project uses yarn or pnpm, translate the command without changing package names. Run pods after native dependencies change:
npx pod-installnpm view stream-chat-expo version dist-tags --json
npx expo install stream-chat-expo@latest @react-native-community/netinfo expo-dev-client expo-image-manipulator react-native-gesture-handler react-native-reanimated react-native-svg react-native-teleport
# MANDATORY on Expo SDK 57: bump past the crash-prone bundled pin (4.5.0/0.10.0), AFTER the line above re-pins it.
npx expo install react-native-reanimated@4.5.2 react-native-worklets@0.10.2Expo Chat apps use a dev-client/native-build lane by default because the SDK includes native code. If the app does not already have native projects, generate them:
npx expo prebuildRun Expo through the dev client:
npx expo start --dev-clientDo not target Expo Go for stream-chat-expo. Also set useNativeMultipartUpload={true} on Chat when upload progress is required.
npm view @stream-io/video-react-native-sdk version dist-tags --json
npm install @stream-io/video-react-native-sdk
npm install @stream-io/react-native-webrtc react-native-svg @react-native-community/netinfo
npm install react-native-safe-area-context
npx pod-installFor Android edge-to-edge, pick the branch matching the host RN version (see "Required Android setup" below) - on RN 0.81+ you do not install react-native-edge-to-edge. If the project uses yarn or pnpm, translate the command without changing package names. Run pods after native dependencies change.
Required Android setup in the host app:
android/build.gradle:minSdkVersion = 24android/app/build.gradle:compileOptions { sourceCompatibility JavaVersion.VERSION_1_8; targetCompatibility JavaVersion.VERSION_11 }AndroidManifest.xml: declareCAMERA,RECORD_AUDIO,MODIFY_AUDIO_SETTINGS(addBLUETOOTH_CONNECTfor Bluetooth audio). Foreground-service permissions are capability-owned - declare them only for background calls (androidKeepCallAlive) or screenshare; see the per-capability list below- Android edge-to-edge (pick the branch matching the host RN version):
- RN 0.81+: set
edgeToEdgeEnabled=trueinandroid/gradle.properties- the RN Gradle plugin handles the rest. Noreact-native-edge-to-edgeinstall, nostyles.xmledit. - Older RN CLI:
npm install react-native-edge-to-edgeand inherit aTheme.EdgeToEdgevariant (e.g.Theme.EdgeToEdge.Material3) inandroid/app/src/main/res/values/styles.xml. Add<item name="enforceNavigationBarContrast">false</item>for a fully transparent nav bar.
- RN 0.81+: set
Required iOS setup:
Info.plist: addNSCameraUsageDescriptionandNSMicrophoneUsageDescription- For ringing/VoIP, also include
voipandaudioinUIBackgroundModes
npm view @stream-io/video-react-native-sdk version dist-tags --json
npx expo install @stream-io/video-react-native-sdk \
@stream-io/react-native-webrtc \
@config-plugins/react-native-webrtc \
react-native-svg \
@react-native-community/netinfo \
react-native-safe-area-context \
expo-build-propertiesEnable Android edge-to-edge in app.json (default-on from Expo SDK 54, opt-in on SDK 53):
{
"expo": {
"android": {
"edgeToEdgeEnabled": true
}
}
}Add config plugins to app.json:
{
"expo": {
"plugins": [
"@stream-io/video-react-native-sdk",
[
"@config-plugins/react-native-webrtc",
{
"cameraPermission": "$(PRODUCT_NAME) requires camera access to capture and transmit video",
"microphonePermission": "$(PRODUCT_NAME) requires microphone access to capture and transmit audio"
}
],
[
"expo-build-properties",
{ "android": { "minSdkVersion": 24 } }
]
]
}
}Then regenerate the native projects:
npx expo prebuild --cleanDo not target Expo Go for Video; the SDK includes native code.
npm view @stream-io/feeds-react-native-sdk version dist-tags --json
npm install @stream-io/feeds-react-native-sdk @react-native-community/netinfo
npm install react-native-safe-area-context
npx pod-installFeeds has no Reanimated, gesture-handler, SVG, or worklets requirement of its own. If the project uses yarn or pnpm, translate the command without changing package names. Run pods after native dependency changes.
npm view @stream-io/feeds-react-native-sdk version dist-tags --json
npx expo install @stream-io/feeds-react-native-sdk @react-native-community/netinfo
npx expo install react-native-safe-area-contextNo Expo config plugin entries are needed for Feeds. A Feeds-only Expo app can stay on the managed workflow; if Chat or Video is also installed, the dev-client lane is required for that other product, and Feeds continues to work alongside.
| User asks for | Packages | Notes |
|---|---|---|
| Ringing (CallKit iOS, Android Telecom) | @stream-io/react-native-callingx |
Wires CallKit/Telecom; see manifest-selected /incoming-calls/* pages |
| Background blur / virtual background | @stream-io/video-filters-react-native |
Optional filter pipeline |
| Noise cancellation | @stream-io/noise-cancellation-react-native |
Audio quality improvement |
| Ringing push delivery (Android FCM) | @react-native-firebase/app, @react-native-firebase/messaging |
Required for ringing on Android; @react-native-firebase/messaging is also the typical library for app-owned non-ringing handling |
| App-owned non-ringing notifications | @react-native-firebase/messaging, expo-notifications, @react-native-community/push-notification-ios, @notifee/react-native (any combination) |
Non-ringing pushes (call.missed, call.notification, call.live_started - the three values of the SDK's NonRingingPushEvent type) are app-owned. Register the device token with client.addDevice(token, provider, providerName) and handle display/taps yourself. See manifest-selected /incoming-calls/non-ringing-notifications-setup/overview/ |
| Permissions helper | react-native-permissions |
Pre-call permission prompts |
After adding native Video optional packages, follow their platform permission steps. For Expo, keep the app in the dev-client/native-build lane and run npx expo prebuild --clean when native config changes need to be regenerated.
Optional dependencies are capability packages. They are not required for every Chat app. Install them only when the user asks for that capability, when selected manifest docs require them, or when an implemented blueprint needs native functionality beyond the core Chat UI.
How to add one:
- Identify the requested capability from the user request and manifest-selected docs.
- Pick the package from the matrix for the detected runtime lane.
- Install with the project's package manager for RN CLI, or
npx expo installfor Expo. - Add required platform permissions or Expo config plugins from the selected package docs.
- Run pods for RN CLI native installs. For Expo, keep the app in the dev-client/native-build lane and run prebuild when native config changes need to be regenerated.
- Verify the capability in the existing app flow; do not leave unused optional packages installed.
| User asks for | RN CLI packages | Expo packages | Notes |
|---|---|---|---|
| React Navigation examples / safe areas | react-native-safe-area-context |
react-native-safe-area-context |
Needed for SafeAreaProvider and useSafeAreaInsets; navigation itself may already be installed |
| Native multipart upload progress | none beyond required Stream peers | none beyond Expo dev-client lane | Set useNativeMultipartUpload={true} on Chat |
| Attachment picker with built-in image media library | @react-native-camera-roll/camera-roll |
expo-media-library |
Enables gallery images in the SDK attachment picker |
| Native image picker / camera image upload | react-native-image-picker |
expo-image-picker |
Use for camera capture and native picker flows |
| File attachments / document picker | @react-native-documents/picker |
expo-document-picker |
Required for file picking |
| Attachment sharing outside the app | react-native-blob-util react-native-share |
expo-sharing |
Share downloaded attachments |
| Video playback / video attachments | react-native-video |
expo-video |
Optional media playback |
| Voice recording and audio attachments | react-native-video react-native-audio-recorder-player react-native-blob-util |
Expo SDK 53+: expo-audio; Expo SDK 51/52: expo-av |
Add microphone permissions/config plugins |
| Copy message | @react-native-clipboard/clipboard |
expo-clipboard |
Clipboard action support |
| Haptic feedback | react-native-haptic-feedback |
expo-haptics |
Optional tactile feedback |
| Offline support | @op-engineering/op-sqlite |
@op-engineering/op-sqlite |
Requires native code; Expo already uses the dev-client lane |
| High-performance message list | @shopify/flash-list |
@shopify/flash-list |
Use when large channels need FlashList |
After adding native optional packages, follow their platform permission steps. For Expo, keep the app in the dev-client/native-build lane and run npx expo prebuild when native config changes need to be regenerated.
Batch capability packages before the first native build. Each native capability package forces a prebuild + native rebuild (minutes). Decide the complete set the app needs up front and install them together before the first expo run:ios / pod install, so you build once — adding one later (e.g. discovering the composer mic needs expo-audio only after the app runs) costs a second full rebuild, the most common avoidable simulator time sink. See references/SIMULATOR-VERIFICATION.md.
On Expo SDK 54+ there is nothing to do here: do NOT create a babel.config.js. babel-preset-expo
resolves react-native-worklets/plugin and pushes it onto the end of the plugin list automatically
whenever the package is installed (verified in babel-preset-expo/build/configs/expo.js — "Automatically
add worklets or reanimated plugin when package is installed"), so the SDK 54+ templates ship no
babel.config.js by design. Hand-writing one duplicates the plugin, and if it names
babel-preset-expo while that isn't a top-level dependency Metro dies with Cannot read properties of undefined (reading 'transformFile') — which reads like a corrupt cache, not a config error. Two real runs
created one anyway; one of them lost Metro to exactly this. A missing babel.config.js on Expo SDK 54+
is not a finding. Below SDK 54, and on RN CLI, write it:
module.exports = {
presets: ["module:@react-native/babel-preset"], // Expo <54: "babel-preset-expo"
plugins: [
// other plugins
"react-native-worklets/plugin",
],
};Use react-native-reanimated/plugin if the project is still on Reanimated 3. Use react-native-worklets/plugin for Reanimated 4+.
Reanimated/Worklets are optional for Video - the SDK falls back to the RN Animated API when they are absent. But Stream's sample apps (including the video-only ones) install react-native-reanimated + react-native-worklets + react-native-gesture-handler for the smoother animated floating-participant tile. If they are installed (or Chat is also in scope) and the lane needs a Babel config per the rule above, add the Reanimated/Worklets plugin as the last Babel plugin.
On Reanimated 4 with Stream Chat, set FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS to false in the app-root package.json — its default (true) is a known regression source for bottom sheets, the message overlay, and context-menu animations. It is read at pod-install time, so add it before the native build (or re-run pod install + rebuild after adding):
"reanimated": { "staticFeatureFlags": { "FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": false } }See RULES.md > Required peer setup.
Wrap the app entry point with GestureHandlerRootView (required for Chat; recommended for Video apps that use any gesture handling).
For Expo Router, the entry point is usually app/_layout.tsx. For RN CLI, it is usually App.tsx or the component registered from index.js.
If Video is in scope, ensure runtime camera/microphone access is configured:
- RN CLI iOS:
NSCameraUsageDescriptionandNSMicrophoneUsageDescriptioninInfo.plist. AddvoipandaudiotoUIBackgroundModesif ringing is in scope. - RN CLI Android: declare
CAMERA,RECORD_AUDIO,MODIFY_AUDIO_SETTINGS(andBLUETOOTH_CONNECTif Bluetooth audio is wanted) inAndroidManifest.xml. AddFOREGROUND_SERVICE/FOREGROUND_SERVICE_CAMERA/FOREGROUND_SERVICE_MICROPHONE/FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACKonly for background calls (matches Expo plugin'sandroidKeepCallAlive); addFOREGROUND_SERVICE+FOREGROUND_SERVICE_MEDIA_PROJECTIONonly for screenshare. A plain foreground call needs none of those. - Expo: handled by the
@config-plugins/react-native-webrtcplugin entry inapp.jsonplusnpx expo prebuild --clean.
Use react-native-permissions if the app needs to request permissions before the first call screen mounts; otherwise the SDK prompts at the first media access.
Always wire safe areas:
- Install
react-native-safe-area-contextand mountSafeAreaProvidernear the root (above the navigator) on every app. UseSafeAreaView(fromreact-native-safe-area-context, notreact-native) for full-screen wrappers anduseSafeAreaInsets()when you need fine-grained padding. - RN 0.85 + Expo 56 + new architecture caveat: the package's
SafeAreaView(v5.7) appears to no-op at the native boundary on this toolchain - inset never applies. TheuseSafeAreaInsets()hook still works. PreferView+useSafeAreaInsets()+ explicitpaddingTop/paddingBottomfor full-screen wrappers when shipping on this stack. AddpaddingBottom: insets.bottom + NtoFlatListcontentContainerStyleunder a native tab bar so the last items clear it. - Android edge-to-edge is required so the app draws under transparent system bars.
- Expo: set
"edgeToEdgeEnabled": trueinapp.jsonunderandroid(default-on Expo SDK 54+). - RN CLI 0.81+: set
edgeToEdgeEnabled=trueinandroid/gradle.properties. The RN Gradle plugin enables the edge-to-edge feature flag automatically - noreact-native-edge-to-edgeinstall, nostyles.xmledit. - Older RN CLI: install
react-native-edge-to-edgeand set the app theme parent to aTheme.EdgeToEdgevariant inandroid/app/src/main/res/values/styles.xml.
- Expo: set
- Status-bar / nav-bar styling: Expo uses
expo-status-barand (optionally)expo-navigation-bar- both are in every Expo template, no extra install needed. RN CLI uses<SystemBars style="auto" />fromreact-native-edge-to-edge. Both APIs are equivalent on Expo SDK 54+ (Expo's wrappers delegate toSystemBarsunder the hood). Do not call deprecated directStatusBarAPIs fromreact-nativewhen edge-to-edge is on. - For Chat:
<Channel>handles its own insets. Do not passtopInsetorbottomInsetby default; add them only after a specific layout or attachment-picker issue proves they are needed. If navigation is used, placeSafeAreaProvidernear the root. When the chat screen sits under a native navigation header, pass that header height toChannelas bothkeyboardVerticalOffsetandtopInset(same value) —topInsetis what the attachment picker uses to compute its bottom sheet top boundary, and without it the sheet clamps short of its snap point.bottomInsetstays opt-in; add it only when a specific layout requires it (e.g. a tab bar that owns the bottom safe-area). - For Video: the SDK does not infer insets. Read them with
useSafeAreaInsets()and bridge into<StreamVideo style={theme}>astheme.variants.insets = { top, right, bottom, left }soCallContent,RingingCallContent,HostLivestream,ViewerLivestream, and participant views respect notches and system bars. Once the theme insets are wired, do not also wrap those components inSafeAreaView, and do not re-readuseSafeAreaInsets()inside a customCallControlsto addpaddingBottom- both produce double padding. For custom top bars rendered outsideCallContent, custom bottom overlays / drawers / subtitles, or other layouts that fightCallContent's built-in padding, follow the patterns on the live Safe area insets cookbook page (scoped<StreamTheme>override ofcallContent.container.paddingTop; reusetheme.variants.insets.bottomin absolute offsets).
Before writing code, confirm credentials.md has resolved the API key, user id, and token or token provider plan for tracks A/B/D.
Follow sdk.md for shared patterns (client lifecycle, auth, provider tree, navigation, lifecycle/cleanup) and then branch by product:
Chat:
- package import lane (
stream-chat-react-nativeorstream-chat-expo) useCreateChatClientfor client lifecycleOverlayProvider+<Chat>root provider hierarchy- channel selection and CID navigation
- thread state
- sign-out and offline cleanup
Feeds:
useCreateFeedsClient({ apiKey, tokenOrProvider, userData })for client lifecycle (returnsundefinedwhile connecting; do not passundefinedto<StreamFeeds>)<StreamFeeds client={feedsClient}>mounted once near the app root, above the navigatorOwnFeedsContextProviderthat createsuser+timelinefeeds withclient.feed(group, id), loads them withgetOrCreate({ watch: true }), and establishes the self-follow (timeline.follow(userFeed.feed)) on first run<StreamFeed feed={...}>around each screen subtree that reads feed state, so descendant hooks resolve the feed from context- Navigation passes
activityIdstrings (notActivityResponseobjects); activity-details modal createsclient.activityWithStateUpdates(id)and disposes on unmount - Sign-out: unmount or change
useCreateFeedsClientinputs (or callclient.disconnectUser()directly)
Video:
StreamVideoClient.getOrCreateInstance({ apiKey, user, tokenProvider, options? })inside auseEffect, withclient.disconnectUser()on cleanup<StreamVideo client={client}>mounted once near the app root, above the navigatorCallcreated exactly once in the destination call screen viaclient.call(type, id, { reuseInstance: true })(the flag is mandatory - the same(type, id)may already be live from a ring/deep link/push, and without it the SDK constructs a duplicate); mount<StreamCall call={call}>; descendants read it viauseCall()and never callclient.call(...)again; navigation hands off only the call id, not the Call instance. Usejoin({ create: true })only for create-on-join lobby flows; ringing, livestream-host, and audio-room flows join withoutcreate.call.leave()on screen unmount, guarded bycall.state.callingState !== CallingState.LEFT(a secondleave()throwsCannot leave call that has already been left); hangup handlers only navigate- audio routing is automatic on
call.join()/call.leave()(defaultaudioRole: "communicator"); only callcallManager.start/stopto override the role - the only other value is"listener"(playback-optimized, for a view-only livestream viewer or audio-room audience member) - error handling around
call.join(),call.camera.enable(),client.connectUser()
Use the real API key and token or the app's token provider. Reference credentials via named constants (e.g., from a local .env file or config module) or the app's token provider. Do not embed raw credential values in final code unless the user explicitly asked for a template only.
Use the requested screen/feature and product to choose the smallest relevant reference set.
Always load:
references/DOCS.mdforllms.txtmanifest lookup
Then load the matching product references:
Chat work:
references/CHAT-REACT-NATIVE.mdfor setup and gotchasreferences/CHAT-REACT-NATIVE-blueprints.mdfor screen/component blueprints
Video work:
references/VIDEO-REACT-NATIVE.mdfor setup and gotchasreferences/VIDEO-REACT-NATIVE-blueprints.mdfor screen/component blueprints
Feeds work:
references/FEEDS-REACT-NATIVE.mdfor setup and gotchasreferences/FEEDS-REACT-NATIVE-blueprints.mdfor screen/component blueprints
Per RULES.md, re-open the relevant blueprint section before every Stream Chat, Stream Video, or Stream Feeds screen, navigation handler, thread / comments flow, ringing handler, call control, participant tile, theming override, offline flow, activity row, composer, follow button, or component customization edit.
For requested optional native capabilities, read the Optional dependency map in the matching product reference file before installing packages.
Use this when the request is a targeted Chat, Video, or Feeds change in an existing app.
- Detect runtime, product(s), and currently installed Stream packages.
- Use
references/DOCS.mdto fetch the relevant manifest (Chat, Video, or Feeds) and selected markdown page for the requested area. - Open the matching blueprint section in the product's
*-blueprints.md. - For cookbook-style requests, use
references/DOCS.mdmanifest search and fetch the best matching cookbook/customization markdown page. - Prefer the smallest change that preserves the app's architecture:
- Chat: style-only -> theme object; slot-level UI ->
WithComponents; behavior -> component prop or documented hook; native capability -> install only the optional package(s) for that capability - Video: style-only -> pass a theme via
<StreamVideo style={theme}>(or scope it with<StreamTheme style={theme}>); slot-level UI ->CallContentslot props (CallControls,CallParticipantsList,FloatingParticipantView,ParticipantView); behavior -> documentedCallmethod oruseCallStateHooks()value; native capability -> install only the optional package(s) for that capability - Feeds: the SDK is headless. Style and structure live in the components you wrote (Activity, ActivityComposer, Reaction, FollowButton, comments UI). Behavior changes go through the state hooks (
useFeedActivities,useActivityComments,useOwnFollows, ...) or direct client / feed methods (client.addActivityReaction,feed.addActivity,timeline.follow, ...).
- Chat: style-only -> theme object; slot-level UI ->
- Verify with the existing project commands.
For Chat message visual or layout changes, fetch the manifest-selected theming/customization pages, then prefer theme values before replacing core message components. For Video customization, prefer slot replacement over full CallContent replacement. For Feeds, edit the components you wrote directly - there is no WithComponents analog.
Use the project's existing verification commands. Prefer the smallest checks that prove the integration works.
Common:
- package install completed and selected Stream package(s) match the docs
- iOS pods resolved for RN CLI native installs
GestureHandlerRootViewwraps the app (Chat: required; Video: recommended when any gestures)- optional dependencies are present only for requested optional features
Chat:
- On Expo SDK 57: assert
react-native-reanimated≥ 4.5.1 andreact-native-worklets≥ 0.10.2 (the SDK pins a crash-prone 4.5.0/0.10.0; verify by version number, not by "it launched" — the crash is on worklet paths, not at boot). Rebuild native after bumping. - On Reanimated 4: assert
reanimated.staticFeatureFlags.FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONSisfalsein the rootpackage.json(defaulttrueregresses bottom sheets / overlay / context-menu animations; read at pod-install time, so confirm a native rebuild ran after adding it). - Babel Reanimated/Worklets plugin is present and last
OverlayProviderandChatare stable near the rootChannelListrenders for the connected user- channel navigation passes a CID, not a
Channelobject ChannelrendersMessageListandMessageComposer- thread navigation passes thread state correctly
- sign-out clears the connected user and, if offline is enabled, resets offline DB before disconnect
Video:
- camera and microphone permissions declared (iOS
Info.plist, AndroidAndroidManifest.xml); Expo: config plugins inapp.jsonandnpx expo prebuild --cleanran - Android
minSdkVersion = 24set (RN CLI direct, Expo viaexpo-build-properties) - client created via
StreamVideoClient.getOrCreateInstance(...)(notnew StreamVideoClient(...)) and disposed on cleanup <StreamVideo>mounted once near the app root, above the navigatorCallcreated exactly once withclient.call(type, id, { reuseInstance: true })in the destination call screen, joined insideuseEffect(usejoin({ create: true })only for create-on-join lobby flows; ringing / livestream-host / audio-room join calls created upstream and pass nocreate), and mounted via<StreamCall>; descendants read it viauseCall()and never callclient.call(...)again; upstream screens (lobby, home) only hand off the call id, do not pre-create the Callcall.leave()called on cleanup guarded bycallingState !== CallingState.LEFT(avoidsCannot leave call that has already been left); hangup handlers only navigate- audio routing left to the SDK (automatic on
call.join()/call.leave()); no manualcallManager.start/stopunless overriding the defaultaudioRole: "communicator" - call navigation passes only the call id, not a
Callobject - error handling around
call.join(),call.camera.enable(),client.connectUser() - ringing-related setup matches manifest-selected
/incoming-calls/*pages when ringing is in scope
Feeds:
useCreateFeedsClienthost rendersnull(or a spinner) while the hook returnsundefined;<StreamFeeds client={...}>is never rendered withundefined<StreamFeeds>mounted once near the app root, above the navigatorOwnFeedsContextProvider(or equivalent) createsuserandtimelinefeeds once and shares them via context, not via navigation params- Self-follow established once after both feeds load (
timeline.follow(userFeed.feed)if not already present inuserFeed.currentState.own_follows); self-follow runs unconditionally on every start (idempotent), not buried in seed logic - Activity rendering uses the state hooks (
useFeedActivities,useActivityComments,useOwnFollows,useAggregatedActivities,useNotificationStatus) and readsactivity.reaction_groups[type]?.count/activity.own_reactionsfor reactive reaction state - Reactions go through
client.addActivityReaction/client.deleteActivityReaction(on the client, not on the feed) - Comments modal passes
activityId(string) through navigation params; createsclient.activityWithStateUpdates(id)once on mount and calls.dispose()on unmount client.disconnectUser()runs on sign-out (or theuseCreateFeedsClienthost unmounts)
Common commands:
npm run typecheck
npm run lint
npm run ios
npm run android
npx expo startRun only commands that exist in the project.
Running on the iOS simulator (Expo dev-client). When you actually boot the app to screenshot and
verify it (especially for a design match), follow the fast loop in
references/SIMULATOR-VERIFICATION.md: batch all capability
packages before the first native build, start Metro not in CI mode (npx expo start --dev-client --clear) so edits aren't served stale, start Metro separately from expo run:ios (which exits and
can take the bundler down), reach non-initial screens with temporary in-code navigation (simctl
can't tap), and wait for the client to reconnect before trusting a screenshot. Three traps from that
page that cost real runs the most time: redirect Metro to a log, never pipe it (a closing pipe kills
it — and a piped gate command returns the pipe's exit status, so a failing build reads as green);
simctl terminate before every simctl launch (launch on a running app returns its PID without
restarting, so you screenshot stale UI); and simctl privacy revoke photos before launching, since
granting does not reliably suppress iOS 26's un-dismissable photo prompt.