Multi-Tenancy Needs Architecture, Not Just Configuration
Instapaid is a multi-tenant white-label payment platform. Each financial provider that uses it gets their own branded experience — their logo, their colour scheme, their supported payment and salary providers — running on shared infrastructure.
When I joined the project, "multi-tenancy" was implemented as a series of if-statements scattered across the codebase. if (tenant === 'provider-a') { ... }. It worked, until it didn't.
How It Breaks
The symptom that forced a rethink: adding a new salary provider integration was taking weeks, not days. The root cause was that the payment flow was a monolithic function with tenant-specific branches baked in. Adding Provider C meant reading through all the Provider A and Provider B logic, understanding what was shared and what wasn't, and carefully inserting new branches without breaking existing tenants.
Two production incidents later, we decided to fix the architecture rather than patch the code.
The Core Principle: Tenancy Is a First-Class Concept
The mistake was treating tenancy as a configuration concern — a flag that changed behaviour. The right model is treating each tenant as a first-class domain entity that owns its configuration, its integration adapters, and its notification preferences.
This means:
1. Tenant configuration is a schema, not a feature flag
We defined a TenantConfig schema that captured every tenant-specific behaviour in one place: branding tokens, enabled payment providers, salary provider IDs, notification settings, and feature toggles. Loading a tenant meant loading its config — nothing else in the codebase needed to know which tenant it was.
2. Payment providers are adapters, not branches
We defined a PaymentProvider interface with methods like initiatePayment, confirmPayment, reconcile, and getTransactionStatus. Each provider implemented this interface independently. The core payment flow never referenced a specific provider — it called the interface.
Adding a new provider became: implement the interface, register the adapter, add it to the relevant tenant config. No changes to existing flows.
3. Notification channels are pluggable
Different tenants used different push notification configurations — some used FCM directly, others had their own notification services. We applied the same adapter pattern, with a NotificationChannel interface. Configuring APNs and FCM per tenant was a config change, not a code change.
Dynamic Branding on Mobile
The React Native app needed to support full white-labelling — different logos, colours, typography, and even copy — per tenant, loaded at runtime.
We used a theme provider pattern where the tenant config included a full design token set. On app launch, the tenant config was fetched (or loaded from cache), and the entire component tree was wrapped in a theme context. Every component referenced design tokens rather than hardcoded values.
This meant a single React Native codebase served all tenants — on launch, the app fetched its tenant configuration from the backoffice at runtime, which drove both the API endpoints and the theming layer. No build-time variation required.
Memory Profiling Saved Us
Multi-tenant apps with dynamic theming and multiple payment SDK integrations are memory-hungry. We ran into significant re-render issues on lower-end Android devices — theme context changes were propagating too broadly, causing large portions of the component tree to re-render unnecessarily.
We ran memory profiling on the React Native app and found several issues:
- Theme context re-renders causing large object retention
- Image assets from all tenants being kept in memory simultaneously
Fixing these required explicit lifecycle management for SDK instances and lazy-loading of tenant-specific assets rather than pre-loading everything at startup.
The Payoff
After the refactor:
- New salary provider integrations dropped from ~3 weeks to ~3 days
- Tenant onboarding (new financial provider wanting the platform) went from months to weeks
- Production incidents from cross-tenant bleed dropped to zero in the following 6 months
- Localization for multiple markets was additive — new locale, new config entry, no code changes
Closing Thoughts
Multi-tenancy is an architectural commitment, not an implementation detail. If your tenants share a codebase, make sure their concerns are separated by design — not by convention. Convention erodes under deadline pressure. Architecture doesn't.
If you're building a multi-tenant platform and want to talk through the design, reach out.