Multi-Tenant Row-Level Security in Embedded Superset: Three Patterns and When Each Fails

Embedding a Superset dashboard in your product is a few hours of work. Making sure that dashboard shows tenant A only tenant A’s rows, for every chart, every filter, every drill-down and every cached result, is the part that decides whether embedded analytics is a feature or an incident.Superset supports three distinct approaches. Teams often pick one because it was the first that worked, without knowing what it does not protect against. This post describes each, shows the configuration, and is specific about where it breaks. It ends with a test plan, because tenant isolation is one of the few things in analytics that you should prove rather than assume.

The setting

A typical embedded setup: your application authenticates its users, then asks Superset for a short-lived guest token scoped to a dashboard. The browser loads the dashboard in an iframe through the Embedded SDK, presenting that token. Superset runs the dashboard’s queries as a guest user with a fixed role and returns the results.

The tenant boundary can be enforced in three places along that path:

  • In the guest token, as RLS clauses your backend attaches when it mints the token.
  • In Superset’s RLS rules, attached to the guest role and parameterised with Jinja from the token’s user attributes.
  • In the data layer, by giving each tenant its own schema or database so that no single query can span tenants.

They are not mutually exclusive. The strongest deployments combine 1 or 2 with 3.

Pattern 1: RLS clauses in the guest token

When your backend requests a guest token, it can include rls clauses. Superset appends each clause to the WHERE of every query the dashboard runs, optionally restricted to one dataset.

json
POST /api/v1/security/guest_token/
{
  "user": { "username": "tenant-acme-user-42", "first_name": "Acme", "last_name": "User" },
  "resources": [{ "type": "dashboard", "id": "c1a4e2d7-...-dashboard-uuid" }],
  "rls": [
    { "dataset": 17, "clause": "tenant_id = 'acme'" },
    { "dataset": 23, "clause": "account_id IN (SELECT id FROM accounts WHERE tenant = 'acme')" }
  ]
}

The clause is SQL. Superset wraps it in parentheses and ANDs it with whatever the chart’s own filters produce.

Why teams like it: all tenant logic lives in your backend, next to the code that already knows who the user is. No Superset configuration per tenant. Adding a tenant is nothing.

Where it fails:

  • You are building SQL from application state. If tenant_id comes from anywhere the user can influence, or is interpolated without care, this is a SQL injection surface inside your security boundary. Derive the tenant identifier from your own authenticated session, never from a request parameter, and use a strict allow-list format (an integer, a UUID) rather than free text.
  • Clauses without a `dataset` apply to every dataset on the dashboard. If one dataset has no tenant_id column, that chart errors. Worse, if a dataset has a column by that name with a different meaning, the filter silently does the wrong thing. Always scope clauses to dataset IDs, and treat a new dataset on the dashboard as a change that needs a matching clause.
  • Virtual datasets. For a dataset defined by a SQL query, the RLS clause is applied to the outer query, not inside the subquery. If the inner SQL pre-aggregates across tenants, the outer filter cannot un-aggregate it. Virtual datasets used in embedded dashboards must keep tenant identifiers at their output grain.
  • Chart-level SQL escape hatches. Custom SQL in ad-hoc metrics or filters still runs inside the same query and is still wrapped by the RLS clause, so this is safe. But SQL Lab and the chart data API used outside the dashboard are not covered by a dashboard-scoped token. Make sure the guest role cannot reach them.
  • Token lifetime. Guest tokens default to a five-minute expiry. A token minted for tenant A and leaked is valid for tenant A’s data for that long. Keep the expiry short and mint tokens on demand, not in advance.

Pattern 2: Superset RLS rules with Jinja

Superset has its own row-level security feature under Settings, Row Level Security. A rule attaches a SQL clause to one or more datasets for one or more roles. The clause can use Jinja, and in an embedded context the interesting variables are the guest user’s attributes:

sql
tenant_id = (
  SELECT tenant_id FROM app_users WHERE username = '{{ current_username() }}'
)

Assign the rule to the guest role (GUEST_ROLE_NAME in superset_config.py), and every query the guest user runs against that dataset is filtered. Your backend puts the tenant-bearing username into the token and does nothing else. Requires ENABLE_TEMPLATE_PROCESSING = True.

Why teams like it: the tenant logic is declared once, in Superset, alongside the datasets it protects. Auditors can read it. A new dashboard on an existing dataset is protected automatically.

Where it fails:

  • The guest role is one role. Every embedded user shares it, so RLS rules cannot distinguish tenants by role. All the discrimination has to come through Jinja and the username, which means the username format is now part of your security model. Document it and validate it when minting tokens.
  • Jinja is a template, not a parameter. current_username() is interpolated into SQL. Superset’s built-in functions are safe, but a rule that interpolates anything user-controlled, such as url_param, is not. Never use url_param or dashboard filter values in an RLS rule; they are user input.
  • Every dataset needs a rule. A dataset added to an embedded dashboard without a rule is fully exposed. There is no default-deny. Put a check in your release process, or a script that lists embedded dashboards’ datasets without RLS rules.
  • Performance. The subquery runs inside every chart query. Index app_users.username, or precompute a mapping table, or the “secure” version of the dashboard is the slow one.
  • The lookup table is inside the analytics database. Your tenant mapping now has to be replicated into the warehouse and kept current. A stale mapping is either an outage or a leak, depending on the direction of the staleness.

Picked a pattern? Now stress-test the failure mode

Each RLS pattern above fails differently at real tenant counts and under caching. A Superset Embedding Feasibility Review pressure-tests your chosen pattern against your actual tenant model before it ships.

Request an Embedding Feasibility Review

Pattern 3: Physical isolation per tenant

Give each tenant its own schema (or database) and point tenant-specific datasets at them. Superset then cannot write a cross-tenant query because no single table contains two tenants.

Two ways to make this work with one set of dashboards:

  • Schema-per-tenant with dataset templating. Superset datasets and SQL templates can use Jinja for the schema name, but the guest token cannot switch schemas on its own. In practice this means one dashboard copy per tenant, generated by script from a template dashboard, each pointing at tenant datasets. The export and import API makes this manageable up to a few hundred tenants.
  • Database-per-tenant with connection-level identity. A separate Superset database connection per tenant, with credentials that can only see that tenant’s data. Combine with the dashboard-per-tenant approach above.

Why teams like it: the boundary is enforced by the database’s own permissions, not by SQL rewriting. A bug in Superset, a missed rule or a bad clause cannot cross it. Compliance conversations are shorter.

Where it fails:

  • Operational load. Hundreds of schemas, database connections and dashboard copies are a real maintenance burden. Dashboard changes must be re-propagated. Metadata database size grows with tenant count.
  • Cross-tenant features are impossible by construction. Benchmarking a tenant against an anonymised peer group, a common premium feature, cannot be done inside the isolated model. You end up building a separate aggregated dataset anyway, with Pattern 1 or 2 protecting it.
  • Provisioning is code you must write and maintain. New tenant means new schema, new connection, new dashboard import, all automated, all tested.

Choosing

Situation Recommendation
Tens to thousands of tenants, shared tables, moderate sensitivity Pattern 1 (token RLS), scoped to dataset IDs, with tenant IDs taken only from the server-side session
Dashboards change often, several teams add datasets Pattern 2 (Superset RLS) for default coverage, plus a release check that every embedded dataset has a rule
Regulated data, contractual isolation requirements, tenant count in the tens Pattern 3 (schema or database per tenant), with Pattern 1 as a second layer
Premium cross-tenant benchmarking Pattern 3 for tenant data, Pattern 1 on a separate pre-aggregated, anonymised dataset

Whichever you choose, apply two rules universally. First, the guest role has the minimum permissions: read on the specific dashboards, nothing on SQL Lab, nothing on the chart or dataset list endpoints. Second, dashboard-level native filters and URL parameters are user experience, never security; a filter can be removed by the user, an RLS clause cannot.

Caching

Superset caches chart data. If two tenants’ queries produce the same cache key, tenant B receives tenant A’s cached rows. Superset includes the query object, which contains the RLS clauses and the guest user context, in the cache key, so the standard patterns above are safe by design. Two things can still go wrong:

  • A custom cache key function or a plugin that builds its own cache key and omits the RLS context.
  • A caching layer outside Superset (a CDN, a reverse proxy cache) in front of the chart data endpoint.

Include a caching test in the plan below rather than reasoning about it.

The test plan

Isolation is testable. Run this before launch and on every change to dashboards, datasets, RLS rules or the token-minting code.

  • Two tenants, known data. Seed tenant A and tenant B with distinctive values you can search for in responses.
  • Positive test. Mint a token for A, load the dashboard, capture every chart data response, assert every row belongs to A.
  • Cross-tenant attempt. Using A’s token, call the chart data endpoint directly with a modified form data payload that removes or alters the tenant filter. Assert the response still contains only A’s rows.
  • Dataset coverage. List every dataset referenced by every embedded dashboard. Assert each has an RLS clause in the token or an RLS rule in Superset.
  • Cache test. Load the dashboard as A, then immediately as B, with identical filters. Assert B’s responses contain none of A’s distinctive values.
  • Expiry. Use an expired token and assert a 401, not stale data.
  • Role scope. Using a guest token, call the SQL Lab, dataset list and chart list APIs. Assert 403 on all of them.

Automate it. A guest token, a couple of HTTP calls and a few assertions fit in any CI pipeline.

Frequently Asked Questions

What is the safest way to isolate tenants in embedded Superset?

Physical isolation, a separate database or schema per tenant, is the strongest guarantee because a mistake in a filter cannot cross a boundary that does not exist. It is also the most expensive to operate. Guest-token RLS clauses are the usual choice for SaaS at scale, and they are safe when the token is minted server-side from the authenticated session and never accepts a tenant id from the browser.

Can one Superset instance safely serve many customers?

Yes, and most embedded deployments do. The conditions are that the tenant identifier comes from your own session rather than from the client, that every dataset in the embedded dashboard carries the isolating clause, and that the test plan below is run on every release. A single dataset without the clause is all it takes for the guarantee to fail.

Does query caching leak data between tenants?

It will if the cache key does not include the tenant. Superset caches on the query and its parameters, so two tenants issuing the same logical query can share a cache entry unless the RLS clause is part of the key. This is the single most commonly missed step in a multi-tenant embed and it does not show up in functional testing.

Do we need a separate database per tenant?

Usually not. A shared database with a tenant column and enforced RLS is adequate for most SaaS products and far cheaper to run. Separate databases earn their cost when a contract, a regulator or a data-residency requirement demands it, or when tenant data volumes differ enough that they need separate tuning.

Build It With Andolasoft

Tenant isolation is one part of an embedded implementation, alongside theming, the token service, SDK integration and event wiring. Our embedded analytics implementation package delivers all of it as a fixed scope, and it starts with a feasibility review in which the tenancy model is the first thing we look at. If your dashboards are already embedded and you want the isolation checked, the Architecture Review covers RLS and role design. The broader controls are in our post on governance and security for Superset deployments.

If you are embedding Superset into a product and want the tenancy model checked before it ships, talk to us. We will review your token service, datasets and cache configuration against the failure modes above, and give you a written plan to close any gaps.