Home Blog

Embedded analytics security: tenant isolation and RLS explained

icon-star-blue

Embedded analytics security: tenant isolation and RLS explained

Résumer cet article avec :

Embedded analytics security isn't just about encryption in transit. The harder problem is tenant isolation: ensuring that tenant A never sees tenant B's rows, even when both are running the same dashboard, on the same infrastructure, at the same time. Get this wrong and you have an IDOR waiting to happen.

This guide walks through the full architecture layer by layer: threat model, RLS implementation, token lifecycle, partitioning strategies, and deployment differences. It's written for engineering and security teams who need to evaluate or implement these controls, not just read about them at a high level.

TL;DR

Embedded analytics security is really a tenant isolation problem: tenant A must never read tenant B's rows on shared infrastructure. Three controls carry the weight: embed tokens issued server-side (signed JWT), row-level security enforced at query time (zero rows returned when tenant context is missing), and cache keys scoped per tenant.

  • Partitioning options: shared schema + RLS (default, cheap, correctness is load-bearing), separate schema, or separate database (strongest isolation, highest cost).
  • SaaS vs self-hosted: the isolation logic is identical; what changes is who owns keys, logs, and incident response.
  • Five test cases (invalid, swapped, expired, revoked, and missing tenant_id) are the minimum bar for any security review.

Security goals and the embedded analytics threat model

Tenant isolation, authorization, and confidentiality are three distinct concerns that often get conflated:

  • Tenant isolation means no cross-tenant row visibility. Tenant A's queries must never return data owned by tenant B, regardless of what parameters are passed.

  • Authorization means a given user within a tenant can only access the resources their role permits.

  • Confidentiality means data is encrypted at rest and in transit, and can't be exfiltrated through side channels.

For ISV and SaaS embeddings specifically, the realistic threat surface looks like this:

  • Tampered client-side filters (e.g., a user modifies the tenant_id query parameter in the browser)

  • Insecure direct object reference (IDOR) via embed parameters

  • Token replay after session invalidation or user offboarding

  • Session confusion when a user opens the embedded dashboard in multiple tabs with different identities

  • Over-broad caching: a cached query result from tenant A served to tenant B

  • Export or "share" pathways that bypass the row-level filter applied to the interactive UI

Against that threat surface, a well-architected embedded analytics platform should meet five explicit goals:

  1. Tenant context integrity: the tenant identity bound to a session must be established server-side and must not be overridable from the browser.

  2. Query-time enforcement: RLS predicates must run at the moment the database query executes, not just at the UI layer.

  3. Least-privilege identities: each embed session should carry only the attributes it needs to see.

  4. Auditability: every query should be attributable to a tenant and user identity in logs.

  5. Safe multi-tenant scale: the isolation model must hold as the number of tenants grows without custom engineering per tenant.

Security review checklist for engineering teams:

  • Where are embed tokens issued? (Must be server-side, never client-side)

  • At which layer is RLS enforced? (Must be query-time, not UI-only)

  • What is the cache scope? (Per-tenant, never cross-tenant)

  • What is the session lifecycle? (TTL, refresh mechanism, revocation path)

  • What happens when a token expires mid-session or is explicitly revoked?

Row-level security: how it actually works in embedded analytics

The canonical RLS pattern: a policy attached to a table or view filters rows based on attributes from the current authenticated session context. The predicate runs inside the query engine, which means it's invisible to the application layer and can't be bypassed by modifying a URL parameter or request body.

A basic Postgres example:

-- Enable RLS on the orders table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: users only see rows matching their tenant_id
CREATE POLICY tenant_isolation_policy ON orders
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

With this policy active, a query like SELECT * FROM orders automatically becomes SELECT * FROM orders WHERE tenant_id = '<resolved_tenant_id>'. The application doesn't add the filter; the database does.

The critical behavior to verify: if app.current_tenant_id is missing or invalid, the policy should default to zero rows returned, not all rows. Deny-by-default is the only safe posture.

In an embedded analytics context, every dashboard widget fires one or more queries. Each of those queries must run under a session context that has the tenant claims pre-bound. Toucan enforces this at the query level: as documented on Toucan's embedded analytics page, "RLS is enforced at every query" and each customer "only sees their own data."

Common failure modes to audit:

  • UI-only filters: the front-end applies a WHERE tenant_id = ? filter, but the underlying API endpoint accepts raw SQL or unvalidated parameters. An attacker can bypass the UI.

  • Missing tenant_id in joins: the main table has RLS, but a joined lookup table doesn't. Lateral data can leak through the join.

  • Shared query cache: a query result cached for tenant A gets served to tenant B because the cache key doesn't include the tenant claim.

  • Mis-scoped semantic layers: a metric definition in a semantic/BI layer resolves to a query that isn't tenant-scoped, even though the dashboard appears to show per-tenant data.

Token-based isolation: lifecycle and revocation

The embed token is the root of trust in a multi-tenant embedded analytics session. Here's how the lifecycle should work:

Issuance (always server-side). Your backend generates a signed JWT after your own authentication confirms the user's identity and tenant. The token payload includes the tenant identifier and any attribute-based claims (role, allowed dimensions, etc.). The browser never generates or modifies this token.

// Example JWT payload
{
  "sub": "user_789",
  "tenant_id": "acme-corp",
  "roles": ["viewer"],
  "allowed_datasets": ["sales", "support"],
  "iat": 1720000000,
  "exp": 1720003600
}

Validation (platform-side, at session init and each request). When the embedded component initializes, the analytics platform validates the JWT signature, checks expiration, and binds the tenant_id and claims to the session. Every subsequent query in that session inherits those claims. Toucan's documented model is explicit: "Toucan enforces tenant data isolation at query time via JWT" (Toucan Blog, published April 2026), and each tenant's data access is "scoped through a JWT token passed at embed time."

Why you should never trust tenant_id from the browser. If the platform accepts a tenant_id from a query parameter or request body without verifying a server-signed token, any user who inspects network traffic can substitute another tenant's ID. This is a textbook IDOR. The signed JWT ensures the tenant claim is cryptographically bound to an identity your server has already verified.

Revocation strategies to ask your vendor about:

  • Short TTL: tokens expire quickly (e.g., 1 hour), limiting the window for replay. Requires a refresh flow.

  • Denylist/revocation list: explicit revocation of a token ID before natural expiration. Useful for immediate user offboarding.

  • Signing key rotation: rotating the signing key invalidates all previously issued tokens. Useful after a key compromise.

  • Immediate invalidation semantics: can the vendor revoke a specific session instantly, or does it take until TTL expiry? Ask for the answer in writing.

Toucan's self-service documentation (published June 2026) confirms the same enforcement applies to AI-driven sessions: "Embed tokens define who the user is... Row-Level Security filters every query based on those attributes."

Tenant data partitioning: shared schema vs. separate database

There are three main partitioning models, and the right choice depends on your tenant count, regulatory obligations, and operational tolerance.

Model Isolation level Blast radius Operational cost
Shared schema + RLS Logical High (misconfiguration leaks all tenants) Low
Separate schema per tenant Schema-level Medium Medium
Separate database per tenant Physical Low High
Hybrid Mixed Variable High

Shared schema + RLS is the right default for platforms with hundreds or thousands of tenants. It's operationally cheap and scales well, but the correctness of the RLS policy is load-bearing. A misconfigured policy is a blast-radius event. This is why query-time enforcement (not UI-layer enforcement) matters so much.

Separate schema per tenant reduces blast radius: a misconfigured RLS on tenant A's schema doesn't affect tenant B's schema. Migration complexity grows linearly with tenant count.

Separate database per tenant gives the strongest isolation guarantee, and some highly regulated industries (healthcare, financial services) require it. The operational cost is significant: connection pooling, migration orchestration, and backup strategies all multiply by tenant count.

Hybrid reserves dedicated resources for high-value or regulated tenants while keeping standard tenants on shared infrastructure. Operationally the most complex to maintain.

Toucan's approach uses token-based isolation scoped per tenant on a shared model, which means tenant boundaries are enforced by the JWT claims and RLS predicates rather than by physical database separation. For teams that need the shared model to pass a security review, this is the architecture to audit: verify that the RLS policy is active at the database level, that the session context is set from the validated JWT (not from user input), and that the query cache keys include the tenant claim.

How partitioning affects audit logs. Regardless of model, every query execution should log: the tenant_id, the user identity, the query timestamp, the dataset accessed, and the number of rows returned. In a shared schema, the tenant_id in the log is your only proof of isolation after the fact. Make sure it's there.

SaaS vs. self-hosted: what actually changes in the security model

The tenant isolation logic (JWT validation + RLS enforcement) should be identical in both deployment modes. What changes is who owns the operational controls around it.

Concern SaaS Self-hosted
Token validation location Toucan's infrastructure Your infrastructure
Log storage and access Vendor-managed Your SIEM / log pipeline
Encryption key management Vendor-managed Your responsibility
Incident response Vendor SLA Your runbook
Data residency Vendor region(s) Your choice
Subprocessor exposure Vendor's subprocessors Reduced or eliminated

For self-hosted deployments, the security questionnaire should cover:

  • What network boundaries are required between the analytics service and the data warehouse?

  • What firewall rules are recommended (and which ports must be open)?

  • How are signing keys for JWT validation stored, and what is the rotation procedure?

  • What admin roles exist, what can they access, and how is that access logged?

  • How are audit logs exported to an external SIEM?

Toucan supports both SaaS and self-hosted deployment. In the self-hosted model, your team owns key management and log pipeline, which gives you stronger sovereignty guarantees but shifts operational responsibility. The isolation guarantees themselves (RLS at query time, JWT-scoped sessions, no shared sessions between tenants) don't change between modes.

End-to-end request flow: token issuance to query execution

Here's the full request flow from user login to data returned, with the security-relevant steps called out explicitly:

[1] User authenticates with your app (your auth system)
        |
        v
[2] Your backend issues a signed JWT
    Payload: { tenant_id, user_id, roles, exp }
    Signed with your secret; never exposed to the browser
        |
        v
[3] JWT passed to the Toucan embed component (web component / React SDK)
    via a server-rendered embed config, not a client-side variable
        |
        v
[4] Toucan validates the JWT (signature + expiry + required claims)
    Binds tenant_id and attributes to the session
        |
        v
[5] Dashboard widget fires a data query
    Toucan constructs the database query under the tenant-scoped session context
    RLS predicate filters rows: WHERE tenant_id = '<validated_tenant_id>'
        |
        v
[6] Query executes in the data warehouse (Snowflake, BigQuery, Postgres, etc.)
    Zero rows returned if tenant context is missing or invalid
        |
        v
[7] Results returned to the embedded component
    Cache key includes tenant_id (cross-tenant cache hits are impossible)

Pseudocode for token issuance in your backend:

import jwt, time

def issue_embed_token(user, tenant):
    payload = {
        "sub": user.id,
        "tenant_id": tenant.id,
        "roles": user.roles,
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,  # 1-hour TTL
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

Security review test cases. Run these assertions before shipping:

  • Invalid token: pass a token with a bad signature. Expected: zero rows returned, session rejected.

  • Swapped tenant_id: issue a valid token for tenant A, manually alter the tenant_id claim. Expected: signature validation fails, session rejected.

  • Expired token: pass a token with exp in the past. Expected: refresh required, no data served.

  • Revoked token: add the token's jti to the denylist. Expected: access denied even if TTL hasn't elapsed.

  • Missing tenant_id claim: issue a token without a tenant_id. Expected: RLS returns zero rows (deny-by-default).

These five tests are the minimum bar for a security review of any embedded analytics platform. If a vendor's documentation doesn't explain how each scenario is handled, that's the gap to press on during the evaluation.

Frequently asked questions

What is tenant isolation in embedded analytics?

Tenant isolation means no cross-tenant row visibility: tenant A's queries must never return data owned by tenant B, even when both run the same dashboard on the same infrastructure at the same time. It is distinct from authorization (what a user can do within a tenant) and confidentiality (encryption). Weak isolation creates IDOR-style data-leak risks.

How does row-level security (RLS) work in embedded analytics?

A policy attached to a table or view filters rows based on attributes from the current authenticated session, and the predicate runs inside the query engine. A query like SELECT * FROM orders is automatically filtered by tenant_id. Because enforcement happens at query time, it cannot be bypassed by modifying a URL parameter or request body. If tenant context is missing, it must return zero rows.

Why should embed tokens be issued server-side and never trusted from the browser?

If the platform accepts a tenant_id from a query parameter or request body without verifying a server-signed token, any user who inspects network traffic can substitute another tenant's ID, which is a textbook IDOR. A signed JWT issued server-side binds the tenant claim to an identity your server has already verified, so the browser can never override it.

What are the main tenant data partitioning models?

There are three main models. Shared schema plus RLS is logical isolation, cheapest to run, and scales to thousands of tenants, but RLS correctness is load-bearing. Separate schema per tenant reduces blast radius at higher migration cost. Separate database per tenant gives physical isolation, which some regulated industries require, at the highest operational cost. A hybrid mixes dedicated and shared resources.

Does the security model differ between SaaS and self-hosted embedded analytics?

The isolation logic (JWT validation and RLS enforcement at query time) is identical in both modes. What changes is who owns the operational controls: in SaaS, the vendor manages token validation, logs, keys, and incident response; in self-hosted, your team owns key management, the log pipeline, and data residency, which gives stronger sovereignty but shifts operational responsibility.

What security test cases should you run before shipping embedded analytics?

Run five assertions: an invalid token (bad signature) should return zero rows; a swapped tenant_id claim should fail signature validation; an expired token should require refresh with no data served; a revoked token should be denied even before TTL expiry; and a token missing the tenant_id claim should return zero rows by deny-by-default. These are the minimum bar for a security review.