Skip to content
← Back to Blog

Expo/React Native Multi-Tenant Architecture: Shipping Mobile + Web with One Product Brain

· 6 min read
react-nativeexpoarchitecturemulti-tenantplatform

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-native imports
  • 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 brand token 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.