Brownfield Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Define brownfield explicitly: If native AppDelegate / Application owns the root UI, RN is a guest - plan embed boundaries before choosing Expo Router routes.
Score embed vs rewrite vs WebView per feature: Use the three-way matrix in Brownfield Basics - permanent WebView shells without an ADR sunset are a common failure mode.
Default to isolated expo-brownfield artifacts: AAR + Swift Package lets native CI skip Node when teams are split - integrated only when one squad edits both sides daily.
One Metro bundler per embedded module: Two RN runtimes in one process require multipleFrameworks: true on iOS and a deliberate symbol strategy - avoid accidental double Metro projects.
Initialize ReactNativeHostManager once at app launch: Late init causes blank RN screens and flaky Expo module lifecycle - mirror Expo docs in host AppDelegate / Application.
Match moduleName to JS registration: Host ReactNativeViewController(moduleName:) must align with registerRootComponent / app.json entry - mismatch shows white screen with no JS error.
Pin Expo SDK across expo and expo-brownfield: Use npx expo install on SDK 57 - Hermes and native binaries drift if versions diverge.
Run npx expo-doctor on every artifact build: Catches native module skew before publishing Maven/XCFramework consumers ingest bad artifacts.
Version Android artifacts in plugin config: android.version must match Maven coordinates - tag Git and Maven with the same semver.
Ship iOS debug and release Swift Packages separately: SPM binary targets are flavor-specific - QA and App Store pins must not cross wires.
Document a bridge contract version: BRIDGE_CONTRACT_VERSION in shared types - breaking SharedStateKeys or message type values require coordinated native + RN releases.
Native keychain owns refresh tokens: RN gets short-lived access tokens via useSharedState - refresh stays native unless auth strangler explicitly migrated.
Mount a root SessionBridge listener: SESSION_UPDATED / LOGOUT messages in _layout.tsx - per-screen listeners miss events on fast navigation.
Never pass refresh tokens in initialProps: Bootstrap props are logged and introspected - use shared state set by native after secure read.
Use Brownfield.popToNative() at flow completion: Checkout and wizard ends should return control to native chrome - do not leave orphan RN stacks under native tabs.
Enable setNativeBackEnabled when native owns back: Hardware back should exit RN module when stack is empty - prevents trapped users on Android.
Pick strangler slices post-auth and off cold start: First slice should not block app launch - home tab RN init is a performance and navigation ownership trap.
Record slice order in ADRs: Incremental Adoption ADR ranked decisions prevent "random settings page" migrations with no journey cohesion.
Separate release trains: RN artifact pipeline, host store pipeline, optional OTA - semver them independently per Brownfield CI/CD.
OTA only within runtimeVersion: eas update on the RN project must not outpace the Hermes runtime embedded in the host's pinned artifact.
Offline-test release artifacts: Airplane mode on host Release build before bumping Maven/SPM - debug Metro paths hide missing embedded bundles.
Use @expo/ui RNHostView for native chrome inside RN screens: Distinct from full brownfield presentation - native toolbars around RN bodies without custom view managers.
Keep RN routes thin; features fat: Same as greenfield - app/ re-exports feature screens per Mobile Architecture Basics.
Prefer config plugins over hand-edited host plists: expo-brownfield plugin regenerates targets on prebuild --clean - manual Gradle edits are lost.
Treat brownfield as alpha-capable, not alpha-careless: Expo documents brownfield support as alpha - budget native build debugging, read SDK release notes, and file upstream issues with reproduction artifacts.
No - use debug brownfield artifacts with Metro or internal host builds. Expo Go cannot load your host shell.
Legal, marketing, and rarely updated read-only content - not core product flows with offline or native navigation requirements.
One unless legal or team boundaries force two - each adds artifact CI, bridge contracts, and iOS linker complexity.
Expo Router owns navigation inside the embedded module only until ADR declares native root retired - see navigation ADR in architecture-design.
RN squad publishes new artifact; native squad integration PR bumps dependency - OTA alone is insufficient for new native modules.
expo-brownfield pluginStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026