All insights
Architecture

One Power BI model, many clients, no leaks between them

Duplicating the report per client does not scale and isolating by workspace gets expensive. Here is how real multi-tenant works, and where the traps are.

Architecture team· · 9 min read
One Power BI model, many clients, no leaks between them

When a data product starts selling to more than one client, the same question always appears: one report per client, or a single one that knows who is looking at it? The first option works with three clients and becomes unsustainable at fifteen.

Why duplicating does not scale

The cost of duplicating is not in creating the copy, it is in maintaining it. A fix to one measure has to be applied fifteen times, and it only takes one being forgotten for a client to see a different number from everyone else. The day that gets discovered, it gets discovered in front of the client.

Row-level security, done properly

The right tool is row-level security (RLS), but it is almost always implemented badly: a filter is written comparing the user's email against a column on the fact table. It works in the demo and degrades in production, because it forces the engine to evaluate the filter across millions of rows.

The approach that holds up is different:

  • A users table relating identity to organisation.
  • An organisations table acting as a dimension.
  • The filter applies to the dimension, not to the facts, and propagates through the relationship.

The performance difference is not subtle: filtering a dimension of a few hundred rows and letting the engine propagate is incomparably cheaper than evaluating every fact row.

The trap of measures that ignore context

The most dangerous failure is not performance, it is isolation. A measure using ALL() to compute a total or a percentage removes the security filter along with every other filter. The result is a percentage computed over every client's data, shown to just one.

The leak is invisible: the client does not see other people's rows, they see a denominator that includes them. It is the kind of error that survives for years without anyone noticing.

The correct way to clear filters without touching security is ALLSELECTED() or, better, REMOVEFILTERS() applied only to the columns that actually need releasing. Never to the whole model.

How to prove the isolation works

Testing RLS by eye is not enough. What we do:

  • A test dataset with two organisations whose totals are deliberately different and easy to recognise.
  • Walk through every measure in the model impersonating a user from each organisation, not just the ones visible on the report.
  • Compare each result against that organisation's expected value. Any measure returning the combined total has a leak.

It is tedious, and it is what separates a product you can sell to several companies from one you can only demo.

When NOT to go multi-tenant

If clients need different metrics — not the same numbers over different data, but genuinely different calculations — forcing a single model produces a monster full of conditionals. There, splitting is right. The question that decides it is not how many clients you have, but how similar their questions are.

Keep reading