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.

TL;DR

Enforce tenant isolation at the data layer, before SQL is generated, using a signed security context your backend issues and the analytics layer verifies. Never derive scope from a client-supplied value. Key every cache and pre-aggregation by the same context. Constrain AI agents with the same policies as dashboards. Then test by trying to break it, not by confirming the happy path.

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.