The bug report was the useful kind: "I can log in, but the dashboard is blank." Not an error. Not a permission dialog. Sign-on worked exactly as designed — single sign-on, Entra ID, the person's real identity — and then nothing rendered. Several staff, same story.
The instinct is to go looking for a bug in the app. There wasn't one. The app was doing precisely what we built it to do: take the signed-in person's own token, hand it to Power BI, and ask for data as them. Power BI looked at who was asking, checked whether that person held a Power BI Pro licence and Build permission on the dataset, found they didn't, and returned nothing.
01 · THE SYMPTOMSigning in is not the same as being entitled
That blank dashboard is not a defect. It is the licensing model working, and it exposed a strategic question hiding underneath an architectural one. We had quietly assumed that anyone who could authenticate could see a dashboard — and the two are governed by completely different systems: ours, and Microsoft's. Authenticating proves who you are. Whether you are entitled to the data is a separate question, and Power BI answers it against the real human on the request.
Signing in is not the same as being entitled. The moment we saw that, the real question surfaced: who should be asking Power BI for the data?
02 · PATH AThe person asks
The relay forwards each viewer's own delegated token, verbatim. Power BI sees Alice, applies Alice's licence, Alice's dataset permissions, Alice's row-level security. Nothing is impersonated; the platform is simply looking at the real person.
The virtue is severe and easy to underrate: the relay holds no data credential. Compromise it and you have stolen nothing, because it owns nothing. The price is a slope. Every additional viewer is another Pro licence (~AUD $20/month) and another Build grant. Twenty viewers, ~$400 a month. Two hundred viewers, ~$4,000 a month — and an administrator adding people to security groups forever.
03 · PATH BThe application asks
The relay authenticates as one service principal — a non-human identity for EXO itself — and queries as itself. Viewers need no Power BI licence at all. Where per-person filtering matters, the relay tells Power BI "apply this person's row-level security" by passing their verified identity as an effective identity.
04 · THE ARGUMENTCost is where this starts — and the least interesting part
The shapes are obvious once drawn. A slope beats a step until it doesn't. Below roughly thirty viewers, Path A is cheaper and simpler. Above it, Path A becomes a tax on curiosity — every colleague who might benefit from looking at the data costs money to let look, which is exactly the wrong incentive for an organisation trying to become data-literate.
But the cost curve is not the argument. The argument is what you give up. Path A's relay is a remarkable thing: a component that sits in the middle of a data path and owns nothing. Path B ends that. You now hold one credential that can read every dataset the app can reach, and the blast radius of losing it is the whole semantic layer.
The part that gets underestimated
Under Path A, row-level security applies because Power BI is looking at a real person — there is nothing to implement and nothing to get wrong. Under Path B, the platform sees only the application, so we become responsible for telling it whose rows to return. That effective-identity stamp must be derived from the cryptographically verified sign-in token and nothing else. Derive it from a request header, a query parameter, or anything the client can influence, and you have built a system where any authenticated user can read any other user's rows by asking nicely.
That is not a reason to avoid Path B. It is a reason to be honest that Path B moves a security guarantee from Microsoft's side of the line to ours.
05 · TWO LAYERSThe distinction people keep collapsing
Almost every confused conversation about dashboard access comes from conflating two separate questions. Both paths need this distinction, and neither path changes the first layer — only the second.
Which dashboards may I open? OURS
Our decision, always. An AD group maps to an Entra app role, the role appears in the sign-in token, and config/models.json tags each dashboard with the roles that may see it. Add a colleague to the Finance AD group and they see the finance dashboards — no config change, no deploy.
identical under Path A and Path B
Which rows do I see inside one? RLS
The semantic model's business, via row-level security. Automatic under Path A, because Power BI is looking at a real person. Explicit under Path B, because we must stamp the effective identity ourselves.
the only layer the decision actually touches
06 · WHAT DOESN'T CHANGEAlmost none of the system moves either way
"We're changing how Power BI auth works" tends to make people brace for a rebuild. It isn't one. The choice is confined to a single line of code — which token the relay presents — which is exactly why it can be made reversibly. Everything else stands untouched.
- The React app and the Apache ECharts dashboards
- The DAX queries and the semantic-modelling workflow
- The config-driven catalog and the read-through cache
- No report embedding, no Microsoft Fabric, no networking work
07 · THE DECISIONStay on Path A. Move when the number says so.
For an audience in the tens, per-user Pro is cheaper, simpler, and the stronger security posture. The blank-dashboard problem that started all this is not an argument for re-architecting — it is an operational fix: grant the affected staff Pro + Build via a security group, and move the one dataset still sitting in a personal workspace into the shared one. The trigger to revisit is written down, so nobody has to relitigate it from intuition.
08 · HONEST UNCERTAINTYThree things to confirm before anyone buys anything
I would not let enthusiasm for the elegant answer paper over these — the indicative pricing above must be confirmed, and Microsoft moves the rules on service-principal executeQueries and on what a capacity is required for.
- The current Microsoft rules for service-principal
executeQueriesand the exact capacity requirement. These change; last year's blog post is not evidence. - Whether any model genuinely needs per-person RLS rather than per-group. Per-group is dramatically less to maintain and less dangerous to get wrong.
- The Embedded SKU size and pause schedule. "Pausable" only saves money if something actually pauses it.