Guidance for AI coding agents (Claude Code, Copilot, Cursor, Codex, Aider, etc.) working in this repository. Human readers are welcome, but this file is written for tools.
Single source of truth.
CLAUDE.mdcontains nothing but@AGENTS.md, which Claude Code expands into this file. Edit this file only β never fork guidance intoCLAUDE.md.
Agents should prioritize backwards compatibility, API stability, accessibility, performance discipline and high test coverage when changing code.
Stream Chat React Native SDK monorepo. The core UI SDK lives in package/ (published as stream-chat-react-native-core) and is built on top of the stream-chat JS client. Two thin wrappers ship it to the two supported toolchains:
stream-chat-react-native(package/native-package/) β React Native CLI / bare RNstream-chat-expo(package/expo-package/) β Expo
Targets iOS and Android.
- Languages: TypeScript + React Native
- Runtime: Node 24 (
.nvmrcisv24; rootengines.nodeis>=20.19.4; CI runs24.x) - Package manager: Yarn 4.15.0 (Berry). The binary lives at
.yarn/releases/yarn-4.15.0.cjsand is activated viayarnPathin.yarnrc.yml. Any globally installedyarn(even the Homebrew classic 1.x) acts only as a launcher β no Corepack required. - Workspaces: single root
yarn.lock; workspaces areconfigs/typescript-config,package,package/native-package,package/expo-package,examples/SampleApp,examples/ExpoMessaging. No Lerna. - Testing: Jest with the
@react-native/jest-preset+@testing-library/react-native. - Build:
react-native-builder-bobβ CommonJS (lib/commonjs), ESM (lib/module), types (lib/typescript) - Lint/format: ESLint 9 flat config + Prettier, strict (
--max-warnings 0) - CI: GitHub Actions β PR validation on build + lint + typecheck + tests
- Release: Conventional Commits + semantic-release, driven by
yarn workspaces foreach
.nvmrc Β· .yarnrc.yml Β· eslint.config.mjs Β· .prettierrc / .prettierignore Β· commitlint.config.js Β· configs/typescript-config/ (base.json, library.json β the shared presets package/tsconfig.json extends) Β· .editorconfig Β· .husky/
Per-package: package/tsconfig.json (library) Β· package/tsconfig.test.json (tests) Β· package/jest.config.js Β· package/babel.config.js Β· package/i18next.config.ts
Respect repo-specific rules. Do not suppress lint rules broadly; justify and scope every exception with an inline comment.
package/β core SDK (stream-chat-react-native-core)native-package/β bare RN wrapper (stream-chat-react-native)expo-package/β Expo wrapper (stream-chat-expo)shared-native/{ios,android}β native source shared by both wrappers; synced into them, never edited in place
examples/βSampleApp(full-featured),ExpoMessagingconfigs/typescript-config/β sharedtsconfigpresetsai-docs/β agent-facing deep dives (see References)perf/β on-device performance toolkit (see Accessibility, RTL & performance)release/β semantic-release scriptsbin/,dotgit/hooks/β release and git-hook scripts
components/β 27 component directories (ChannelList,MessageList,MessageInput,Thread,Poll,ImageGallery,ChannelDetails,MessageMenu, β¦)contexts/β 40 React Context providers. The primary way components receive state and callbacks. Key ones:chatContext,channelContext,messagesContext,themeContext,translationContexthooks/β shared custom hooks; component-specific hooks live in that component'shooks/state-store/β client-side stores onuseSyncExternalStorewith a selector pattern (audio player, video player, image gallery, message overlay, attachment picker, β¦)store/β offline SQLite persistence:OfflineDB.ts,SqliteClient.ts,schema.ts,mappers/,apis/theme/β theming system +topologicalResolution.ts+generated/tokensi18n/β the 13 translation JSON files (theStreami18nwrapper class lives inutils/i18n/)a11y/β accessibility primitives (a11yUtils.ts,hooks/)middlewares/β command UI middlewares (attachments.ts,emojiControl.ts)icons/β SVG icon componentsmock-builders/β test fixtures and fakes (also aliased asmock-buildersin Jest)native.tsβ native-capability interfaces +registerNativeHandlers()
Use the closest folder's patterns and conventions when editing.
nvm use # Node 24
yarn install # every workspace, single root lockfile
yarn test:unit # smoke-check the setupAll commands run from the repo root unless noted.
# Install
yarn install # every workspace (single root lockfile)
yarn install --immutable # CI-style; fails if yarn.lock would change
# Build
yarn build # SDK build (commonjs + esm + types) via builder-bob
# Test
yarn test:unit # all unit tests (sets TZ=UTC)
yarn test:coverage # with coverage β what CI runs
cd package && TZ=UTC npx jest path/to/file.test.tsx # single file
# Type checking
yarn typecheck # every workspace + example app, in parallel
cd package && yarn test:typecheck # SDK src + tests + mock-builders
# Lint / format
yarn lint # prettier --list-different + eslint --max-warnings 0 + validate-translations
yarn lint-fix # ALWAYS run this before committing
yarn eslint <path> # eslint a single path
# Translations
yarn workspace stream-chat-react-native-core build-translations # i18next-cli sync
# Shared native sync (after editing package/shared-native/)
yarn workspace stream-chat-react-native-core shared-native:sync
# Sample app
yarn workspace sampleapp start # Metro bundler
yarn workspace sampleapp ios
yarn workspace sampleapp androidType gates β know which one is strict. yarn typecheck fans out to every workspace; each SDK workspace runs tsc --noEmit -p tsconfig.test.json, which includes tests and mock-builders but relaxes noUnusedLocals / noUnusedParameters. package's typecheck and test:typecheck are currently the same command. The strictest gate is yarn build: bob type-checks with package/tsconfig.json, which keeps the unused-symbol rules on and excludes __tests__ / mock-builders. Always run cd package && yarn test:typecheck after code changes β yarn lint and yarn test:unit do not catch all type errors.
Adding dependencies. .yarnrc.yml sets npmMinimalAgeGate: 3d, so packages published within the last three days are refused unless listed under npmPreapprovedPackages (currently stream-chat, react-native-teleport). enableScripts: false disables install scripts globally; per-package opt-ins live in root dependenciesMeta (@swc/core, better-sqlite3, react-native-nitro-modules, unrs-resolver). nmHoistingLimits: workspaces keeps workspace deps unhoisted β expect duplicated copies under each workspace's node_modules.
<OverlayProvider> # gesture/overlay host, accessibility config
ββ <Chat client={client}> # root: SDK metadata, offline DB, subscriptions
ββ <ChannelList>
ββ <Channel> # state container: messages, threads, composer
ββ <MessageList>
ββ <MessageInput>
ββ <Thread>
<Chat> is the entry point. It sets SDK metadata on the stream-chat client (identifier, device info), disables the JS client's recoverStateOnReconnect (the SDK handles recovery itself), registers subscriptions for threads/polls/reminders (cleaned up on unmount), initializes OfflineDB when enableOfflineSupport is set, and wraps children in ChatProvider β TranslationProvider β ThemeProvider β ChannelsStateProvider.
Every context in package/src/contexts/ follows the same shape:
createContext()with a sentinel default (DEFAULT_BASE_CONTEXT_VALUE)- an
<XProvider>wrapper component - a
useXContext()hook that throws when used outside the provider (suppressed in tests viaisTestEnvironment())
Context values are assembled in dedicated useCreateXContext() hooks (e.g. useCreateChannelContext) that memoize with selective dependencies to avoid unnecessary re-renders.
ChannelProps does not accept component overrides (that was the v8 API β see ai-docs/ai-migration.md Β§3.1). Slots come from ComponentsContext, populated by <WithComponents overrides={{ β¦ }}>, which merges over the parent context so nesting works (closest wins) and deep-merges the nested icons map:
<WithComponents overrides={{ Message: MyMessage, SendButton: MySendButton, icons: { Mute: MyMute } }}>
<Channel channel={channel}>
<MessageList />
<MessageInput />
</Channel>
</WithComponents>package/src/contexts/componentsContext/defaultComponents.ts is the authority: it exports DEFAULT_COMPONENTS with ~173 slots plus a nested icons map of ~92 icons, and ComponentOverrides is derived from it β adding a default automatically makes it overridable. Read slots with useComponentsContext(), which merges user overrides over the defaults so every slot is guaranteed defined and callers destructure without fallbacks.
Two mechanics not to "fix":
- Both
WithComponentsanduseComponentsContextmemoize with[]β overrides are read once at mount and must be stable. Do not inline an override object that changes identity per render. defaultComponentsisrequired lazily insidegetDefaults()to break a circular import (defaultComponentsβ components βuseComponentsContext). Do not convert it to a static top-level import.
Channel's own props are behavioral escape hatches instead, typed as Pick<β¦ContextValue, β¦> over the contexts it provides (handlers like handleDelete / handleReaction, messageActions, supportedReactions, myMessageTheme, overrideOwnCapabilities, β¦).
When adding a customizable component: add it to DEFAULT_COMPONENTS, then read it via useComponentsContext().
state-store/ holds useSyncExternalStore-based stores consumed with useStateStore(store, selector) for fine-grained subscriptions outside the context system. Define selectors at module scope so they stay referentially stable β an inline selector re-subscribes on every render.
package/src/native.ts declares TypeScript interfaces for every platform-specific capability (image picking, compression, haptics, audio/video, clipboard, share). Implementations are injected at runtime via registerNativeHandlers(): stream-chat-expo supplies Expo implementations, stream-chat-react-native supplies bare-RN ones. Calling an unregistered handler throws with a message naming the package to import.
Platform branching uses runtime Platform.select() / Platform.OS checks. There is no moduleSuffixes in any tsconfig, and the only platform-suffixed source files are the generated theme tokens (theme/generated/*/StreamTokens.{ios,android,web}.ts), resolved by Metro's platform extensions. Do not introduce new .ios.ts / .android.ts splits.
Both wrappers are thin. They:
- call
registerNativeHandlers()with platform-specific implementations - export optional dependency wrappers (
Audio,Video,FlatList) fromsrc/optionalDependencies/ - re-export everything:
export * from 'stream-chat-react-native-core'
Native code shared by both wrappers lives in package/shared-native/{ios,android} and is copied into each wrapper by shared-native:sync. Edit shared-native/, never the synced copies.
- Memoization: components use
React.memo()with customareEqualcomparators (not HOCs). Comparators check cheap props before deep message comparison β keep that ordering when extending one. - Offline-first: SQLite-backed persistence with sync-status tracking and a pending-task queue. Writes must go through
OfflineDB, not raw SQL. - Selective memo dependencies:
useCreateXContexthooks intentionally omit unstable values. Adding a dependency there can cause a re-render storm; removing one can cause stale UI. Profile before changing. - Cancel stale async work: media and network operations must be cancelled on unmount (
AbortControllerfor fetch-like APIs, unsubscribe listeners). Check instance IDs / timestamps before applying async results to state to avoid races.
- Edit generated or synced files (see below) β regenerate them instead.
- Add
channelorchannel.stateto dependency arrays β usechannel.cid, which is stable. - Mutate
channel.state.messagesdirectly β go through thestream-chatclient's state API. - Inline a
useStateStoreselector β define it at module scope. - Use unguarded web-only APIs in shared code β it runs on Hermes, not a browser.
- Bypass lint or type errors with broad disables or force merges.
package/lib/and all build artifactspackage/src/theme/generated/{light,dark}/StreamTokens.{ios,android,web}.tsβ regenerate withpackage/sync-theme.shpackage/{native,expo}-package/{ios,android}/**/shared/β regenerate withshared-native:syncexamples/ExpoMessaging/{ios,android}(prebuild output);ios/buildandandroid/buildin the other sample appsnode_modules/everywhere
- Clear Metro cache on module-resolution weirdness:
yarn react-native start --reset-cache(RN CLI) oryarn expo start --dev-client -c(Expo) - Test on both iOS and Android for native-module or platform-specific UI changes
- If an example app fails to build or install:
watchman watch-del-all && rm -rf ~/Library/Developer/Xcode/DerivedData/*(cd ios && bundle exec pod install)(RN CLI sample apps)npx expo prebuild(after changingExpoMessaging'sapp.json)rm -rf ios && rm -rf android(after installing new native modules inExpoMessaging)
Policy: add or extend tests in the matching module's __tests__/ folder. Cover new public API, bug fixes (as regression tests), and performance-sensitive utilities. Reuse the repo's fakes and mock builders instead of hand-rolling new ones. Do not let global coverage drop.
Runner: Jest (package/jest.config.js) with the @react-native/jest-preset, testEnvironment: 'node', TZ=UTC forced by yarn test:unit, maxWorkers: 2 on CI. mock-builders(.*) is aliased to src/mock-builders. Test files live alongside source at src/**/__tests__/*.test.ts(x).
package/jest-setup.tsx calls registerNativeHandlers() with test doubles and jest.mock()s every peer native module (reanimated, worklets, gesture-handler, netinfo, @gorhom/bottom-sheet, @op-engineering/op-sqlite, @shopify/flash-list, safe-area-context, react-native-teleport, RefreshControl). Add new peer native modules there or tests will fail to resolve them.
To run one test file, prefer cd package && TZ=UTC npx jest path/to/file.test.tsx. The testRegex array in jest.config.js also accepts a temporary path β revert it before committing.
Mock builders (package/src/mock-builders/):
api/initiateClientWithChannelsβ creates a test client + channels in one call (fastest path)api/β response builders (getOrCreateChannel,queryChannels,queryMembers,sendMessage,sendReaction,threadReplies,error) plususeMockedApisgenerator/βgenerateMessage(),generateChannel(),generateUser(),generateMember(),generateReaction(),generateStaticMessage(seed)(deterministic via UUID v5)attachments.tsβgenerateImageAttachment(),generateFileAttachment(),generateAudioAttachment()event/,DB/β event dispatchers and offline-DB fakes
Tests use render() / renderHook() from @testing-library/react-native. Components and hooks must be wrapped in the required provider stack (e.g. Chat β Channel β feature provider). Mock methods on the channel/client β never replace the whole object.
yarn build runs package's build: rimraf lib β build-translations (i18next-cli sync) β bob build β copy-translations (copies src/i18n into lib/typescript/i18n).
react-native-builder-bob emits three targets from src:
| Target | Output | Entry point in package.json |
|---|---|---|
commonjs |
lib/commonjs |
main |
module |
lib/module |
module |
typescript |
lib/typescript |
types |
shared-native:sync is not wired into install or build β run it manually after editing package/shared-native/.
Three-tier token architecture: primitives (raw colors) β semantics (e.g. colors.error.primary) β components (per-component overrides). Token references use a $key string syntax (e.g. "$blue500") resolved by a topological sort in package/src/theme/topologicalResolution.ts, so declaration order does not matter.
Platform-specific tokens are generated: package/src/theme/generated/{light,dark}/StreamTokens.{ios,android,web}.ts. Regenerate via package/sync-theme.sh when design tokens change β never hand-edit them.
Custom themes are passed as the style prop to <Chat>. mergeThemes() deep-merges the custom style over the base theme (deep-cloned via JSON.parse(JSON.stringify())). Light/dark mode is auto-detected via useColorScheme().
- 13 locales in
package/src/i18n/*.json:ar,en,es,fr,he,hi,it,ja,ko,nl,pt-br,ru,tr Streami18n(package/src/utils/i18n/Streami18n.ts) wraps i18next with per-locale calendar formats (calendarFormats.ts); accesstviauseTranslationContext()- Extraction:
yarn workspace stream-chat-react-native-core build-translations(i18next-cli sync, configured inpackage/i18next.config.ts) - Validation:
validate-translationsruns insideyarn lintand in CI β zero tolerance for empty translation values - Adding a string: use
t()β runbuild-translationsβ fill in every locale file
The SQLite schema lives in package/src/store/schema.ts. Versioning uses PRAGMA user_version; a mismatch triggers a full DB reinit (there are no incremental migrations). The current version is SqliteClient.dbVersion (package/src/store/SqliteClient.ts) β bump it whenever the schema changes, or existing installs will read a stale schema.
OfflineDB owns channels, messages, reactions, members, drafts and reminders through mappers/. Offline support is opt-in via <Chat enableOfflineSupport>.
This repo ships three project skills β load the relevant one before touching these areas rather than improvising:
.claude/skills/accessibilityβ VoiceOver/TalkBack work: interactive components, gestures, modals, lists, media controls, focus behavior, live announcements.claude/skills/rtlβ anything with a horizontal or directional axis: styles, positioning, flex, swipe gestures, animated transforms, icons, text alignment.claude/skills/perf-benchmarkingβ on-device measurement: Hermes CPU profiles, render profiling, deterministic call counting, memory/jank capture. Drivesexamples/SampleAppon a connected Android device via theperf/toolkit (scenario-lib.sh,capture-hermes-profile.js,analyze-react-profile.js,analyze-cpuprofile.js,android-heap-dump.sh; seeperf/README.md).
Accessibility is opt-in β see ai-docs/accessibility.md for the full contract. New interactive UI should reuse the primitives in package/src/a11y/.
Performance guidelines: minimize re-renders (memoization, stable refs); reach for React.memo / useCallback / useMemo when profiling justifies it, not reflexively; clean up side effects; prefer lazy loading for optional heavy modules; monitor bundle size and justify increases over 2% per package (tracked by the sdk-size-metrics workflow).
- Semantic versioning; avoid breaking changes, prefer additive evolution
- Public surfaces get explicit TypeScript types/interfaces
- Consistent naming:
camelCasefor functions and properties,PascalCasefor components and types - Mark removals with
@deprecatedJSDoc plus replacement guidance - Provide migration docs for breaking changes
- Mark with
@deprecated+ rationale + alternative - Maintain for at least one minor release unless security-critical
- Add to migration documentation
- Remove only in the next major
- Public API: throw descriptive errors or return typed error results, consistent with existing patterns
- No console noise in production builds; gate internal debug logging behind an env flag
- Never leak credentials or user data in errors
Run yarn lint-fix before every commit. Follow the zero-warnings policy β fix new warnings, never introduce any. Scope eslint-disable narrowly with an inline rationale; no broad rule disabling.
Prettier: single quotes, trailing commas, 100-char width (120 for Markdown) β see .prettierrc.
.husky/commit-msgβcommitlint --edit(Conventional Commits enforced).husky/pre-commitβdotgit/hooks/pre-commit-format.sh && dotgit/hooks/pre-commit-reject-binaries.py- Root
postinstallrunshusky
Conventional Commits: feat:, fix:, docs:, refactor:, chore:, β¦
feat(MessageInput): add audio recording support
Implement MediaRecorder integration with MP3 encoding.
Closes #123
- Never commit directly to
developormainβ always create a feature branch - PRs target
develop;mainis production releases only - Never commit unless explicitly requested
Follow PULL_REQUEST_TEMPLATE.md. Keep PRs small and focused.
-
yarn lint-fixpassed -
yarn test:unitpassed -
cd package && yarn test:typecheckpassed -
yarn buildsucceeds - Tests added for changes
- No new warnings (zero tolerance)
- Screenshot or video (before/after) for UI changes
- Public API changes documented
- Breaking changes labeled clearly in the description
.github/workflows/check-pr.yml (Node 24): yarn install --immutable β yarn build β yarn lint β yarn typecheck β yarn test:coverage. Other workflows: changelog-preview, lint-pr-title, release, sample-distribution, sdk-size-metrics.
Failing or flaky tests: fix them, or quarantine with a justification comment and a follow-up.
Conventional Commits feed semantic-release; the pipeline uses yarn workspaces foreach directly (no Lerna). Release-participating workspaces (core SDK + SampleApp) are hardcoded in release/release.config.js. Version bump β changelog β tag β publish; deprecations are noted in CHANGELOG. Ensure docs are updated before publishing breaking changes. See RELEASE_PROCESS.md.
Avoid large dependencies without justification (size, maintenance). Prefer existing utilities. Keep upgrades separate from feature changes. Respect the npmMinimalAgeGate rule above.
New public feature: update at least one sample app. Breaking change: provide a migration snippet. Keep code snippets compilable. Use placeholder keys (YOUR_STREAM_KEY).
Never commit API keys or real user data. Example code must use obvious placeholders. Scripts must fail closed on missing env vars. Avoid introducing unmaintained dependencies. See SECURITY.md.
yarn buildsucceedsyarn lintclean, no new warningscd package && yarn test:typecheckcleanyarn test:unitgreen, coverage not reduced- No generated or synced files modified by hand
- Public API docs updated if the API changed
- Samples updated if a feature surfaced
- Both platforms checked for native or platform-specific UI changes
- Agent deep dives:
ai-docs/ai-migration.md(v8 β v9 migration reference β load this instead of the prose upgrade guide for agent-driven migrations),ai-docs/accessibility.md(opt-in a11y layer) - Repo skills:
.claude/skills/{accessibility,rtl,perf-benchmarking},perf/README.md - Contributing / process:
CONTRIBUTING.md,RELEASE_PROCESS.md,PULL_REQUEST_TEMPLATE.md,SECURITY.md - Component docs: https://getstream.io/chat/docs/sdk/reactnative/
- Stream Chat API: https://getstream.io/chat/docs/javascript/
- Stream agent skills (installed via
getstream init): https://getstream.io/agent-skills/docs/installation/
End of machine guidance. Edit this file to refine agent behavior over time; keep human-facing explanations in README.md and the docs site.