A modular monolith ships one installable binary with feature modules enabled by flags and config. Multi-app repos build separate store listings - different bundle IDs, icons, and often independent release trains. Pick based on release independence, not team preference.
# Same repo - different installable apps via EAS profilesAPP_VARIANT=consumer eas build --profile production-consumer --platform iosAPP_VARIANT=business eas build --profile production-business --platform ios
When to reach for this:
Product asks for a "business app" alongside consumer - decide if it is a flag or a store listing.
White-label tenants need custom bundle IDs but share 90% of code.
Compliance requires isolating admin tooling from consumer-facing binaries.
Release managers want one vs two App Store Connect records.
// Fail closed when required module is disabledimport { getFeatureFlags } from "@/shared/config/featureFlags";export function B2BPortalScreen() { if (!getFeatureFlags().b2bPortal) { return null; // or redirect - route should also be unregistered } // ...}
Prefer excluding routes from the navigator over rendering empty screens.
Feature flags without module boundaries - Disabled B2B still links if features/b2b imports leak. Fix: Isolate features/b2b; lint-import consumer-only apps away from it.
One slug, two bundle IDs - Expo dashboard and OTA channels conflate. Fix: Separate EAS Update channels per APP_VARIANT.
Runtime flags for compliance - Admin code still in the JS bundle. Fix: Multi-app or build-time exclusion for regulated SKUs.
Premature multi-app split - Two apps, 99% shared code, double CI cost. Fix: Start monolith + variants; extract apps/ when native config diverges.
Hard-coded API URLs per app - Duplicated env logic. Fix: Central getAppConfig() keyed by appVariant.
Expo Go cannot test variants realistically - Bundle IDs and icons need dev builds. Fix: EAS development profile per variant.
God if (flags.x) in every screen - Unmaintainable branching. Fix: Register routes and navigators from a single useAppRoutes() / module manifest.
One installable app binary containing multiple feature modules. Modules are bounded in code (features/b2b) and enabled via config or flags - not separate microservices.
When is one binary with flags enough?
When products share navigation, native plugins, and release cadence - only some tabs or flows differ (B2B portal, beta features, tenant branding).
When do I need separate apps in the repo?
When bundle IDs, store accounts, native entitlements, or release schedules are genuinely independent - not merely different themes.
Do feature flags remove code from the bundle?
Runtime flags hide UI; they do not tree-shake native modules. Use build-time variants or separate apps to exclude regulated SDKs.
How does Expo Updates work with variants?
Assign one channel per variant (production-consumer, production-business). Publishing to the wrong channel updates the wrong listing.
Can modular monolith support white-label?
Yes - TENANT env in app.config.ts per EAS profile. Shared packages/features hold code; tenant assets live in profile-specific env or assets/tenants/<name>/.
APP_VARIANT vs EXPO_PUBLIC feature flags?
APP_VARIANT typically drives identity (name, bundle ID) at build time. EXPO_PUBLIC_* flags suit runtime experiments embedded in JS - both can coexist.
How do I hide Expo Router routes when a flag is off?
Conditionally register Tabs.Screen / Stack.Screen entries from a route manifest - do not rely on users never deep-linking to disabled paths.
Is multi-app the same as monorepo?
No. Monorepo is repo layout. Multi-app means multiple Expo apps (apps/a, apps/b). A monorepo can host one app (modular monolith) or many.
What about Android applicationId vs iOS bundleIdentifier?
Both branch from app.config.ts per variant. Keep them in sync with APP_VARIANT - mismatched IDs break deep links and push credentials.
Can I merge consumer and business apps later?
Easier than splitting. Migrate business-only modules behind flags, unify bundle ID strategy deliberately - store listings may require user reinstall.
How does this relate to EAS build profiles?
Each profile sets env (APP_VARIANT, TENANT, flags). Profiles are the delivery mechanism for both monolith variants and multi-app targets.
Should B2B and consumer share analytics?
Architecturally separate event namespaces per appVariant. Multi-app may use different analytics projects - plan before merging dashboards.
Modular monolith vs feature flags service?
Local flags gate compile-time modules and navigation. Remote flag services gate experiments - use both: module boundaries locally, remote for A/B.