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.
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.