DEV Community

Cover image for Multi-Tenancy in a Super App Isn't a tenant_id Column — Here's What It Actually Takes
FinClip Super-App
FinClip Super-App

Posted on

Multi-Tenancy in a Super App Isn't a tenant_id Column — Here's What It Actually Takes

The difference between "we filter by tenant" and "tenants are architecturally incapable of seeing each other" is the difference between passing an audit and failing one.

If you're building a super app that will host mini-apps from more than one business unit — let alone outside partners — you have a multi-tenancy problem whether you've named it or not. And the most common way teams "solve" it is the way that fails hardest in production: a shared database, a tenant_id column, and a sincere promise that every query remembers to filter on it.

Let's talk about why that breaks, and what real isolation looks like in a mini-app platform.

The seductive wrong answer

Here's the model almost everyone reaches for first:

-- One shared table, separation by convention
SELECT * FROM miniapp_data
WHERE tenant_id = :current_tenant   -- the entire isolation guarantee
  AND key = :key;
Enter fullscreen mode Exit fullscreen mode

This works in the demo. It works for months. Then one of these happens:

// The query that forgot the filter — now tenant A reads everyone's data
const rows = await db.query(
  "SELECT * FROM miniapp_data WHERE key = ?", [key]
); // 🔥 no tenant_id — silent cross-tenant leak
Enter fullscreen mode Exit fullscreen mode

There's no error. Nothing crashes. The data just quietly crosses a boundary that a regulator assumed was airtight. In a consumer app that's a bug. In a bank's super app hosting a partner's mini-app, that's a reportable incident.

The root problem: isolation-by-convention puts the guarantee in every developer's hands, forever. One forgotten WHERE clause, in code written two years from now by someone who's never heard of this article, breaks it.

What real isolation moves

Strong multi-tenancy moves the guarantee out of application code and into the platform — so that a mini-app physically operates inside a tenant context it cannot escape, regardless of what its queries say. Three things have to be true.

1. Tenant context is injected, not requested. A mini-app never names its own tenant. The platform binds the tenant to the execution context at launch, and every downstream call inherits it:

// Mini-app code never sees or sets tenant_id — it can't lie about it
const data = await fc.storage.get("user:profile");
// The platform resolves this within the tenant bound to THIS runtime
// instance. There is no API surface for the mini-app to query another.
Enter fullscreen mode Exit fullscreen mode

2. Storage is partitioned, not shared-with-a-filter. Instead of one table everyone shares, each tenant's data lives in its own logical store. The tenant boundary becomes a property of where the data is, not of whether you remembered the filter:

# Platform-level tenant config — not visible or editable by mini-apps
tenants:
  - id: business-unit-a
    storage: { namespace: "bu-a", isolation: "schema" }
    identity: { provider: "iam-internal", realm: "bu-a" }
  - id: partner-acme
    storage: { namespace: "acme", isolation: "database" }   # hard split
    identity: { provider: "oidc-external", realm: "acme" }
    network:  { default: "deny", allow: ["api.acme-partner.com"] }
Enter fullscreen mode Exit fullscreen mode

Note isolation: "database" for the external partner — a partner's mini-app data doesn't share a schema with your business units at all. The level of isolation scales with how much you trust the tenant.

3. Identity and session state are per-tenant. A session in tenant A carries no authority in tenant B. They resolve against different identity realms, so a token minted in one is meaningless in the other — there's no shared session pool to leak across.

The part the cloud-only platforms can't give you

Here's where it stops being a pure code problem. Even perfect logical isolation doesn't answer the regulator's actual first question: where does the data physically live, and under whose control?

For a regulated enterprise, "in the vendor's multi-tenant SaaS" is frequently a hard no — no matter how clean the isolation. Which is why the deployment model matters as much as the isolation model. The platform has to be able to run entirely inside the enterprise's own boundary:

deployment:
  mode: on-premise          # or: private-cloud
  data_residency: "eu-self-hosted"
  control_plane: "self-managed"   # vendor has no operational access
  egress: "deny-by-default"
Enter fullscreen mode Exit fullscreen mode

When the control plane is self-managed and egress is deny-by-default, you can make a statement no SaaS contract lets you make: the vendor cannot reach the data, because the vendor isn't in the path. That's the difference between trusting a boundary and owning one.

Putting it together

Stack these — injected tenant context, partitioned storage, per-tenant identity, and a deployment model that lives inside your perimeter — and "multi-tenant" stops meaning "everyone shares a table" and starts meaning "tenants are architecturally incapable of reaching each other, inside infrastructure you control." Here's the shape of it:

Multi-tenant isolation inside a self-hosted enterprise boundary

This is the case for not hand-rolling this layer. The isolation model and the private-deployment model are both deep infrastructure problems, and getting either subtly wrong is the kind of mistake you discover during an audit rather than a code review. A mini-app platform built for regulated industries — FinClip is one example — ships this as a foundation: per-tenant data boundaries, configurable isolation levels, and deployment in public cloud, private cloud, or fully on-premise, so data sovereignty is something you configure, not something you invent.

The test

Before you onboard a second tenant — especially an external one — check your own stack against these:

  1. Can a mini-app issue a query (or forget a filter) that returns another tenant's data? (It must be structurally impossible, not just discouraged.)
  2. Does a session/token from tenant A carry any authority in tenant B? (It shouldn't resolve at all.)
  3. Can you run the entire platform inside your own boundary, with the vendor having zero operational access to the data? (A regulator will ask.)
  4. Can you set a stronger isolation level for an untrusted partner than for an internal unit? (Trust isn't binary.)

A "no" on #1 or #3 isn't a backlog item. It's an audit finding waiting to happen.

Which of these is hardest to answer "yes" to in your current architecture? That's usually the one that tells you whether you built a tenant-isolated platform or a shared app with a filter. 👇


More on super app architecture, isolation, and deployment models → https://super-apps.ai/

Top comments (0)