Articles

How to Secure Multi-Tenant Embedded Analytics

Last updated August 4, 2026

Embedded analytics has a failure mode that internal BI doesn't: the people looking at your dashboards don't work for you, and they don't work for each other. A bug that shows one internal team another team's numbers is embarrassing. The same bug across tenant boundaries is a breach notification.

The good news is that the design that prevents it is well understood. The bad news is that most of the ways it goes wrong look correct in a demo.

The threat model, briefly

Three things you're actually defending against:

  1. A tampered client. Someone opens devtools, changes a request, and asks for a different tenant's data. Any scope derived from the browser is a suggestion, not a control.
  2. A missed code path. The new endpoint, the CSV export, the scheduled email, the AI agent — each one is a place where a filter can be forgotten if filtering is something each surface does individually.
  3. Shared infrastructure. Caches, pre-aggregated rollups, and materialized tables are shared by design. If they aren't scoped, they're a cross-tenant channel that no amount of correct query filtering will close.

Notice that only the first is an attack. The other two are ordinary engineering mistakes, which is why architecture matters more than vigilance here.

Where enforcement belongs

The single most consequential decision is where the tenant filter is applied.

Where filtering happensWhat happens when it's wrong
In the front endNo protection at all — the client is attacker-controlled
After results returnSensitive rows were already read; one missed path exposes them
In each dashboard's queryEvery author is a potential vulnerability; new surfaces start unprotected
Before SQL is generated, in a shared layerOne rule, applied to every query from every surface

The last row is the only one that scales, because it's the only one where adding a new surface doesn't add a new place to get it wrong. This is the argument for putting access policies in the semantic layer rather than in the tools above it: the rule is defined once, and dashboards, APIs, exports, and agents all inherit it because they all go through the same model.

The request path

The shape that works, step by step:

  1. Your application authenticates the user. This is your existing auth — the analytics layer shouldn't be inventing its own identity system.
  2. Your backend issues a signed token, typically a JWT, carrying a security context: the tenant ID, the user's role, and whatever else scopes access. Signed by your backend, so its claims can't be authored by the client.
  3. The embedded client presents that token with its requests. It can carry the token; it can't change what the token says without invalidating the signature.
  4. The analytics layer verifies the signature and derives every filter from the context. Not from a query parameter, not from a header the client set — from the verified claims.
  5. SQL is generated with the tenant scope already applied, and runs on your warehouse.

The property worth stating explicitly: at no point does a client-supplied value decide what data is visible. That's the whole design.

Row-level and column-level policies

Row-level security answers which records. In a multi-tenant product the baseline rule is usually a single predicate — this tenant's rows only — but real systems layer more on top: a customer's own users may have roles, and an admin at the customer may see more than an end user there.

Column-level policies answer which fields, and they're easy to forget. A shared model often carries attributes that should never cross to the customer side: your cost basis, internal risk scores, identifiers belonging to other parties. Hiding a column in the UI is not removing it from an API response — if the field is in the payload, it's disclosed.

Cube supports access policies at both levels in the semantic layer, evaluated against the signed security context before SQL is emitted. Because the policy is attached to the model rather than to a dashboard, a new embedded surface doesn't start life unprotected.

The part that leaks: caching

Embedded analytics needs caching, because customer-facing workloads mean many users querying concurrently and expecting sub-second responses. Caching is also where careful systems leak.

A cache serves a stored result when a key matches. If the key describes the query but not the requester, two tenants asking the same question generate the same key — and whoever asks second is served whoever asked first. The same logic applies to pre-aggregations: a materialized rollup that isn't scoped per tenant is, structurally, shared data.

The rule is simple to state and easy to violate: anything that scopes the query must also scope the cache key. When you evaluate a platform, ask specifically how pre-aggregations behave under multi-tenancy — not whether caching exists, but what the cache is keyed by. Cube's pre-aggregations are built for multi-tenant deployments for this reason; the question is worth asking of anything you're comparing, because "we cache aggressively" and "we cache safely" are different claims.

AI agents inherit all of it

An agent answering questions in natural language composes queries nobody reviewed in advance. If your access rules live in dashboards, the agent routes around them — not maliciously, just structurally, because it isn't a dashboard.

This is the practical reason the semantic layer belongs underneath the AI rather than beside it. When the agent can only select from governed definitions, and those definitions carry access policies evaluated against the same signed security context, the agent is constrained by construction. Ask it for another tenant's revenue and the model has nothing to give it.

Cube's Analytics Chat API and MCP server operate on the same governed model and the same security context as every other surface, which is what keeps "answer any question" from meaning "read any row."

Pre-launch checklist

Test by trying to break it. The happy path always works.

  • Request with a modified token — signature verification rejects it
  • Request with an expired token — rejected, not silently accepted
  • Request another tenant's resource by ID — returns nothing, not an error that confirms existence
  • Run the same query as two tenants in sequence — the second gets its own numbers, not a cached copy of the first's
  • Inspect the raw API response for restricted columns — absent from the payload, not just hidden in the UI
  • Ask the AI agent in natural language for another tenant's data — and for aggregate figures that would reveal it
  • Confirm exports and scheduled deliveries carry the same scope as interactive queries
  • Re-run all of the above after adding a new embedded surface

Where Cube fits

Cube is the agentic analytics platform built on a semantic layer, and multi-tenancy is architectural rather than a deployment pattern. Access policies — row-level and column-level — live in the semantic layer and are evaluated against a signed security context at query time, so every surface reads the same rules: dashboards and workbooks, the Core Data APIs (SQL, REST, GraphQL), Creator Mode where your customers build their own views, and the Analytics Chat API for AI-driven answers.

That single-enforcement-point property is the reason the checklist above stays short as you add surfaces. The alternative — each surface implementing isolation correctly, forever — is the design that eventually produces the incident.

Methodology

The threat model and enforcement patterns here are general to multi-tenant analytics and hold whichever platform you choose; we've kept them separable from the Cube-specific sections so they're useful either way. Cube capabilities described are current as of August 2026. We build Cube, so treat the closing section as what it is — but the checklist is worth running against any vendor, including us.

Frequently asked questions

How do you secure multi-tenant embedded analytics?
Enforce isolation at the data layer, not in the UI. Your application authenticates the user and issues a signed token (typically a JWT) carrying a security context — tenant ID, role, and whatever else scopes access. The analytics layer derives every filter from that signed context before generating SQL, so a tampered client can't widen its own scope. Then make sure caching is keyed by the same context, and that any AI agent answering questions is constrained by the same policies.
Where should tenant filtering happen?
Before the SQL is emitted, in the layer that generates the query. Filtering in the front end is not security — the client can be modified. Filtering after results return means the sensitive rows were already read, and one missed code path exposes them. Filtering inside each dashboard's query means every dashboard author is a potential vulnerability. In Cube, access policies live in the semantic layer, so the rule is defined once and applies to every query regardless of which surface issued it.
Can I just pass the tenant ID from the browser?
No. Anything the browser sends can be changed by whoever controls the browser. The tenant ID must arrive in a token your backend signs and the analytics layer verifies — that's what makes it a security context rather than a suggestion. The client can carry the token; it must not be able to author its claims.
How does caching leak data across tenants?
A cache returns a stored result when it sees a matching key. If the key is built from the query shape but not from the tenant, two tenants asking the same question produce the same key — and the second one gets the first one's numbers. The same applies to pre-aggregated rollups: a rollup that isn't scoped or partitioned per tenant is shared data by construction. Cache keys and pre-aggregations must incorporate the tenant scope.
What about column-level restrictions, not just rows?
Row-level security decides which records a tenant sees; column-level policies decide which fields. Both matter in embedded analytics, where you often expose a shared model to many customers but some attributes — cost basis, internal scores, other parties' identifiers — should never leave your side. Cube supports access policies at both the row and column level in the semantic layer.
How do AI agents change the security model?
They make enforcement location decisive. A dashboard issues queries a developer wrote; an agent composes queries in response to whatever a user types. If access rules live in the dashboards, the agent bypasses them by definition. If the rules live in the semantic layer and the agent can only select from governed definitions under the same security context, natural language stops being an escape hatch. This is the practical reason to put the semantic layer underneath the AI rather than beside it.
Is one database per tenant more secure than row-level security?
It's a different set of tradeoffs, not automatically better. Separate databases give strong isolation and simple reasoning, at the cost of migration overhead, per-tenant operational burden, and expensive cross-tenant analytics. Shared-schema with enforced row-level security scales better operationally and keeps one model to maintain, provided enforcement genuinely happens before SQL runs. Many teams use a hybrid: shared by default, isolated for the customers who contractually require it.
What should we test before launch?
At minimum: attempt a request with a modified or expired token; request a resource belonging to another tenant by ID; run the same query as two tenants in sequence and confirm the second isn't served the first's cached result; verify restricted columns are absent from API responses rather than merely hidden in the UI; and ask an AI agent, in natural language, for data belonging to another tenant. That last one surprises teams most often.
Does a semantic layer slow down multi-tenant analytics?
It generates SQL that runs on your warehouse, so the compute is the engine you already use. The performance concern in embedded analytics is usually concurrency — many customers querying at once — which is what pre-aggregations address by materializing common rollups so repeated queries hit condensed tables. The thing to get right is that those rollups stay tenant-scoped.

Get started with Cube