Expo/React Native Multi-Tenant Architecture: Shipping Mobile + Web with One Product Brain
Multi-tenant products are where “just ship the app” turns into an operating system.
You’re not only shipping features - you’re shipping:
- different branding per tenant
- different feature sets
- different compliance requirements
- different release cadences
- sometimes different backends or environments
And if you’re doing it with Expo/React Native (often with a web target too), you quickly hit an architectural question:
How do we keep one product brain while supporting many tenant realities?
This post shares the structure that has worked for me: typed configuration, feature flags, tenant theming, strict module boundaries, and EAS pipelines designed for multi-tenant release management.
Decide early: one binary per tenant vs one binary for all tenants
There are two broad models:
Model A: separate apps per tenant
Pros:
- clean branding and app store presence per tenant
- separate push credentials, bundle IDs, and release timelines
- clearer blast radius if something breaks
Cons:
- more operational overhead (builds, store listings, certificates)
- cross-tenant features require repeated work
Model B: one app that supports multiple tenants at runtime
Pros:
- one codebase, one binary, one deployment line
- easier to ship global improvements
Cons:
- configuration becomes a first-class problem
- a bad release can affect many tenants at once
- you must be disciplined about boundaries
Both can work. The rest of this post assumes Model B, but most patterns still apply to Model A.
“One product brain”: separate domain logic from platform UI
The trick is to stop thinking “shared UI” and start thinking “shared product logic.”
We split the system into:
- Domain layer: pure business logic, types, validation, state machines
- Application layer: use cases, orchestration, caching, data access
- Platform adapters: React Native UI, web UI, native modules, navigation
The domain layer should:
- avoid
react-nativeimports - avoid
window/document - be testable in Node
This makes it portable across:
- iOS
- Android
- web
- background tasks
If you only share UI components, tenants will diverge anyway. If you share the product brain, you can diverge presentation while staying behaviorally consistent.
Typed configuration: treat config as input, not globals
Multi-tenant apps fail when config is:
- scattered across
process.env - read directly in random components
- untyped and unvalidated
We standardized on a single config object:
- built at startup
- validated once
- passed through a provider
The key rule:
No feature should read environment variables directly.
That sounds strict, but it makes the app predictable.
Build-time config vs runtime config
- Build-time config (app identifiers, deep links, some secrets) belongs in
app.config.ts/ EAS profiles. - Runtime config (tenant theme, feature flags, endpoints) belongs in a remote config service or a tenant bootstrap endpoint.
Treat both as untrusted input: validate and fail gracefully.
Feature flags: a multi-tenant safety valve
Feature flags are not just product experimentation. In multi-tenant systems, they are:
- rollout control
- tenant-specific customization
- emergency kill switches
Design flags with:
- typed keys (avoid stringly-typed “flag soup”)
- defaults and expiry dates (or you’ll accumulate debt forever)
- per-tenant overrides
- segmentation support (tenant tier, region, app version)
Operationally:
- ship flag logic early in the app lifecycle
- cache flags with TTL
- include “last updated” so operators can reason about state
Tenant theming: tokens first, components second
The fastest path to multi-tenant UI chaos is ad-hoc theming:
- “Just change the primary color here”
- “This tenant needs a special button style”
- “This tenant wants a different spacing scale”
Instead, build a token system:
- colors
- typography
- radii
- spacing
- elevation/shadows (platform-specific)
Then generate platform outputs:
- web: CSS variables
- native: typed theme object
The reason tokens work well with multi-tenant apps is that they form a stable contract:
- tenants customize tokens
- the UI system consumes tokens
- components don’t fork per tenant unless absolutely necessary
Module boundaries: prevent tenant-specific hacks from spreading
The most important guardrail I’ve seen is a simple one:
- tenant-specific code lives behind an explicit boundary
Patterns that help:
tenants/<tenantId>/overrides.ts(explicit overrides)- “capabilities” instead of “if tenant === X” in random components
- shared “policy” functions that map config → behavior
Example mindset:
- not: “Tenant A gets button style X”
- but: “Tenants have a
brandtoken set; the button renders the token set”
When tenant-specific hacks leak into shared code, every future feature becomes conditional logic soup.
EAS pipelines: make environments and tenants boring
EAS is great, but multi-tenant release management needs structure:
- build profiles per environment (
dev,staging,prod) - channels for OTA updates (by env and sometimes by tenant tier)
- consistent versioning and rollback story
What worked well:
- staged rollouts by tenant group (canary tenants first)
- EAS update channels mapped to release trains (e.g.,
prod-canary,prod-stable) - compatibility checks (backend schema/version vs app version)
The main thing to avoid:
- “one big red button” that updates everyone instantly
Release management: protect tenants from each other
In a multi-tenant world, reliability is a product feature.
Guardrails that paid off:
- per-tenant kill switches for risky features
- canary rollouts by tenant tier
- dashboards segmented by tenant (crashes, performance, API error rates)
- “break glass” procedures for disabling a tenant-specific integration quickly
Testing: the tenant matrix is real
Multi-tenant bugs are often “only tenant X with flags Y on Android.”
We kept tests sane by:
- validating config schemas in unit tests
- running a small tenant matrix in CI (top 3 tenants × both platforms)
- snapshotting token outputs (theme diffs are easy to review)
You don’t need to test every combination. You do need to test enough to catch “tenant-specific drift.”
What I’d do differently next time
- Treat configuration as a product with versioning (migrations for config changes).
- Invest earlier in observability by tenant (otherwise you can’t roll out safely).
- Push tenant-specific behavior into explicit “capabilities” modules, not scattered conditionals.
The core takeaway
Multi-tenant apps are sustainable when:
- the domain logic is shared and clean (one product brain)
- configuration is typed, validated, and centralized
- theming is token-driven
- feature flags provide safe rollout and customization
- EAS pipelines make releases predictable and reversible
If you get those right, “mobile + web + many tenants” stops being a horror story and becomes a manageable operating model.