
Stream React Native
- 503 installs
- 17 repo stars
- Updated August 4, 2026
- getstream/agent-skills
stream-react-native is an agent skill that integrates Stream Chat, Video, and Feeds into React Native and Expo apps with navigation, hooks, and cross-platform UI patterns for developers shipping real-time messaging or ac
About
stream-react-native is a GetStream/agent-skills sub-skill for building and integrating Stream products in React Native CLI and Expo projects. It classifies intent, detects existing packages such as stream-chat-react-native, stream-chat-expo, @stream-io/video-react-native-sdk, and @stream-io/feeds-react-native-sdk, then routes through credentials setup, install commands, native requirements, and shared client lifecycle patterns in sdk.md. Non-negotiable rules in RULES.md enforce New Architecture assumptions, correct import packages per lane, and provider nesting when combining Chat, Video, and Feeds. builder.md walks existing-app modification while references/ hold Chat and Video blueprints and llms.txt doc manifests. Install on demand via getstream skills or invoke /stream-react-native directly. Reach for it when adding chat channels, video calls, livestreams, audio rooms, or activity feeds to a React Native or Expo codebase.
- React Native Stream SDK bootstrap
- Cross-platform chat UI integration
- Token auth and connection lifecycle
- Shared patterns for iOS and Android targets
Stream React Native by the numbers
- 503 all-time installs (skills.sh)
- Ranked #303 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/getstream/agent-skills --skill stream-react-nativeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 503 |
|---|---|
| repo stars | ★ 17 |
| Last updated | August 4, 2026 |
| Repository | getstream/agent-skills ↗ |
How do you add Stream chat to React Native?
Add Stream-powered chat or activity features to a React Native app with navigation, hooks, and cross-platform UI patterns.
Who is it for?
React Native and Expo developers integrating Stream Chat, Video, or Feeds with official SDK patterns, credentials flow, and New Architecture constraints.
Skip if: Native iOS Swift or Android Kotlin Stream integrations, or web-only React apps without React Native.
When should I use this skill?
The user asks to add Stream chat, video calls, livestreams, activity feeds, or Stream SDK setup to a React Native or Expo project.
What you get
Installed Stream SDK packages, configured API keys and tokens, wired provider trees, and working Chat, Video, or Feeds screens with correct client lifecycle cleanup.
- SDK install commands
- provider tree wiring
- Chat or Video screen integration
By the numbers
- Covers 3 Stream SDK products—Chat, Video, and Feeds—for React Native CLI and Expo
- Ships 6 core modules: SKILL.md, RULES.md, credentials.md, builder.md, sdk.md, and references/
Files
Stream React Native - skill router + execution flow
Rules: Read [`RULES.md`](RULES.md) once per session. Every non-negotiable React Native Chat, Video, and Feeds rule is stated there.
This file is the single entrypoint: intent classification, product selection, project detection, and module pointers for Stream Chat React Native, Stream Video React Native, and Stream Feeds React Native work.
---
Step 0: Intent classifier (mandatory first - never skip)
Before any tool call, decide the track from the user's input alone. Do not probe the filesystem first.
Signals -> track
| Signal in user input | Track |
|---|---|
| "Build/create/scaffold a new React Native app", "create an Expo app", "new Stream Chat RN app", "new Stream Video RN app", "new Stream Feeds RN app", empty directory + React Native/Expo Chat / Video / Feeds | A - New app |
| "Add/integrate Stream Chat into this app", "wire Chat RN", "set up stream-chat-expo", "add a video call", "wire Stream Video", "set up @stream-io/video-react-native-sdk", "add an activity feed", "wire Stream Feeds", "set up @stream-io/feeds-react-native-sdk", "change/customize this Chat / Video / Feeds UI" | B - Existing app |
React Native, Expo, Expo Router, stream-chat-react-native, stream-chat-expo, @stream-io/video-react-native-sdk, @stream-io/feeds-react-native-sdk, Stream Chat RN, Stream Video RN, Stream Feeds RN, Chat React Native, Video React Native, Feeds React Native, migration | C - Reference lookup if the user only asks how/docs; otherwise B - Existing app |
Explicit product/runtime token: Chat React Native, Chat Expo, Video React Native, Video Expo, Feeds React Native, Feeds Expo | C - Reference lookup |
| Words "docs" or "documentation" around Stream Chat, Stream Video, or Stream Feeds React Native / Expo work | C - Reference lookup |
| "How do I {X} in React Native/Expo?", "What does {SDK component/hook/prop} do?" | C - Reference lookup |
| "Audit/review/check an existing Stream Video integration against best practices", "is my video app production-ready?", "what am I missing before launch?" | C - Reference lookup (read-only) - run the Integration best-practices audit in `references/VIDEO-REACT-NATIVE.md` |
| "Install Stream packages", "set up Chat RN", "set up Video RN", "set up Feeds RN", "wire auth/token flow" with no broader feature request | D - Bootstrap / setup |
| Stream Moderation review UI or any other non-Chat / non-Video / non-Feeds Stream RN product | Reject bundled scope and route to live docs only if the user wants docs |
Bare /stream-react-native with no args | List the tracks briefly and wait |
Disambiguation flow
If the request is ambiguous between wiring code and reference lookup, ask one short question and wait:
Do you want me to wire this into the project, or just map the React Native SDK pattern and files?
If the user wants a new app but did not name Expo or RN CLI, default to Expo because it is the shortest successful path. Use RN CLI when the user asks for it or when native project constraints require it.
Product classifier (after track is known)
Identify the product from the user's input or detected packages. Tracks A, B, and D all need the product before continuing; Track C selects the matching reference pair.
| Product signal | Product | References |
|---|---|---|
stream-chat-react-native, stream-chat-expo, channel, message, MessageComposer, thread, attachment, offline support | Chat | `references/CHAT-REACT-NATIVE.md` + `references/CHAT-REACT-NATIVE-blueprints.md` |
@stream-io/video-react-native-sdk, video call, livestream, audio room, ringing, CallContent, ParticipantView, screenshare, picture in picture | Video | `references/VIDEO-REACT-NATIVE.md` + `references/VIDEO-REACT-NATIVE-blueprints.md` |
@stream-io/feeds-react-native-sdk, useCreateFeedsClient, StreamFeeds, StreamFeed, activity feed, timeline feed, notification feed, for-you feed, useFeedActivities, useActivityComments, reactions, comments, follows, follow / unfollow, activity composer, activityWithStateUpdates | Feeds | `references/FEEDS-REACT-NATIVE.md` + `references/FEEDS-REACT-NATIVE-blueprints.md` |
| Two or more products in one app (chat alongside a video call, feeds + chat, feeds + video, etc.) | Combined | Load only the reference pairs for the products in scope. Nest the providers (e.g. <StreamVideo> inside <OverlayProvider><Chat> inside <StreamFeeds>); do not place them as siblings. See `RULES.md` and the manifest-selected /video/docs/react-native/advanced/chat-with-video.md for the Chat + Video interop notes |
If the request is ambiguous between products, ask one short question and wait:
Are you wiring Stream Chat (channels, messages), Stream Video (calls, livestream, audio rooms), or Stream Feeds (activity feed, timeline, comments, reactions, follows)? Or more than one in the same app?
Scope rejection
This skill bundles Chat, Video, and Feeds React Native. If the user asks for Stream Moderation review UI or another non-bundled Stream RN product, say:
The React Native skill currently bundles Chat, Video, and Feeds references. I can help with any combination of those here, or switch to live docs for other products.
Do not invent missing React Native Moderation API details from memory.
After classification
- Tracks A, B, D -> run Project signals, then continue in `builder.md` and `sdk.md`. Run `credentials.md` before writing Chat, Video, or Feeds connection code or creating requested demo data.
- Track C -> skip credentials and project probes if the product + runtime are explicit. Only run a read-only probe if RN CLI vs Expo is ambiguous and the answer affects the guidance.
---
Step 0.5: Credentials, token, and demo data (tracks A, B, D only)
Use `credentials.md` once per session before writing code that connects to Stream Chat, Stream Video, or Stream Feeds.
It resolves:
- Stream API key
- user id and display name
- user token or token provider plan
- optional demo data, only when requested, via Stream CLI calls (Chat:
UpdateUsers,GetOrCreateChannel,SendMessage; Video and Feeds do not require seed data because calls are ephemeral and activities can be posted at runtime)
For Track A, it is acceptable to scaffold the app first if the runtime or target directory must be resolved before credentials. Do not render a connected Chat, Video, or Feeds UI until credentials or a token-provider plan are resolved.
---
Project signals (tracks A/B/D - once per session; Track C on demand only)
Read-only local probe. Use it to detect empty/new workspace, RN CLI vs Expo, New Architecture hints, navigation setup, and existing Stream packages.
bash -c 'echo "=== PACKAGE ==="; test -f package.json && grep -oE "\"(stream-chat-react-native|stream-chat-expo|@stream-io/video-react-native-sdk|@stream-io/feeds-react-native-sdk|@stream-io/react-native-webrtc|@stream-io/react-native-callingx|react-native|expo|@react-navigation/[^\"]+|expo-router|react-native-reanimated|react-native-worklets|react-native-teleport|@op-engineering/op-sqlite)\": *\"[^\"]*\"" package.json 2>/dev/null; echo "=== EXPO ==="; find . -maxdepth 2 \( -name "app.json" -o -name "app.config.js" -o -name "app.config.ts" -o -path "./app/_layout.*" \) -print 2>/dev/null; echo "=== NATIVE ==="; find . -maxdepth 2 \( -name "ios" -o -name "android" \) -type d -print 2>/dev/null; echo "=== CONFIG ==="; find . -maxdepth 2 \( -name "babel.config.js" -o -name "metro.config.js" \) -print 2>/dev/null; echo "=== EXPO_SDK ==="; node -e "try{console.log(require(\"./node_modules/expo/package.json\").version)}catch(e){try{console.log(require(\"./package.json\").dependencies.expo)}catch(e){console.log(\"-\")}}" 2>/dev/null; echo "=== EMPTY ==="; test -z "$(ls -A 2>/dev/null)" && echo "EMPTY_CWD" || echo "NON_EMPTY"'Hold the result in conversation context. Do not re-run unless the user changes directory, packages are installed, or the project shape changes.
Use the result to produce a one-line status, for example:
Empty workspace detected - defaulting to Expo new app unless the user asked for RN CLIExpo app detected - stream-chat-expo absent - Expo Router present - ready for Chat setupExpo app detected - Expo SDK 56+ - apply RULES.md > Expo Router SDK 56+ rule (no @react-navigation/*)Expo app detected - @stream-io/video-react-native-sdk absent - ready for Video setupExpo app detected - @stream-io/feeds-react-native-sdk absent - ready for Feeds setupRN CLI app detected - ios/android present - stream-chat-react-native installed - checking provider placementRN CLI app detected - both @stream-io/video-react-native-sdk and stream-chat-react-native installed - Chat + Video interop appliesExpo app detected - @stream-io/feeds-react-native-sdk and stream-chat-expo installed - nest StreamFeeds + Chat providers (no sibling mounts)No RN/Expo app detected in a non-empty directory - create a new app in a child directory or ask before reusing this directory
If there is no RN/Expo project and Track A applies, scaffold one through `builder.md` > 2. New app scaffold. If Track B/D applies in a non-RN directory, ask before creating a child app because that changes project ownership.
---
Module map
| Track | Module(s) |
|---|---|
| A - New app | `builder.md` + `sdk.md` + llms.txt docs lookup + product references (Chat / Video / Feeds) |
| B - Existing app | `builder.md` + `sdk.md` + llms.txt docs lookup + product references (Chat / Video / Feeds) |
| C - Reference lookup | `sdk.md` + `references/DOCS.md` + relevant product reference files |
| D - Bootstrap / setup | `builder.md` + `sdk.md` + llms.txt docs lookup |
---
Reference layout
Shared React Native and Expo patterns live in [`sdk.md`](sdk.md).
Product-specific setup, docs lookup, gotchas, and UI blueprints live under `references/`:
- `llms.txt` docs lookup (Chat, Video, Feeds): `references/DOCS.md`
- Chat setup/reference: `references/CHAT-REACT-NATIVE.md`
- Chat screen/component blueprints: `references/CHAT-REACT-NATIVE-blueprints.md`
- Video setup/reference: `references/VIDEO-REACT-NATIVE.md`
- Video screen/component blueprints: `references/VIDEO-REACT-NATIVE-blueprints.md`
- Feeds setup/reference: `references/FEEDS-REACT-NATIVE.md`
- Feeds screen/component blueprints: `references/FEEDS-REACT-NATIVE-blueprints.md`
If the requested product file is not bundled yet, say so plainly and only switch to live docs if the user asks.
---
Track A - New app
Full detail: `builder.md` - use the new-app path.
| Phase | Name | What you do |
|---|---|---|
| A1 | Detect | Run Project signals. Empty workspace is valid for Track A. |
| A2 | Choose lane and product | Default to Expo if unspecified; use RN CLI when requested. Confirm product (Chat, Video, Feeds, or any combination). |
| A3 | Scaffold | Create the app with current framework tooling; do not explain full RN/Expo environment setup. |
| A4 | Install + wire | Use the manifest-selected Installation docs for each product, verify npm dist-tags, install package and peers, then wire providers and first UI. |
| A5 | Verify | Confirm install, root providers, permissions (Video: camera/mic), auth, and first rendered Chat / Video / Feeds screen. |
---
Track B - Existing app
Full detail: `builder.md` - use the existing-project path.
| Phase | Name | What you do |
|---|---|---|
| B1 | Detect | Run Project signals and inspect existing app structure before editing. Note any existing Chat / Video / Feeds packages. |
| B2 | Preserve | Keep Expo/RN CLI lane, package manager, navigation stack, and auth architecture unless asked to migrate. |
| B3 | Integrate | Use llms.txt lookup for the requested area, then load only the Chat / Video / Feeds reference/blueprint sections needed. |
| B4 | Verify | Confirm the requested Stream Chat / Video / Feeds flow builds and renders in the existing app. |
---
Track C - Reference lookup
Load only the relevant files for the requested product:
llms.txtmanifest lookup rules -> `references/DOCS.md`- Shared lifecycle / auth / provider / runtime patterns -> `sdk.md`
- Chat RN setup and gotchas -> `references/CHAT-REACT-NATIVE.md`
- Chat RN screen/component structure -> `references/CHAT-REACT-NATIVE-blueprints.md`
- Video RN setup and gotchas -> `references/VIDEO-REACT-NATIVE.md`
- Video RN screen/component structure -> `references/VIDEO-REACT-NATIVE-blueprints.md`
- Feeds RN setup and gotchas -> `references/FEEDS-REACT-NATIVE.md`
- Feeds RN screen/component structure -> `references/FEEDS-REACT-NATIVE-blueprints.md`
- Audit an existing Video integration against best practices -> the Integration best-practices audit section in `references/VIDEO-REACT-NATIVE.md` (read-only review; produce findings before changing any code)
If the user asks for exact API details not bundled here, use `references/DOCS.md` to fetch the right manifest and selected markdown page. If implementation still needs source-level confirmation, inspect the installed package under the target app's node_modules after dependencies are installed. Do not use machine-specific documentation paths.
---
Track D - Bootstrap / setup
Use when the user wants package install and shared wiring more than a full feature build. Branch by product:
- detect RN CLI vs Expo
- use
llms.txtlookup for the matching product's Installation docs and verify current npm dist-tags - install the correct package and required peers (Chat:
stream-chat-react-nativeorstream-chat-expo; Video:@stream-io/video-react-native-sdk; Feeds:@stream-io/feeds-react-native-sdk+@react-native-community/netinfo) - Chat-specific wiring: add Reanimated/Worklets Babel plugin as the last plugin, wrap the entry point with
GestureHandlerRootView, placeOverlayProviderandChat, wireuseCreateChatClientor the app's backend token provider - Video-specific wiring: declare camera/mic permissions, add Expo config plugins where applicable, create
StreamVideoClientand mountStreamVideo - Feeds-specific wiring: call
useCreateFeedsClient(returnsundefinedwhile connecting), mount<StreamFeeds client={client}>once near the app root, and (typically) wrap anOwnFeedsContextProviderthat creates the user + timeline feeds and the self-follow - stop before product-specific UI if the user only asked for setup
Stream React Native - build and integration flow
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.
---
1. Detect the workspace
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 Reanimated/Worklets plugin (Chat)
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.md` and `sdk.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.
---
2. New app scaffold
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.
Pick target directory
- 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.
Scaffold the runtime (product-agnostic)
Expo default lane (replace MyApp with the target directory):
npx create-expo-app@latest MyApp
cd MyAppRN CLI lane (only when the user asks for RN CLI or requirements point there):
npx @react-native-community/cli@latest init MyApp
cd MyAppInstall Stream packages by product
Pick 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
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. If you installed the animation peers, add react-native-worklets/plugin as the last 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 - no `react-native-edge-to-edge` install, no `styles.xml` edit. - 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.
Navigation setup (required - blueprints assume it)
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 (see [RULES.md](RULES.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 in [references/CHAT-REACT-NATIVE-blueprints.md`](references/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.
New app continuation
After scaffold and packages:
1. Use `references/DOCS.md` to fetch the appropriate manifest (Chat, Video, or Feeds) and selected Installation markdown page. 2. Confirm the installed Stream package matches the selected docs and npm dist-tag. 3. Run `credentials.md` or wire the app's token provider plan. 4. Configure Babel (Chat: Reanimated/Worklets plugin) and root providers. 5. 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.
6. Start the dev server only when useful and feasible for the environment (npx expo start --dev-client, npm run ios, or npm run android).
---
3. Choose the integration lane
Resolve five things before editing an existing app:
1. Runtime: Expo or RN CLI 2. Product: Chat, Video, Feeds, or any combination (from Step 0 of `SKILL.md`) 3. Navigation: React Navigation, Expo Router, existing custom navigation, or no navigation 4. Scope: setup only, core Chat / Video / Feeds screens, optional native capability, or customization 5. 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`.
---
4. Install packages
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.
Chat - RN CLI lane
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-installChat - Expo lane
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-teleportExpo 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.
Video - RN CLI lane
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. No `react-native-edge-to-edge` install, no `styles.xml` edit. - 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.
Required iOS setup:
Info.plist: addNSCameraUsageDescriptionandNSMicrophoneUsageDescription- For ringing/VoIP, also include
voipandaudioinUIBackgroundModes
Video - Expo lane
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.
Feeds - RN CLI lane
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.
Feeds - Expo lane
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.
Video - optional capabilities
| 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.
Chat - optional packages by capability
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:
1. Identify the requested capability from the user request and manifest-selected docs. 2. Pick the package from the matrix for the detected runtime lane. 3. Install with the project's package manager for RN CLI, or npx expo install for Expo. 4. Add required platform permissions or Expo config plugins from the selected package docs. 5. 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. 6. 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.
---
5. Configure native/runtime requirements
Babel plugin (Chat only)
If Chat is in scope, ensure the Reanimated or Worklets plugin is the last Babel plugin:
module.exports = {
presets: ["module:@react-native/babel-preset"],
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), add the Reanimated/Worklets plugin as the last Babel plugin.
Entry point
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.
Permissions (Video only)
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.
Safe area and Android edge-to-edge
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. - 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).
---
6. Wire shared setup
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 by `call.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.
---
7. Load only the needed reference files
Use the requested screen/feature and product to choose the smallest relevant reference set.
Always load:
- `references/DOCS.md` for
llms.txtmanifest lookup
Then load the matching product references:
Chat work:
- `references/CHAT-REACT-NATIVE.md` for setup and gotchas
- `references/CHAT-REACT-NATIVE-blueprints.md` for screen/component blueprints
Video work:
- `references/VIDEO-REACT-NATIVE.md` for setup and gotchas
- `references/VIDEO-REACT-NATIVE-blueprints.md` for screen/component blueprints
Feeds work:
- `references/FEEDS-REACT-NATIVE.md` for setup and gotchas
- `references/FEEDS-REACT-NATIVE-blueprints.md` for 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.
---
8. Existing app modification flow
Use this when the request is a targeted Chat, Video, or Feeds change in an existing app.
1. Detect runtime, product(s), and currently installed Stream packages. 2. Use `references/DOCS.md` to fetch the relevant manifest (Chat, Video, or Feeds) and selected markdown page for the requested area. 3. Open the matching blueprint section in the product's *-blueprints.md. 4. For cookbook-style requests, use `references/DOCS.md` manifest search and fetch the best matching cookbook/customization markdown page. 5. 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, ...).
6. 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.
---
9. Verify before you stop
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:
- 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 by `callingState !== 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.
Stream React Native - credentials, token, and demo data
Run this once per session for tracks A, B, and D before writing connected Chat, Video, or Feeds code or creating requested demo data. Track C does not need credentials.
Goal
Collect the Stream API key, user id, and user token so the app can connect to real Stream data on first run. The API key + token flow is identical for Chat, Video, and Feeds.
- Chat can also seed optional demo data (channels, users, messages) - see Step C.
- Video needs no demo/seed data: calls are ephemeral and created at runtime by
client.call(type, id), so skip Step C for Video-only sessions and confirm only the API key, user, and token. - Feeds can also seed optional demo data (users, follows, activities) - see Step C (Feeds branch). A freshly scaffolded Feeds app renders empty until the connected user has someone to follow and activities to read, so demo data is what makes a new app actually demoable.
For a brand-new app, scaffolding may happen before this file if the runtime or target directory must be resolved first.
This flow uses the `getstream` CLI (binary name getstream). It is the same getstream CLI used across the pack. If the CLI is missing, ask the user to install it from https://getstream.io and wait - never fetch or run an install script yourself.
Single upfront question (ask exactly once, then act immediately)
Post one message asking all relevant things together. Do not split into multiple rounds. Include question 4 (demo data) only when Chat is in scope - for Video-only sessions, drop it (calls need no seed data):
To wire Stream with real data, I need a few quick answers:
>
1. Credentials - Should I fetch your API key via the Stream CLI and generate a token, or will you paste them yourself?
2. User - What user id and display name should the app connect as?
3. Token expiry - If I am generating the token: should it expire? (for example1h,1d,30m) or never expire?
4. Demo data (Chat and Feeds) - Do you want me to create demo data? For Chat: namespaced channels (and optionally demo users and messages). For Feeds: namespaced demo users, follows from your timeline to their feeds, and a few activities on each so your timeline is not empty. (I will namespace every demo id under a per-session demo-<random>- prefix, and confirm with you before writing into a Stream app that already has real data.)>
If you want to handle credentials yourself, paste your API key and token (and, for Chat / Feeds, tell me whether to create demo data).
After the user replies - act without further prompting
Once the user answers, execute the needed CLI steps in sequence without pausing between them. Narrate each step briefly, but do not ask "shall I continue?" between steps. Demo data calls are mutating; run them only when the user asked for them.
Step A0 - Confirm CLI install and auth
Detect the binary:
command -v getstreamIf getstream does not resolve, ask the user to install it from https://getstream.io and wait - never fetch or run an install script yourself. Do not continue until command -v getstream resolves, unless the user chooses to paste API key and token manually.
Authentication and app selection are handled in Step A below: run the command and follow its output.
Step A - API key
If the user wants CLI-based credentials:
getstream env --target expogetstream env writes the app's public API key to .env as EXPO_PUBLIC_STREAM_API_KEY; read it in code with process.env.EXPO_PUBLIC_STREAM_API_KEY. You don't need to hold the key yourself, and the secret is never written. (Bare RN CLI without Expo: getstream env has no native target yet - inline the public key or wire it through your env library; never the secret.) If getstream env reports the project isn't initialized or you're not signed in, run getstream init, then re-run it. If the user pastes an API key, use it directly and skip this step.
Echo the selected app to the user before any mutation: before running Step C, say Selected Stream app: "<app_name>" (api_key: <last_4_chars>) so a misconfigured project is caught before it writes to the wrong app. If the user expected a different app, run getstream init to switch it before continuing.
Step B - Token
Generate a token for the chosen user id. getstream token accepts a duration string; omit --ttl for a never-expiring local dev token.
# Never-expiring local dev token
getstream token <user_id>
# Expiring token
getstream token <user_id> --ttl 1hHold the token in context for code edits. Do not print it in summaries.
If the user pastes a token, hold it in context and skip generation.
Step C - Demo data (Chat and Feeds, and only if the user asked)
Video needs no demo data - skip this step for Video-only sessions. Chat and Feeds both have a demo-data flow; pick the branch(es) that match the products in scope:
- Chat - C1 (users), C2 (channels), C3 (messages)
- Feeds - C1 (users, shared with Chat), C4 (follows), C5 (activities), C6 (
foryoufeed group config), C7 (reactions soforyouhas content)
For Chat, create 3 to 5 channels with realistic usernames; the connected user must be a member of at least one demo channel, or ChannelList will render empty. For Feeds, create 3 to 5 demo users, have the connected user's timeline follow each of their user feeds, post a few activities on each demo user's feed so the timeline has content, configure the foryou feed group with the popular selector, and seed one reaction per activity so the popular score clears min_popularity and foryou is not empty on first launch.
These calls are mutating. All demo ids must be namespaced so they cannot collide with real users, channels, follows, or activities that already exist in the selected Stream app.
C0 - Pre-mutation safety check
Before running any UpdateUsers / GetOrCreateChannel / SendMessage / GetOrCreateFollows / AddActivity / UpdateFeedGroup / AddActivityReaction:
1. Generate a per-session demo prefix and hold it in context. Default form: demo-<short_random>- where <short_random> is 4-6 lowercase chars (e.g., demo-k3p9-). Every demo user id, channel id, activity id, and seeded record custom field uses this prefix. Do not reuse a prefix across sessions - generate a fresh one each time so retries land in a fresh namespace. 2. Detect whether the selected app already has real data. For Chat in scope, run getstream api QueryChannels --request '{"filter_conditions":{"type":"messaging"},"limit":1}'. For Feeds in scope, run getstream api QueryActivities --request '{"limit":1}'. Check whether either response includes records that do not start with a demo- prefix. 3. Confirm explicitly when the app is non-empty. If real channels or activities exist, surface the count and a sample id, and require the user to type a confirmation before continuing:
Selected Stream app"<app_name>"already has real data (e.g.,<example_cid_or_activity_id>). I am about to create demo users / channels / follows / activities namespaced under<demo_prefix>so they cannot collide. Confirm withseed demoto proceed, or saycancel.
4. Empty / dev app: announce and proceed without explicit confirmation:
Selected Stream app"<app_name>"looks empty. Creating namespaced demo data under prefix<demo_prefix>now.
If the user cancels, stop Step C and return to Step D with credentials only.
Route demo data through the getstream CLI; confirm endpoint and body shapes with getstream api -h. Only run mutating calls after the explicit demo-data request and the confirmation above.
C1 - Create namespaced user records (shared by Chat and Feeds)
User records must exist before channel membership can be added. Apply the demo prefix to every demo user id (the token user keeps its own id - that is the user the app will connect as):
# <demo_prefix> e.g. demo-k3p9-
getstream api UpdateUsers --request '{"users":{"<token_user_id>":{"id":"<token_user_id>","name":"<display_name>"},"<demo_prefix>alice":{"id":"<demo_prefix>alice","name":"Alice (demo)"},"<demo_prefix>bob":{"id":"<demo_prefix>bob","name":"Bob (demo)"},"<demo_prefix>carol":{"id":"<demo_prefix>carol","name":"Carol (demo)"}}}'UpdateUsers is upsert - it is safe to re-run. The same demo users serve both Chat (as channel members) and Feeds (as the owners of user:<demo_user_id> feeds that the connected user's timeline will follow).
C2 - Create each channel with members (namespaced ids) [Chat]
Use GetOrCreateChannel. Prefix every channel id with <demo_prefix> and tag the channel with a seeded_by_skill: true marker in data.custom so later runs can detect this skill's own seeded data:
getstream api GetOrCreateChannel --type messaging --id <demo_prefix>general --request '{"data":{"name":"General (demo)","created_by_id":"<token_user_id>","members":[{"user_id":"<token_user_id>"},{"user_id":"<demo_prefix>alice"},{"user_id":"<demo_prefix>bob"}],"custom":{"seeded_by_skill":true,"demo_prefix":"<demo_prefix>"}}}'Use namespaced channel ids such as <demo_prefix>general, <demo_prefix>random, <demo_prefix>team-alpha. Make sure the token user appears in data.members. GetOrCreateChannel is idempotent on the (type, id) pair - re-running with the same prefix returns the existing channel rather than duplicating it.
After creating demo channels, summarize without secrets and without printing user tokens:
Created demo channels in"<app_name>":<demo_prefix>general(<token_user_id>, <demo_prefix>alice, <demo_prefix>bob),<demo_prefix>random(<token_user_id>, <demo_prefix>carol),<demo_prefix>team-alpha(<token_user_id>, <demo_prefix>alice)
C3 - Send demo messages idempotently (only if the user asked for messages or more demo data) [Chat]
Use SendMessage (confirm its body shape with getstream api SendMessage -h). Each message's user_id must belong to an existing user (so use the namespaced demo users from C1, or the token user). Tag every seeded message with a stable custom.seed_key so a re-run can detect and skip already-seeded messages.
Before sending, check whether the channel already contains a message with the same seed_key:
# Skip-if-present check (one query per (channel, seed_key)):
getstream api QueryChannels --request '{"filter_conditions":{"type":"messaging","cid":"messaging:<demo_prefix>general"},"messages_limit":50}'
# If the returned messages already include one whose custom.seed_key matches
# <seed_key>, skip the send for that key.Then send only the missing messages:
getstream api SendMessage --type messaging --id <demo_prefix>general --request '{"message":{"text":"Hello from Alice","user_id":"<demo_prefix>alice","custom":{"seed_key":"<demo_prefix>general:hello-1","seeded_by_skill":true}}}'Generate seed_key deterministically per channel + index (<demo_prefix><channel_short>:hello-1, :hello-2, ...). A second SendMessage with the same seed_key should be skipped client-side - the Stream API itself does not dedupe on custom fields, so the skip-if-present check above is what makes seeding safe to retry.
Do not send demo messages when the user only asked for credentials or channels.
C4 - Have the connected user's timeline follow each demo user [Feeds]
A timeline feed only renders activities from feeds it follows, so a Feeds app stays empty until the connected user's timeline follows somebody. Use GetOrCreateFollows (idempotent batch upsert) to set up follows from timeline:<token_user_id> to each demo user's user: feed:
getstream api GetOrCreateFollows --request '{"follows":[{"source":"timeline:<token_user_id>","target":"user:<demo_prefix>alice"},{"source":"timeline:<token_user_id>","target":"user:<demo_prefix>bob"},{"source":"timeline:<token_user_id>","target":"user:<demo_prefix>carol"}]}'GetOrCreateFollows is upsert - re-running is safe, existing follows are returned without error. The endpoint also broadcasts FollowAddedEvent only for newly created follows, so re-runs do not double-fire notifications.
If the user wants their own posts on their own timeline (the "self-follow"), the skill's blueprint OwnFeedsContextProvider already establishes that client-side on app boot. You can also seed it here by adding {"source":"timeline:<token_user_id>","target":"user:<token_user_id>"} to the follows array.
C5 - Post demo activities idempotently [Feeds]
For each demo user, post 1-3 activities on their user: feed via AddActivity. Required fields: feeds (target feed array), type. Use a deterministic id per (user, index) so retries are idempotent - re-running with the same id returns the existing activity instead of creating a duplicate.
getstream api AddActivity --request '{"id":"<demo_prefix>alice-1","feeds":["user:<demo_prefix>alice"],"user_id":"<demo_prefix>alice","type":"post","text":"Just shipped a new feature! Activity feeds are wild.","custom":{"seeded_by_skill":true,"demo_prefix":"<demo_prefix>"}}'
getstream api AddActivity --request '{"id":"<demo_prefix>alice-2","feeds":["user:<demo_prefix>alice"],"user_id":"<demo_prefix>alice","type":"post","text":"Loving the React Native ecosystem lately.","custom":{"seeded_by_skill":true,"demo_prefix":"<demo_prefix>"}}'
getstream api AddActivity --request '{"id":"<demo_prefix>bob-1","feeds":["user:<demo_prefix>bob"],"user_id":"<demo_prefix>bob","type":"post","text":"Stream Feeds makes social apps surprisingly simple.","custom":{"seeded_by_skill":true,"demo_prefix":"<demo_prefix>"}}'
getstream api AddActivity --request '{"id":"<demo_prefix>carol-1","feeds":["user:<demo_prefix>carol"],"user_id":"<demo_prefix>carol","type":"post","text":"Anyone else excited about the new SDK?","custom":{"seeded_by_skill":true,"demo_prefix":"<demo_prefix>"}}'Generate ids deterministically per user + index (<demo_prefix>alice-1, :alice-2, ...). Tag every seeded activity with custom.seeded_by_skill: true and custom.demo_prefix: <demo_prefix> so later runs can identify this skill's seeded data.
After the follows and activities are in place, the connected user's timeline will render the demo users' posts on first launch.
C6 - Configure the foryou feed group [Feeds]
foryou ships with no activity_selectors configured, so the connected user's foryou feed returns empty until a selector is set on the group. UpdateFeedGroup is idempotent (PUT) - safe to re-run with the same body.
# `popular` formula: reactions + comments*2 + bookmarks*3 + shares*3.
# min_popularity: 1 means "at least one reaction-equivalent in cutoff_window".
# cutoff_window 7d is the default but worth making explicit so the selector
# does not silently drift if defaults change.
getstream api UpdateFeedGroup --id foryou \
--request '{"activity_selectors":[{"type":"popular","min_popularity":1,"cutoff_window":"7d"}]}'This is a server-side config on the feed group itself, not on a per-user feed. It applies to every foryou:<user_id> feed in the app. Once configured, every user's foryou feed will return activities that match the selector.
C7 - Seed one reaction per activity [Feeds]
The popular selector picks activities by their popularity score. A freshly seeded activity has score 0, so it does not clear min_popularity: 1 and does not appear in foryou even after C6. Adding one reaction per activity bumps the score to 1 and makes the activity eligible. Use AddActivityReaction (one call per activity). Use a non-<token_user_id> reactor (e.g. carol reacts to alice) so the demo looks realistic - the connected user does not see their own reaction pre-filled on every post.
getstream api AddActivityReaction --activity-id <demo_prefix>alice-1 --request '{"type":"like","user_id":"<demo_prefix>carol"}'
getstream api AddActivityReaction --activity-id <demo_prefix>alice-2 --request '{"type":"like","user_id":"<demo_prefix>bob"}'
getstream api AddActivityReaction --activity-id <demo_prefix>bob-1 --request '{"type":"like","user_id":"<demo_prefix>alice"}'
getstream api AddActivityReaction --activity-id <demo_prefix>carol-1 --request '{"type":"like","user_id":"<demo_prefix>alice"}'AddActivityReaction is not idempotent by default - a second call with the same (activity_id, type, user_id) will fail with "reaction already exists" or, with enforce_unique: true, replace the existing reaction. The safest pattern for re-runs is to skip silently on the "already exists" error rather than crash the seeding flow. Adding more variety (type: "love" from one user, type: "like" from another) is fine - the popularity formula counts all reactions equally.
After C6 + C7, summarize without printing tokens:
Configuredforyoufeed group withpopularselector (min_popularity: 1,cutoff_window: 7d). Seeded 1 like per demo activity so they clear the popularity threshold. The connected user'sforyoutab should render these on first launch.
Do not run C4 through C7 when Feeds is not in scope, or when the user only asked for credentials. If only C4 + C5 ran (timeline-only demo, no Explore tab), foryou stays empty and that is fine - the Home tab still works.
Step D - Proceed automatically
After credentials and requested demo data succeed, return to `SKILL.md` and continue into `builder.md`. No additional prompt is needed.
When generating a local demo form, prefill the editable API key, token, user, and channel values from this flow by default. Do not print user tokens in final summaries.
If any CLI step fails and cannot be recovered, ask the user to paste the missing API key or token manually before editing code.
What NOT to do
- Never put the API secret in app code, Expo config, native files, or chat.
- Never invent credentials.
- Never ask "should I continue?" between Step A, B, C, and D after the upfront answers.
- Never use
CreateChannel; useGetOrCreateChannel. - Never use
CreateUser; useUpdateUsers. - Never assume
created_by_idadds a member. Membership must be set throughdata.members. - Never pass bare user id strings as channel members. Use
[{"user_id":"alice"}]. - Never put channel members at the top level of the
GetOrCreateChannelbody. - Never write demo data with generic ids like
alice,bob,general,random,team-alpha. Generic ids collide with real users/channels in any non-empty Stream app and silently mutate production data. Always apply the per-session<demo_prefix>from Step C0. - Never skip the Step C0 pre-mutation check. A misconfigured CLI default can point at a production app - echo the app name + (only) the api key's last 4 chars and require explicit confirmation before seeding into a non-empty app.
- Never send unmarked demo messages. Tag with
custom.seeded_by_skill: trueand a deterministiccustom.seed_key, and run the skip-if-present query before each send so retries do not duplicate the seed. - Never send a `user_id` to the customer's token endpoint from the client. The server must derive the Stream user id from its own authenticated session (see `sdk.md` > Auth model and the Production auth gate blueprint).
Chat React Native - Screen and Component Blueprints
Load only the section you are implementing. For llms.txt manifest search, see DOCS.md. For setup, packages, and gotchas, see CHAT-REACT-NATIVE.md.
Expo lane: change imports from "stream-chat-react-native" to "stream-chat-expo" unless the symbol comes from React Navigation, React Native, or stream-chat.
---
Request -> Blueprint section
| Request | Read section |
|---|---|
| root setup, providers, auth gate, login | App Provider and Auth Gate |
| brand new React Native or Expo app | Fresh App Scaffold |
| channel list, conversation list, channel tap | Channel List Screen |
| message list, message composer, chat screen | Channel Screen |
| optional native capability | DOCS.md -> primary manifest lookup, then Optional Native Capability Blueprint |
| thread navigation, replies, thread list | Thread Screen or Thread List Screen |
| React Navigation or Expo Router shell | Navigation Shell |
| theme, dark mode, colors, design tokens | Theming Blueprint |
| UI slot, component, behavior, or composer customization | DOCS.md -> primary manifest lookup, then Component Override Blueprint |
| offline support or sign-out cleanup | Offline and Sign-out Blueprint |
If no row matches, read DOCS.md and CHAT-REACT-NATIVE.md first, then verify symbols in manifest-selected docs or the installed package before coding.
---
App Provider and Auth Gate
Use this when adding Stream Chat to the app root. Replace static credentials with values from the app's auth flow or `../credentials.md`.
import React, { useCallback, useState } from "react";
import { ActivityIndicator, Button, TextInput, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
Chat,
OverlayProvider,
useCreateChatClient,
} from "stream-chat-react-native";
type Session = {
apiKey: string;
token: string;
userId: string;
userName: string;
};
const Loading = () => (
<View style={{ alignItems: "center", flex: 1, justifyContent: "center" }}>
<ActivityIndicator size="large" />
</View>
);
const LoginScreen = ({
demoDefaults,
onSession,
}: {
demoDefaults?: Partial<Session>;
onSession: (session: Session) => void;
}) => {
const [apiKey, setApiKey] = useState(demoDefaults?.apiKey ?? "");
const [token, setToken] = useState(demoDefaults?.token ?? "");
const [userId, setUserId] = useState(demoDefaults?.userId ?? "");
const [userName, setUserName] = useState(demoDefaults?.userName ?? "");
const signIn = useCallback(async () => {
if (apiKey && token && userId) {
onSession({ apiKey, token, userId, userName: userName || userId });
return;
}
const response = await fetch(
`https://your-api.example.com/stream-token?user_id=${encodeURIComponent(userId)}`,
);
const body = await response.json();
onSession({
apiKey: body.apiKey,
token: body.token,
userId,
userName: body.userName || userName || userId,
});
}, [apiKey, onSession, token, userId, userName]);
return (
<View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
<TextInput
autoCapitalize="none"
onChangeText={setApiKey}
placeholder="API key"
value={apiKey}
/>
<TextInput
autoCapitalize="none"
onChangeText={setToken}
placeholder="User token"
value={token}
/>
<TextInput
autoCapitalize="none"
onChangeText={setUserId}
placeholder="User id"
value={userId}
/>
<TextInput
autoCapitalize="words"
onChangeText={setUserName}
placeholder="User name"
value={userName}
/>
<Button disabled={!userId} onPress={signIn} title="Sign in" />
</View>
);
};
const ConnectedChat = ({
children,
session,
}: {
children: React.ReactNode;
session: Session;
}) => {
const chatClient = useCreateChatClient({
apiKey: session.apiKey,
tokenOrProvider: session.token,
userData: { id: session.userId, name: session.userName },
});
if (!chatClient) return <Loading />;
return (
<OverlayProvider>
<Chat client={chatClient}>{children}</Chat>
</OverlayProvider>
);
};
export const StreamChatRoot = ({
children,
demoDefaults,
}: {
children: React.ReactNode;
demoDefaults?: Partial<Session>;
}) => {
const [session, setSession] = useState<Session | null>(null);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{session ? (
<ConnectedChat session={session}>{children}</ConnectedChat>
) : (
<LoginScreen demoDefaults={demoDefaults} onSession={setSession} />
)}
</GestureHandlerRootView>
);
};Wiring:
useCreateChatClientreturnsStreamChat | null.- Clearing
sessionunmountsConnectedChatand lets the hook disconnect. - For production, fetch tokens from the app backend.
- For local demos, prefill editable form values from CLI-generated API key, token, user, and channel setup when available.
- Do not print user tokens in final summaries or logs.
---
Fresh App Scaffold
Use this when the current directory is empty or the user asks for a brand-new React Native or Expo Chat app. Do not cover full React Native environment setup. Scaffold the app, install Stream Chat and mandatory peers, wire the root providers, and create the first Chat screens.
Expo:
Replace MyChatApp with the target directory, or use . only when the current directory is empty and the user asked to scaffold in place.
npx create-expo-app@latest MyChatApp
cd MyChatApp
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
npx expo prebuildRN CLI:
Replace MyChatApp with the target directory, or use . only when the current directory is empty and the selected RN CLI supports in-place init.
npx @react-native-community/cli@latest init MyChatApp
cd MyChatApp
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-installInstall navigation (required - blueprints below assume it). RN CLI has no navigation by default and the bundled blueprints import from @react-navigation/* (including useHeaderHeight from @react-navigation/elements). For RN CLI: npm install @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screens then npx pod-install. For Expo, create-expo-app ships Expo Router under app/ - skip the React Navigation install and use the Expo Router branch of the Navigation Shell blueprint instead. (Expo apps that prefer React Navigation can npx expo install the same four packages.)
After scaffolding and navigation install, continue with these sections in order: App Provider and Auth Gate, Navigation Shell, Channel List Screen, and Channel Screen. Start Expo with npx expo start --dev-client; do not target Expo Go. Optional native capabilities stay opt-in and use the dependency map in CHAT-REACT-NATIVE.md.
---
Optional Native Capability Blueprint
Use this when the user asks for a capability that needs extra native packages beyond the required Stream Chat peers.
Dependency choice and install flow
1. Use DOCS.md to fetch the manifest-selected docs for the requested capability. 2. Read CHAT-REACT-NATIVE.md > Optional dependency map. 3. Install only the packages needed for the requested capability. 4. Add the permissions, config plugins, pods, or prebuild steps required by those packages. 5. Verify the capability in the screen that uses it.
Screen wiring
Keep optional UI inside the same provider and Channel hierarchy as the core Chat screen unless the manifest-selected docs require otherwise.
Expo Router SDK 56+ swap. Same caveat as Channel Screen above — replaceuseHeaderHeight()with thePlatform.OS + useSafeAreaInsets().toprecipe so this screen doesn't drag@react-navigation/elementsinto an SDK 56+ project. See Channel Screen for the snippet.
import React, { useMemo } from "react";
import { useHeaderHeight } from "@react-navigation/elements";
import {
Channel,
MessageComposer,
MessageList,
useChatContext,
} from "stream-chat-react-native";
export const ChannelScreenWithNativeCapability = ({ route }) => {
const { channelCid } = route.params;
const { client } = useChatContext();
const headerHeight = useHeaderHeight();
const channel = useMemo(() => {
const [type, id] = channelCid.split(":");
return client.channel(type, id);
}, [channelCid, client]);
return (
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<MessageList />
<MessageComposer />
</Channel>
);
};Wiring:
- Keep
MessageComposerinsideChannel. - Pair
keyboardVerticalOffsetwithtopInsetset to the same header height — without it, the attachment picker bottom sheet gets clamped short of its full snap point. - Add
bottomInsetonly when a specific layout requires it (e.g. tab bar that owns the bottom safe-area). - Use
WithComponentsfor custom buttons, previews, rows, or capability-specific UI slots.
---
Navigation Shell
Use this for React Navigation. Keep OverlayProvider above navigation screens and Chat stable.
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Chat, OverlayProvider } from "stream-chat-react-native";
export type RootStackParamList = {
Channels: undefined;
Channel: { channelCid: string };
Thread: undefined;
Threads: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export const NavigationShell = ({ chatClient }) => (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<OverlayProvider>
<NavigationContainer>
<Chat client={chatClient}>
<Stack.Navigator>
<Stack.Screen name="Channels" component={ChannelListScreen} />
<Stack.Screen name="Channel" component={ChannelScreen} />
<Stack.Screen name="Thread" component={ThreadScreen} />
<Stack.Screen name="Threads" component={ThreadListScreen} />
</Stack.Navigator>
</Chat>
</NavigationContainer>
</OverlayProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);Expo Router:
- Put
GestureHandlerRootView,SafeAreaProvider,OverlayProvider, andChatinapp/_layout.tsx. - Keep route files thin:
app/index.tsxfor channel list,app/channel/[cid].tsxfor channel screen,app/thread.tsxfor active thread if using global thread state.
---
Channel List Screen
Use stable filters and pass channel.cid when a user selects a channel.
import React, { useMemo } from "react";
import { ChannelList } from "stream-chat-react-native";
export const ChannelListScreen = ({ navigation, route }) => {
const userId = route.params?.userId;
const filters = useMemo(
() => ({ members: { $in: [userId] }, type: "messaging" }),
[userId],
);
const sort = useMemo(() => [{ last_message_at: -1 }], []);
const options = useMemo(() => ({ limit: 20, messages_limit: 30 }), []);
return (
<ChannelList
filters={filters}
onSelect={(channel) => navigation.navigate("Channel", { channelCid: channel.cid })}
options={options}
sort={sort}
/>
);
};Wiring:
filtersshould include the connected user for normal messaging lists.- Keep
filters,sort, andoptionsmemoized. - Do not pass
channelthrough navigation params. - For multiple lists, use
channelRenderFilterFnor event handler overrides to keep events from reordering unrelated lists.
---
Channel Screen
Recreate the channel from CID using the provided Chat client. Channel owns MessageList and MessageComposer.
Expo Router SDK 56+: theuseHeaderHeight()import below comes from@react-navigation/elements, which must not be installed on Expo Router 56+ — Metro halts withexpo-router is no longer compatible with react-navigation(see ../RULES.md > Expo Router SDK 56+ — no React Navigation). Swap that one import + theuseHeaderHeight()call for:
>
```tsx
import { Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const { top } = useSafeAreaInsets();
const headerHeight = (Platform.OS === "ios" ? 44 : 56) + top;
```
>
This matches what useHeaderHeight() returns internally (native-stack default 44pt iOS / 56dp Android + top safe-area inset). On RN CLI and Expo Router SDK ≤ 55, the original snippet stands unchanged.import React, { useMemo } from "react";
import { View } from "react-native";
import { useHeaderHeight } from "@react-navigation/elements";
import {
Channel,
MessageComposer,
MessageList,
useChatContext,
} from "stream-chat-react-native";
export const ChannelScreen = ({ navigation, route }) => {
const { channelCid } = route.params;
const { client } = useChatContext();
const headerHeight = useHeaderHeight();
const channel = useMemo(() => {
const [type, id] = channelCid.split(":");
return client.channel(type, id);
}, [channelCid, client]);
return (
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<View style={{ flex: 1 }}>
<MessageList />
<MessageComposer />
</View>
</Channel>
);
};Wiring:
Channelinitializes and watches the channel by default.- Use
keyboardVerticalOffset={headerHeight}for navigation headers, and pass the same value astopInsetso the attachment picker bottom sheet reaches its full snap point. - Add
bottomInsetonly when a specific layout requires it (e.g. tab bar that owns the bottom safe-area). - If implementing threads, store the selected thread in context or parent state. See Thread Screen.
---
Thread Screen
Use explicit thread state. The main channel should receive the active thread while the thread screen is open; the thread screen renders Channel with threadList.
import React, { createContext, useContext, useMemo, useState } from "react";
import type { LocalMessage } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
Thread,
useChatContext,
} from "stream-chat-react-native";
const ThreadStateContext = createContext<{
setThread: (thread?: LocalMessage) => void;
thread?: LocalMessage;
}>({ setThread: () => undefined });
export const useThreadState = () => useContext(ThreadStateContext);
export const ThreadStateProvider = ({ children }) => {
const [thread, setThread] = useState<LocalMessage | undefined>();
return (
<ThreadStateContext.Provider value={{ setThread, thread }}>
{children}
</ThreadStateContext.Provider>
);
};
export const ChannelScreenWithThreads = ({ navigation, route }) => {
const { channelCid } = route.params;
const { client } = useChatContext();
const { setThread, thread } = useThreadState();
const channel = useMemo(() => {
const [type, id] = channelCid.split(":");
return client.channel(type, id);
}, [channelCid, client]);
return (
<Channel channel={channel} thread={thread}>
<MessageList
onThreadSelect={(selectedThread) => {
setThread(selectedThread);
navigation.navigate("Thread", { channelCid });
}}
/>
<MessageComposer />
</Channel>
);
};
export const ThreadScreen = ({ route }) => {
const { channelCid } = route.params;
const { client } = useChatContext();
const { setThread, thread } = useThreadState();
const channel = useMemo(() => {
const [type, id] = channelCid.split(":");
return client.channel(type, id);
}, [channelCid, client]);
if (!thread) return null;
return (
<Channel channel={channel} thread={thread} threadList>
<Thread onThreadDismount={() => setThread(undefined)} />
</Channel>
);
};Wiring:
Threadmust render insideChannel.threadListmarks the screen as thread mode.onThreadDismountshould clear the active thread.- Offline mode does not support thread access in the referenced docs.
---
Thread List Screen
Use this when the user asks for a list of threads.
import React from "react";
import { useIsFocused } from "@react-navigation/native";
import { ThreadList } from "stream-chat-react-native";
export const ThreadListScreen = ({ navigation }) => {
const isFocused = useIsFocused();
const { setThread } = useThreadState();
return (
<ThreadList
isFocused={isFocused}
onThreadSelect={async (selectedThread, channel) => {
setThread(selectedThread.thread);
navigation.navigate("Thread", {
channelCid: channel.cid,
});
}}
/>
);
};Wiring:
ThreadListmust render insideChat.onThreadSelectreceives(thread, channel).- Keep list item customization lightweight.
---
Theming Blueprint
Put overlay-level style on OverlayProvider and Chat style on Chat. Keep the object stable.
import React, { useMemo } from "react";
import type { DeepPartial, Theme } from "stream-chat-react-native";
import { Chat, OverlayProvider } from "stream-chat-react-native";
export const ThemedChat = ({ chatClient, children }) => {
const chatTheme = useMemo<DeepPartial<Theme>>(
() => ({
messageItemView: {
content: {
markdown: {
text: {
fontSize: 16,
},
},
},
},
}),
[],
);
return (
<OverlayProvider value={{ style: chatTheme }}>
<Chat client={chatClient} style={chatTheme}>
{children}
</Chat>
</OverlayProvider>
);
};Wiring:
- Prefer semantic tokens from the SDK when reading theme values.
- Do not mutate theme objects inline during render.
- Overlay components do not inherit only from
Chat; pass style throughOverlayProvidertoo.
---
Component Override Blueprint
Use WithComponents for custom subcomponents. Keep custom message rows memoized and use SDK context hooks.
import React, { memo } from "react";
import {
Channel,
MessageComposer,
MessageList,
WithComponents,
} from "stream-chat-react-native";
const CustomSlotComponent = memo(() => {
// Use the context hook documented for the selected slot.
return null;
});
const overrides = {
DocumentedSlotName: CustomSlotComponent,
};
export const CustomChannel = ({ channel }) => (
<WithComponents overrides={overrides}>
<Channel channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
</WithComponents>
);Wiring:
- Prefer the smallest documented override that satisfies the requested customization.
- Avoid replacing core message components unless required.
- If replacing message row structure and still using the long-press overlay, preserve overlay anchor behavior by reading the manifest-selected context docs.
---
Offline and Sign-out Blueprint
Use this only when offline support is requested.
import React, { useCallback } from "react";
import { Button } from "react-native";
import { Chat, useChatContext } from "stream-chat-react-native";
export const OfflineChat = ({ chatClient, children }) => (
<Chat client={chatClient} enableOfflineSupport>
{children}
</Chat>
);
export const SignOutButton = ({ onSignedOut }) => {
const { client } = useChatContext();
const signOut = useCallback(async () => {
await client.offlineDb?.resetDB();
await client.disconnectUser();
onSignedOut();
}, [client, onSignedOut]);
return <Button onPress={signOut} title="Sign out" />;
};Wiring:
- Install
@op-engineering/op-sqlite. - Expo apps use the dev-client/native-build lane by default; do not target Expo Go.
- Reset DB before disconnecting.
- Do not promise offline thread access.
Chat React Native - Setup and Integration
Stream Chat React Native provides pre-built Chat UI for React Native CLI and Expo apps. This file covers packages, app setup, client/auth patterns, navigation, offline support, and gotchas. For llms.txt docs lookup, see DOCS.md. For screen structures, see CHAT-REACT-NATIVE-blueprints.md.
Rules: ../RULES.md (Chat-only, New Architecture, secrets, runtime lane ownership, provider placement, blueprint reads).
Manifest-selected docs are the authority. Use DOCS.md before installing packages or making API-specific claims.
---
Quick ref
| Area | RN CLI | Expo |
|---|---|---|
| Chat package | stream-chat-react-native | stream-chat-expo |
| Required peers | @react-native-community/netinfo, react-native-gesture-handler, react-native-reanimated, react-native-teleport, react-native-worklets, react-native-svg | @react-native-community/netinfo, expo-image-manipulator, react-native-gesture-handler, react-native-reanimated, react-native-svg, react-native-teleport |
| Install command | package manager install | npx expo install |
| Root wrapper | GestureHandlerRootView | GestureHandlerRootView in App.tsx or app/_layout.tsx |
First path:
1. Pick RN CLI vs Expo. 2. Use DOCS.md to fetch the manifest-selected Installation page and verify npm dist-tags. 3. Install package and required peers. 4. Add Reanimated or Worklets Babel plugin last. 5. Wrap root with GestureHandlerRootView. 6. Place OverlayProvider and Chat high in the tree. 7. Use useCreateChatClient for normal auth. 8. Render ChannelList, Channel, MessageList, MessageComposer, and optional Thread.
Full screen blueprints: CHAT-REACT-NATIVE-blueprints.md. Load only the section you are implementing.
---
App Integration
Installation
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
npx pod-installExpo:
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 prebuildInstall @latest only after confirming the npm dist-tag matches the selected docs. If not, use the manifest-selected docs' tag or exact version.
Optional dependency map
Optional dependencies are opt-in native capability packages, not default Chat requirements. Use this map only after the user asks for the capability or after manifest-selected docs require it.
Add optional dependencies with the runtime's normal install lane:
- RN CLI: use the project's package manager, then run pods after native packages change.
- Expo: use
npx expo installso versions match the Expo SDK. - Expo Chat apps use a dev-client/native-build lane by default because the SDK includes native code. Do not target Expo Go.
- If an Expo app does not already have native projects, run
npx expo prebuild; run it again when native config changes need to be regenerated. - Add platform permissions and config plugins from the selected package docs.
| Feature | Packages |
|---|---|
| React Navigation safe areas | RN CLI: react-native-safe-area-context; Expo: npx expo install react-native-safe-area-context |
| Native multipart upload progress | RN CLI: none beyond required Stream peers; Expo: none beyond dev-client lane |
| Attachment picker with built-in image media library | RN CLI: @react-native-camera-roll/camera-roll; Expo: expo-media-library |
| Native image picker / camera image upload | RN CLI: react-native-image-picker; Expo: expo-image-picker |
| File attachments / document picker | RN CLI: @react-native-documents/picker; Expo: expo-document-picker |
| Attachment sharing outside the app | RN CLI: react-native-blob-util react-native-share; Expo: expo-sharing |
| Video playback / video attachments | RN CLI: react-native-video; Expo: expo-video |
| Voice recording and audio attachments | RN CLI: react-native-video react-native-audio-recorder-player react-native-blob-util; Expo SDK 53+: expo-audio; Expo SDK 51/52: expo-av |
| Copy message | RN CLI: @react-native-clipboard/clipboard; Expo: expo-clipboard |
| Haptic feedback | RN CLI: react-native-haptic-feedback; Expo: expo-haptics |
| Offline support | RN CLI: @op-engineering/op-sqlite; Expo: @op-engineering/op-sqlite |
| High-performance message list | RN CLI: @shopify/flash-list; Expo: @shopify/flash-list |
What the common entries mean:
- Media library packages let the SDK or app read existing photos/videos from the device library.
- Native multipart upload progress uses
useNativeMultipartUpload={true}onChat; Expo already uses the dev-client/native-build lane. - Image picker packages let the app open native picker and camera capture flows.
- Document picker packages let the app choose arbitrary files outside the media library.
- Sharing packages let the app hand an attachment to another app.
- Audio packages add recording or playback primitives and usually need microphone permissions.
- Offline storage packages add a local database and require native code.
- List virtualization packages are performance helpers, not required for basic Chat screens.
If the user request is ambiguous, inspect the selected docs and existing app behavior before installing. For example, image library access, camera capture, document picking, and sharing are separate native capabilities and should not all be installed for one vague request.
After dependencies are installed, keep optional UI inside the normal Channel and MessageComposer flow unless manifest-selected docs require a different placement.
Babel and entry point
The Reanimated or Worklets plugin must be last:
module.exports = {
presets: ["module:@react-native/babel-preset"],
plugins: [
// other plugins
"react-native-worklets/plugin",
],
};Use react-native-reanimated/plugin when the app is on Reanimated 3. Use react-native-worklets/plugin for Reanimated 4+.
Wrap the entry point:
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* app */}
</GestureHandlerRootView>
);
}Client setup
Use useCreateChatClient for normal connection and cleanup:
import {
Chat,
OverlayProvider,
useCreateChatClient,
} from "stream-chat-react-native";
const chatClient = useCreateChatClient({
apiKey,
tokenOrProvider,
userData: { id: userId, name: userName },
});
if (!chatClient) return null;
return (
<OverlayProvider>
<Chat client={chatClient}>{children}</Chat>
</OverlayProvider>
);For Expo, import from stream-chat-expo.
Token route pattern
Production apps should use a backend route that upserts the current user and returns a Stream user token:
// Server-side only
import { StreamChat } from "stream-chat";
const serverClient = StreamChat.getInstance(apiKey, apiSecret);
await serverClient.upsertUsers([{ id: userId, name: userName }]);
const token = serverClient.createToken(userId);Client response shape can be:
{ apiKey: string, token: string, userId: string, userName: string }Local demo tokens can come from `../credentials.md`.
---
Core components
| Component/hook | Use |
|---|---|
useCreateChatClient | Creates, connects, returns `StreamChat |
OverlayProvider | Top-level overlay/image gallery/attachment picker provider |
Chat | Provides client, theme, translations, online state |
ChannelList | Queries and renders channels |
Channel | Provides channel, keyboard, messages, composer, attachment picker, and thread contexts |
MessageList | Renders messages inside Channel |
MessageComposer | Current message input; use instead of old message input patterns |
Thread | Renders replies for a selected parent message |
ThreadList | Renders an inbox of threads inside Chat |
WithComponents | Replaces component slots and subcomponents |
useChatContext | Reads the provided client inside Chat |
useMessageContext | Reads current message state in custom message subcomponents |
---
Navigation rules
- Put
OverlayProviderabove navigation screens; with React Navigation, prefer it aboveNavigationContainer. - Keep
Chathigh and stable so screen transitions do not reconnect the socket. - Pass
channel.cidthrough navigation params. Do not passChannelobjects. - Recreate a channel from
client.channel(type, id)in the destination screen. - Use
keyboardVerticalOffset={headerHeight}onChannel, and pair it withtopInset={headerHeight}so the attachment picker bottom sheet reaches its full snap point when a native navigation header is present. bottomInsetstays opt-in. Add it only when a specific layout requires it (e.g. a tab bar that owns the bottom safe-area).- For threads, pass the active
threadto the mainChannelwhile the thread screen is open and render the thread screen withthreadList.
---
Customization
Use DOCS.md to fetch the manifest-selected theming/customization page first. Prefer these in order:
1. Channel props for behavior changes. 2. Theme via OverlayProvider value={{ style }} and Chat style={style}. 3. WithComponents overrides for the documented slot that matches the requested customization. 4. Full core component replacement only when the smaller slots cannot satisfy the request.
WithComponents can wrap any subtree. Inner overrides merge over outer overrides.
---
Offline support
Offline support is opt-in:
npm install @op-engineering/op-sqliteExpo:
npx expo install @op-engineering/op-sqliteEnable it:
<Chat client={chatClient} enableOfflineSupport>
{children}
</Chat>Caveats from the manifest-selected docs:
- Expo apps already use a dev-client/native-build lane. Expo Go is not a supported target for this skill.
- Threads are not available in offline mode.
- Reset the DB on sign-out before disconnecting:
await chatClient.offlineDb?.resetDB();
await chatClient.disconnectUser();---
Expo SDK 55 → 56 changes
Expo SDK 56 changed several Chat-relevant defaults. Pick the right blueprint based on the SDK version reported by the project-signals probe in ../SKILL.md:
- *`@react-navigation/
is no longer co-installable withexpo-router`.** Metro halts with "As of SDK 56, expo-router is no longer compatible with react-navigation." On SDK 56+ use the Platform-based header-offset swap in CHAT-REACT-NATIVE-blueprints.md > Channel Screen. See also ../RULES.md > Expo Router SDK 56+ — no React Navigation. - `"edgeToEdgeEnabled": true` is no longer needed in `app.json`. Android 16 makes edge-to-edge mandatory;
expo prebuildwarns and ignores the entry. Safe to omit on SDK 56+; still required on SDK 53–55. - Reanimated 4 ships by default, so the last Babel plugin should be
react-native-worklets/plugin. Usereact-native-reanimated/pluginonly when the project pinned Reanimated 3. - React 19 / RN 0.85 baseline. Most Chat blueprints work unchanged, but third-party libraries that haven't bumped their peer ranges may need
--legacy-peer-depsduring install.
When in doubt, run the probe and check the EXPO_SDK line before applying any blueprint.
---
Gotchas
- The bundled references assume React Native New Architecture.
react-native-teleportis required for overlays.useCreateChatClientreturnsnullwhile connecting.- Never pass
nulltoChat. - Do not create multiple connected
StreamChatclients. - Do not pass
Channelinstances through navigation params. - A channel created with only a members list gets a generated id and cannot later add/remove members in the usual channel-id flow; use explicit ids when membership editing matters.
- Use
MessageComposerfor message input. - Use
WithComponentsfor component overrides instead of old prop-heavy override patterns. - Do not wrap
MessageComposerin extraSafeAreaViewto fix spacing; useChannelinsets. - Remove old Android negative
keyboardVerticalOffsethacks during migration. - Keep theme objects stable with
useMemo. - For upload progress, use
useNativeMultipartUpload={true}onChat. - On iOS Simulator, after fully closing and reopening the app, the first native multipart upload can fail while later uploads may proceed. Verify on a real device before treating it as a general SDK bug.
- If using push notifications, fetch the manifest-selected push notification docs before changing setup. Do not assume background WebSocket behavior or default prop values from memory.
Stream React Native - llms.txt docs lookup (Chat, Video, Feeds)
Use llms.txt manifests as the only docs entrypoint for Stream Chat, Stream Video, and Stream Feeds React Native work. Do not maintain direct page URLs in this skill. The manifest is an index, not the source: fetch the selected markdown page from the manifest before coding or making API-specific claims.
Pick the manifest set that matches the requested product. Most requests touch one product only; for combined apps (Chat + Video interop, Feeds + Chat, etc.), consult each product's manifest.
---
Manifests
Chat (React Native)
| Manifest | Use for |
|---|---|
https://getstream.io/chat/docs/sdk/react-native/llms.txt | Primary source for Stream Chat React Native UI SDK: installation, packages, components, contexts, theming, customization, UI cookbook, offline, push, New Architecture, native handlers, migration guides. |
https://getstream.io/chat/docs/react-native/llms.txt | Secondary source for low-level Chat API/client topics: tokens, users, channels, messages, query syntax, permissions, events, webhooks, push provider setup, imports/exports. |
Video (React Native)
| Manifest | Use for |
|---|---|
https://getstream.io/video/docs/react-native/llms.txt | Primary source for Stream Video React Native: installation (RN CLI and Expo), quickstart, calls and call lifecycle, call types, components (CallContent, CallControls, ParticipantView, LivestreamPlayer, HostLivestream, IncomingCall, OutgoingCall), UI cookbook, incoming calls / ringing setup, push providers (Firebase, APNs/PushKit), advanced topics (Chat-with-Video interop, PiP, broadcasting, custom video filters, screenshots), migration guides. |
Feeds (React Native)
| Manifest | Use for |
|---|---|
https://getstream.io/activity-feeds/docs/react-native/llms.txt | Primary source for Stream Feeds React Native: installation, tokens and authentication, user permissions, feed groups, aggregation, activity selectors, processors, stories, search, feed visibility, membership levels, activities, activity feedback, reactions, comments, user mentions, pins, event handling, contexts and hooks, state layer, logging, importing data, push providers and multi-bundle setup, ranking, notification feeds, For You feed, follows, members, query activities, bookmarks, file uploads, URL previews, polls, moderation, error handling, V2-to-V3 migration. |
The primary Chat manifest should identify itself as the current React Native docs. If it does not, treat that as a docs-version problem and verify from the manifest title before continuing. The same applies to the Video and Feeds manifests.
---
Lookup workflow
1. Identify the product (Chat, Video, or Feeds) from the user request, then fetch the matching primary manifest. 2. Search manifest link text for the exact component, hook, guide, or feature name. 3. Fetch the selected markdown URL from the manifest. 4. Confirm the markdown page matches the current React Native SDK docs when doing SDK work. 5. Code from the fetched markdown page plus this skill's rules and blueprints. 6. If the Chat primary manifest does not contain the topic and the request is low-level Chat API/client behavior, repeat the lookup in the Chat secondary manifest. 7. If multiple titles match, prefer the exact component or guide title over generic Overview pages.
Do not code from the manifest list alone. Do not paste or rely on direct docs URLs outside the manifests.
Before installing, verify current npm tags for the selected product. Pick the package(s) that match the request:
# Chat
npm view stream-chat-react-native version dist-tags --json
npm view stream-chat-expo version dist-tags --json
# Video
npm view @stream-io/video-react-native-sdk version dist-tags --json
# Feeds
npm view @stream-io/feeds-react-native-sdk version dist-tags --jsonInstall @latest when the npm dist-tag matches the selected docs. If it does not, use the tag or exact version recommended by the manifest-selected installation page.
---
Manifest search strategy
Do not maintain a feature-to-page table in this skill. The manifest is the live table of contents, and agents should search it at task time.
Build search terms from the current request and codebase:
1. Exact SDK symbols, packages, props, hooks, and component names already present in the prompt or code. 2. Exact user phrases for the requested feature or behavior. 3. Runtime words from the request or codebase when setup differs by runtime or architecture. 4. Broad domain words from the request only when exact terms do not hit.
Search order:
1. Search the primary manifest for exact symbols and exact phrases first. 2. If no exact result exists, search with split feature nouns from the user's request. 3. If several manifest entries match, fetch the two or three most relevant markdown pages and choose from their contents. 4. For cookbook or customization requests, find the current cookbook/customization entries in the primary manifest instead of assuming page names. 5. For low-level Chat API/client behavior, repeat the same search process in the secondary manifest only after deciding the UI SDK manifest is not the right source. 6. If neither manifest has a clear match, say the manifest has no exact match, fetch the closest overview or API page, and inspect installed package source only when code-level verification is still needed.
Do not convert these heuristics into a static mapping. If the docs add, rename, move, or split a page, the next agent should discover that from the manifest.
---
Source selection
Chat work. Use the Chat primary manifest for React Native UI SDK work: package installation, providers, components, hooks, theming, customization, native handlers, offline UI behavior, push UI setup, migration, and cookbook-style UI recipes. Use the Chat secondary manifest for Chat API/client work: tokens, auth, users, channels, messages, reactions, query syntax, permissions, events, typing, webhooks, import/export, rate limits, and API errors.
Video work. Use the Video manifest for everything: installation (RN CLI + Expo), client and call lifecycle, ringing/push, UI components and cookbook, advanced topics. Video does not have a separate API/client manifest; the same manifest covers both UI and lower-level call/state behavior.
Feeds work. Use the Feeds manifest for everything: installation, client creation (useCreateFeedsClient, StreamFeeds), feed groups, activities, reactions, comments, follows, notification feeds, For You feed, activity selectors, contexts and hooks (state layer), error handling, push, polls, moderation. Feeds does not have a separate API/client manifest; the same manifest covers both contexts/hooks and lower-level state behavior.
Combined apps (Chat + Video, Feeds + Chat, Feeds + Video, or all three). Consult each product's manifest. The canonical Chat-with-Video interop entrypoint is the Video manifest's https://getstream.io/video/docs/react-native/advanced/chat-with-video.md. Feeds has no built-in cross-product integration page; link out from your own UI.
Related skills
How it compares
Pick stream-react-native for RN and Expo SDK wiring; use stream-swift or stream-android sub-skills for native mobile platforms outside React Native.
FAQ
Which Stream products does stream-react-native cover?
stream-react-native bundles guidance for Stream Chat, Stream Video, and Stream Feeds in React Native CLI and Expo apps. Product selection happens during intent classification before install and provider wiring.
What packages differ between Expo and RN CLI?
stream-react-native uses stream-chat-expo on Expo and stream-chat-react-native on RN CLI for Chat. Video and Feeds share @stream-io/video-react-native-sdk and @stream-io/feeds-react-native-sdk across both lanes.