A multi tenant flutter saas is one app binary serving several customer organizations at once, where tenant identity is part of every read and every write rather than a column you remember to filter on. The server-side version of that problem is well documented. The mobile version has failure modes the backend write-ups never reach, because on a phone the tenant has to survive a cold launch, an app store review, and a push notification sent to a device nobody has opened in a week.
I have been living in this for ten months. BookBed is a property management SaaS I built solo across iOS, Android, Web, macOS, Linux and Windows from one Flutter codebase on Firebase and Stripe, starting 16 October 2025. Every property owner on it is a tenant. Nine euros a month, up to twenty units each. Small tenants, lots of them, one binary.
What Actually Makes a Flutter App Multi-Tenant?

A Flutter app is multi-tenant when tenant identity is resolved once at session start and then carried implicitly by every layer below it. The identity is never an argument you thread through call sites. It sits in the auth boundary, and the repositories underneath read it as ambient context, the same way they read a locale.
The distinction that matters here is not the one people expect. Most teams reach for the storage question first, which is the multi-tenant SaaS question every backend article covers: shared schema with a tenant_id column, a collection tree per tenant, or a whole database per tenant. Answer it, obviously. But that answer is portable. It is the same answer whether your client is a browser or a phone, and reading about it will not prepare you for what mobile adds.
What mobile adds is state that outlives the process. A browser tab starts empty. A phone starts with a keychain entry, a persisted auth session, a cached FCM registration token, an SQLite file, and possibly a notification the user tapped to get here. Every one of those is tenant-scoped, and every one of them is restored before your first widget builds. Get the ordering wrong and the app comes up authenticated as the right human inside the wrong organization.
Tenant Selection Belongs to the Session, Not the Screen

Tenant selection is an authentication concern, not a navigation concern, and building it as a screen after login is the most expensive mistake in this space. Expensive is the wrong word, actually. It is cheap to build and painful to remove, which is worse. The screen is the visible part. The contract underneath it is that no repository can be constructed until a tenant is known, which means the choice has to happen inside your auth state machine rather than downstream of it.
If you go the managed route, Google Cloud Identity Platform gives you real tenant silos with their own users and identity providers. It also has a wall you will hit in development: Google's Identity Platform quota documentation caps you at 2 tenants per project without a billing instrument, and lifts that to unlimited once billing is attached. Fine in production. Confusing at 9pm on a Tuesday when your third seed tenant will not create and nothing in the error suggests billing.
The sharper edge is what happens on relaunch. Firebase's iOS SDK 11.0.0 shipped a bug, filed in September 2024 and forwarded from the FlutterFire repository, where a persisted multi-tenant session failed to restore and threw error 17072: the provided user's tenant ID does not match the Auth instance's tenant ID (firebase-ios-sdk issue 13565). The user was silently logged out on every cold start. That specific bug has a fix, but the class of bug does not go away, because the ordering it exposes is permanent. The Auth instance has to know its tenant before it restores anything, and your bootstrap has to persist the tenant somewhere it can read synchronously, before Firebase initializes.
Plan against the plugin surface rather than the native one. FlutterFire's multi-tenancy request (issue 1584) was filed in December 2019 and has since been closed, so tenantId is exposed on the Dart API today. The rough edges moved rather than disappeared, and they cluster around federated sign-in. One developer documented in September 2024 that OAuthProvider.newBuilder() internally calls FirebaseAuth.getInstance() with no way to specify a tenant, which blocks per-tenant OIDC on Android (FlutterFire discussion 13252); the thread still has no replies. If your customers bring their own identity providers, build a throwaway prototype of that exact call before you commit the sprint.
For a B2B SaaS product where one human legitimately belongs to several organizations, add one more rule: the tenant list is a property of the account, and the active tenant is a property of the session. Conflating those two gives you the bug where switching organizations on the tablet silently switches it on the phone three hours later.
White-Label Theming: One Binary or Twelve?

Ship one binary with runtime theming until a customer needs their own store listing, then add a build-time flavor for that one customer. The split is about whose name appears on the listing, not about which shade of blue the buttons are. Flutter flavors handle the second case, and the docs are clear that they are compile-time: appFlavor returns the string you passed to --flavor during the build, and null if you passed nothing (Flutter flavors setup). Compile-time means a tenant is a build artifact.
| Approach | Tenant identity comes from | Adding a tenant costs | Falls over when |
|---|---|---|---|
| Build-time flavors | --flavor passed at compile time | A build, a store listing, a review cycle | You pass roughly ten tenants and the release train becomes the product |
| Runtime config | Tenant document fetched right after auth | One database row | A customer demands their own icon on the home screen |
| Hybrid | Flavor for app identity, remote config for brand | Depends which tier they bought | Sales promises the top tier to a customer paying for the bottom one |
Most teams should start at runtime config and stay there far longer than they expect. A tenant-scoped theme document holding a seed color, a logo URL, and a display name feeds ColorScheme.fromSeed and covers most of what customers mean when they say white label. Cache it locally, because it has to be present before the first frame on a cold launch, and a network fetch there gives you a flash of default purple that every client will notice and email you about.
Flavors earn their cost when the customer needs to be discoverable in the store under their own brand. That decision reaches past engineering. Google's own white label best practices for Play recommend a separate developer account per client or client group rather than one central account, on the reasoning that policy issues with one app can negatively affect all the others, and require genuinely distinct descriptions, icons, graphics and screenshots per listing. A template listing cloned twelve times is how you learn about the repetitive content policy.
One recent change makes the runtime path more attractive. Flutter 3.47, released 12 August 2026, moved Material and Cupertino out of the SDK into standalone material_ui and cupertino_ui packages at 1.0, on a weekly release cadence, so you can take design-system fixes without bumping the whole SDK (What's new in Flutter 3.47). When twelve brands share one binary, decoupling the widget layer from the toolchain is worth more than it sounds. If you are assembling the rest of the toolchain around this, I keep a running list of Flutter developer tools worth installing.
Should You Route Push Notifications by FCM Topic?
No, because any client can subscribe to any FCM topic string it can guess, and tenant identifiers are exactly the kind of string people guess correctly. Firebase's own topic messaging documentation says topics are best suited for publicly available information like weather alerts, and recommends targeting registration tokens instead for latency-sensitive or user-specific delivery (FCM topic messaging). Same page: one app instance can hold at most 2000 topic subscriptions, and subscription changes are throttled at 3000 queries per second per project.
The pattern that holds up is a token registry you own. Store each FCM registration token in a document keyed by user and tenant, with the token's own lifecycle attached. Then fan out server-side, filtered by tenant, from code you can audit. Yes, it is more work than a topic string. It is also the difference between a leaked notification being a bug and being a breach disclosure.
Two details that bite later. Tokens rotate, and a rotated token that keeps its old tenant association will deliver one organization's alerts to a device that has since switched to another. Delete the token row on sign-out rather than waiting for an uninstall. And route the notification payload through the same tenant guard your API uses, because a payload containing a bookingId from tenant A opened by a session in tenant B is a permission check you probably have on the server and probably do not have on the deep link handler.
Deep Linking Into the Correct Tenant
A deep link arrives before your app knows who anybody is. That is the whole problem. The link fires, Flutter boots, the router wants a route, and your auth state machine is still reading the keychain. Resolve it in this order:
- Park the link. Capture the incoming URI into a pending-intent holder and render your splash. Do not let the router act on it yet.
- Restore the session. Auth first, including tenant, using the synchronous-read rule from earlier in this post.
- Extract the tenant from the link, not from the session. A link is a claim about which organization the content belongs to. Treat it as untrusted input.
- Compare, then branch. Link tenant matches session tenant, navigate. Different tenant the user belongs to, switch tenants and then navigate. Tenant the user does not belong to, show a plain "you do not have access to this" screen rather than a 404, because the user is usually signed into the wrong account and needs to be told which one.
- Replay the parked link exactly once. Clear the holder after navigation, or the link fires again on the next resume and drags the user out of whatever they moved on to.
Now the platform trap. On Android 11 and lower, Google's App Links verification documentation states the system makes your app the default handler only if it finds a matching Digital Asset Links file for all hosts declared in the manifest. All hosts. If you have given eight tenants vanity domains and one of them lets their certificate lapse or reorganizes their site, verification fails for the whole app, and every tenant's links start opening in the browser. Prefer one canonical domain with the tenant in the path, and treat per-tenant vanity domains as a premium feature with an operational cost you have priced.
The Switch Nobody Tests
Switching tenants inside a live session has to tear down more than you think. Repositories, streams, the SQLite handle, image caches keyed by ID, and any provider holding a snapshot from thirty seconds ago. The safest implementation is the boring one: dispose the entire dependency graph and rebuild it from the root, the same way a sign-out does. Slower. Correct.
Where to Start This Week
Open your app and answer one question before you write any code: at what exact moment does your process know which tenant it is in? If the answer is "after the first screen renders", every problem in this post is already in your codebase and the fix is a bootstrap rewrite, not a feature. Move tenant resolution into the auth state machine, make the router wait on it, and only then argue about theming.
Then go count your registration tokens. How many rows in your push table belong to a user who has not been in that tenant for months?
