Skip to content
Tech Stack14 August 2026 · 11 min read

Multi-Tenant Mobile SaaS in 2026: Flutter Patterns

Tenant identity has to survive a cold launch, a store review, and a push notification. What multi-tenant Flutter costs on mobile that no backend architecture post covers.

Multi-Tenant Mobile SaaS in 2026: Flutter Patterns

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 grid of identical wooden pigeonhole mail slots with one drawer pulled open in the centre, a blank paper tag hanging from it on a string and a strip of peeling tape residue beside the handle

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

A single blank brass cloakroom tag on a leather loop lying on a worn wooden counter next to a coffee ring stain, with a row of identical hanging tags fading into shadow behind it

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?

One plain white paper cup with a dented rim wearing a single halftone-printed sleeve, a fan of twelve differently printed sleeves spread beside it on a sunlit concrete windowsill

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.

ApproachTenant identity comes fromAdding a tenant costsFalls over when
Build-time flavors--flavor passed at compile timeA build, a store listing, a review cycleYou pass roughly ten tenants and the release train becomes the product
Runtime configTenant document fetched right after authOne database rowA customer demands their own icon on the home screen
HybridFlavor for app identity, remote config for brandDepends which tier they boughtSales 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:

  1. 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.
  2. Restore the session. Auth first, including tenant, using the synchronous-read rule from earlier in this post.
  3. 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.
  4. 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.
  5. 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?

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

How do you build a white label Flutter app?

Ship one Flutter binary that loads a tenant configuration document after authentication, and reach for build-time flavors only when a customer needs their own store listing. The runtime path stores a seed color, a logo URL, and a display name per tenant, feeds them into ColorScheme.fromSeed, and caches the result locally so the first frame after a cold launch is already branded. Flavors change the application ID, the icon, and the store presence, which is a different decision with a release cycle attached. Google's own Play guidance for white label developers also pushes you toward a separate developer account per client, so the flavor path costs you account administration on top of build configuration.

What is multi-tenant mobile SaaS?

Multi-tenant mobile SaaS is one installed app serving several customer organizations, isolating each tenant's data while sharing a single codebase and backend. The mobile part adds constraints the web version never has. A phone restores a persisted auth session, a cached push token, and a local database file before your first widget renders, and every one of those artifacts is tenant-scoped, which turns bootstrap ordering into a correctness problem rather than a performance one. The isolation model itself is the same set of choices you would make server-side: a tenant column on shared tables, a collection tree per tenant, or a database per tenant. Pick that, then spend your remaining effort on the client-side ordering.

How should tenant switching work in a Flutter app?

Treat a tenant switch as a partial sign-out: dispose the whole dependency graph and rebuild it from the root instead of mutating a tenant ID in place. Anything holding tenant-scoped state has to go, including repositories, open streams, the local database handle, and any image cache keyed by an ID that is only unique inside one tenant. Mutating the ID and letting widgets rebuild feels faster, and it leaks data across the boundary the first time a stream outlives the switch. Keep the list of available tenants on the account and the active tenant on the session, so switching organizations on a tablet does not silently switch them on the phone.

How do you handle deep links in a multi-tenant app?

Park the incoming link, restore the session including its tenant, then read the tenant out of the link itself and compare the two before navigating anywhere. Treat the link's tenant as untrusted input, because a user can open a URL for an organization they do not belong to, and the right response is an explicit access message rather than a 404 that leaves them guessing which account they are signed into. Clear the pending link after navigation or it replays on the next app resume. On Android 11 and lower, Google's App Links verification rules fail for the whole app if any single host in your manifest lacks a matching Digital Asset Links file.

Does Firebase Auth support multi-tenancy in Flutter?

Yes, through Google Cloud Identity Platform, with two caveats worth knowing before you scope the work. The Dart API exposes a tenant ID, but the Auth instance has to be told its tenant before it restores a persisted session, or a cold launch can fail to restore the user at all and log them out silently. The second caveat is federated sign-in: developers have reported that the Android OAuth provider builder calls the default Auth instance with no way to pass a tenant, which blocks per-tenant OIDC. There is also a quota to plan around. Without a billing instrument attached, Google's 2026 Identity Platform quota documentation caps a project at two tenants, which is easy to hit while seeding a development environment.