Multi-tenant data isolation - critical security skill for SaaS applications. Use whenever each user or customer must see only their own data. Triggers on: row-level security, tenant isolation, "users see each other's data", "data leak", per-customer filtering, designer/owner access with data isolation. Covers three isolation patterns with tradeoffs. Use eagerly - wrong isolation is a security breach, not a bug. Pair with core for token generation and data-integration for connection-level overrides.
68
—
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Entry-point for multi-tenant embedding and data isolation. Security-critical - use when embedded end-users must only see their own tenant's data.
developer.luzmo.com is Luzmo's first-party, allowlisted documentation domain, maintained by the same publisher as this skill.https://developer.luzmo.com/.../*.md docs and their referenced URLs for implementation details.https://developer.luzmo.com/llms.txt and/or /llms-full.txt for discovery only.All multi-tenant filtering is configured SERVER-SIDE in the authorization token request — never in client-side code.
For full auth/embed-token guidance, see core.
Understanding enforcement and operational tradeoffs is essential:
Isolation Pattern Comparison
─────────────────────────────────────────────────────────────────────────
Pattern 1: Parameterized EmbedFilterGroup [SECURE, LESS VERBOSE]
├─ Filter definitions are configured on dataset associations
├─ Tokens pass tenant values through parameter_overrides
└─ One parameter can drive filters across one or more datasets
Pattern 3: Connection Overrides [STRONGEST]
├─ Physical/logical infra isolation
├─ Each tenant queries its own DB/schema
└─ Override account credentials per token
Pattern 2: Token-Level filters [SECURE, MORE VERBOSE]
├─ Filters are specified directly in the token
├─ Must include a filter for every dataset that needs isolation
└─ Must be extended whenever token access adds more datasets
Dashboard-Level Filters [INSECURE — DO NOT USE]
├─ Live in dashboard JSON
├─ Anyone with edit rights can remove
└─ DO NOT use for tenant isolationPattern 1 (Parameterized EmbedFilterGroup) — SECURE, LESS VERBOSE
parameter_overridesPattern 2 (Token-Level filters) — SECURE, MORE VERBOSE
Pattern 3 (Connection Overrides) — STRONGEST (by infrastructure isolation)
Do your tenants share the same database tables?
├─ YES: Will multiple datasets share the same tenant parameter?
│ ├─ YES: Prefer Pattern 1 (Parameterized EmbedFilterGroup)
│ └─ NO: Pattern 2 (Token filters) is fine when every accessible dataset is filtered
│
└─ NO: Each tenant has separate database/schema?
└─ YES: Use Pattern 3 (Connection Overrides)| Data model | Pattern |
|---|---|
| Row-level security for shared datasets with parameterized embed filters (multi-tenant data sources) | Pattern 1 — Parameterized embed filters |
| Row-level security via static token-level filters for specified datasets (multi-tenant data sources) | Pattern 2 — Static token filters |
| Each tenant has a separate database, schema, or connection (single-tenant data sources) | Pattern 3 — Connection overrides (account_overrides) |
This is the most maintainable pattern when one tenant parameter should apply to one or more dataset-level filters.
Why this is usually preferred:
parameter_overrides value can drive filters across multiple datasetsDocs:
https://developer.luzmo.com/guide/dashboard-embedding--handling-multi-tenant-data--parameter-filtering.md
https://developer.luzmo.com/api/createEmbedFilterGroup.md
https://developer.luzmo.com/api/associateEmbedFilterGroup.md
https://developer.luzmo.com/api/createAuthorization.mdSteps:
EmbedFilterGroup.associateEmbedFilterGroup) and put the parameterized filter definitions on that association.createAuthorization), pass parameter_overrides with the tenant-specific value.Setup example:
const group = await client.create('embedfiltergroup', {});
await client.associate(
'embedfiltergroup',
group.id,
{ role: 'Securables', id: datasetId },
{
filters: [
{
clause: 'where',
origin: 'global',
securable_id: datasetId,
column_id: tenantColumnId,
expression: '? in ?',
value: {
parameter: 'metadata.tenant_id',
type: 'array[hierarchy]',
value: [defaultTenantId],
},
},
],
}
);Token example:
const auth = await client.create('authorization', {
type: 'embed',
role: 'designer',
username: user.id,
access: {
datasets: [{ id: datasetId, rights: 'use' }],
},
parameter_overrides: {
tenant_id: [user.tenant_id],
},
});Critical limitation: EmbedFilterGroup is limited to one group per organization. Each dataset needs to be associated with the same group with one or more (parameterized) filters.
Key facts:
EmbedFilterGroup per Luzmo organization (not per dataset)parameter_overrides can also clear a parameter: { "clear": true } (useful to e.g. only apply a subset of the filters for specific users)filtersUse when you want the embed token itself to enforce query-level filters without a dataset-level filter group.
Important operational note: Token filters are equally secure when complete, but more verbose than Pattern 1. Specify a filter for every dataset in the token's access.datasets that needs tenant isolation, and extend the filter list whenever you grant access to more datasets.
Docs: https://developer.luzmo.com/api/createAuthorization.md
Minimal example:
const token = await client.create('authorization', {
type: 'embed',
username: 'user@example.com',
name: 'John Doe',
email: 'user@example.com',
access: {
datasets: [{ id: '<dataset-id>', rights: 'use' }],
},
role: 'viewer',
filters: [
{
clause: 'where',
origin: 'global',
securable_id: '<dataset-id>',
column_id: '<tenant-column-id>',
expression: '? = ?',
value: 'tenant-123',
}
]
})Consult the createAuthorization doc for the complete filter expression shape and available operators.
account_overrides) for Single-Tenant Data SourcesUse when each tenant has their own database, schema, table, or plugin configuration — the data is not shared at the row level.
Use cases:
Docs:
https://developer.luzmo.com/guide/dashboard-embedding--handling-multi-tenant-data--connection-overrides.md
https://developer.luzmo.com/api/createAuthorization.mdImportant: The property name is account_overrides (not connectionOverrides or connection_overrides).
Configuration differences:
Database connection example:
const auth = await client.create('authorization', {
type: 'embed',
username: user.id,
access: {
datasets: [{ id: datasetId, rights: 'use' }],
},
account_overrides: {
'<base-connection-uuid>': {
host: tenant.db_host,
database: tenant.db_name,
schema: tenant.db_schema,
},
},
});Plugin connection example:
account_overrides: {
'<plugin-connection-uuid>': {
properties: {
instance_url: tenant.sf_instance,
access_token: tenant.sf_token,
},
},
}Consult both the connection overrides guide and the specific plugin documentation before configuring overrides.
createAuthorization call.account_overrides is the documented property name (not connectionOverrides or similar).When to escalate to other skills:
corecore (saved dashboards) or data-visualization (Flex)analytics-studiodata-integration for connection setupthemingai-analyticsresource-managementtroubleshooting FIRST to confirm, then return here for the fixThis skill does NOT cover:
core)data-integration)Each pitfall below includes a frequency marker, the symptom you'll see, why it fails, and the secure alternative. [WARNING] Wrong patterns here cause real data breaches.
[ERROR] Using dashboard-level filters for tenant isolation ([WARNING] VERY COMMON — SECURITY CRITICAL):
// Wrong - editors can remove dashboard filters
dashboard.filters = [{ expression: "tenant_id = '123'" }]You'll see: users with designer/owner roles successfully removing or modifying the filter and viewing other tenants' rows. No error appears.
Why this fails: Dashboard-level filters live in the dashboard JSON. Anyone with edit permission on the dashboard can change them. This is a data-breach-in-waiting whenever editor roles are exposed.
[OK] Use server-side token/query enforcement instead:
// Correct - Pattern 1 centralizes the tenant filter definition
await client.create('embedfiltergroup', {
expression: 'tenant_id = ?',
parameters: [{ name: 'tenant_id' }]
})
// In authorization:
parameter_overrides: { tenant_id: '123' }[ERROR] Using wrong property name for connection overrides ([WARNING] COMMON — silent failure):
// Wrong - property names
connectionOverrides: {...}
connection_overrides: {...}You'll see: NO error. The token is issued, queries run, and they hit the BASE account's database — meaning every tenant sees the same (often the default) data. Detected only when a tenant complains about wrong data.
Why this fails: Luzmo ignores unknown properties on createAuthorization. The misspelling silently does nothing.
[OK] Use exact property name:
// Correct
account_overrides: {...}[ERROR] Implementing tenant filtering in client-side code:
// Wrong - client-side filtering is not secure
const userTenantId = getCurrentUser().tenantId
<luzmo-embed-dashboard filters={[{tenant_id: userTenantId}]} />[OK] Enforce tenant filtering server-side in token:
// Correct - server-side token generation
const token = await client.create('authorization', {
parameter_overrides: { tenant_id: user.tenantId }
})
// Send token.id and token.token to client[ERROR] Creating multiple EmbedFilterGroups:
// Wrong - only one EmbedFilterGroup per organization
await client.create('embedfiltergroup', { expression: 'tenant_id = {{tid}}' })
await client.create('embedfiltergroup', { expression: 'region = {{r}}' }) // Will fail[OK] Use a single EmbedFilterGroup with multiple parameters:
// Correct - one group, multiple parameters
await client.create('embedfiltergroup', {
expression: 'tenant_id = {{tenant_id}} AND region = {{region}}'
})
// Override both in token:
parameter_overrides: {
tenant_id: '123',
region: 'EU'
}[ERROR] Forgetting to extend Pattern 2 filters when dataset access expands:
// Wrong - token grants two datasets but filters only one
access: { datasets: [{ id: ordersDatasetId, rights: 'use' }, { id: invoicesDatasetId, rights: 'use' }] },
filters: [{
clause: 'where',
origin: 'global',
securable_id: ordersDatasetId,
column_id: ordersTenantColumnId,
expression: '? = ?',
value: 'tenant-123',
}][OK] Keep Pattern 2 filters complete:
// Correct - every tenant-scoped dataset in access has a matching token filter
filters: [
{
clause: 'where',
origin: 'global',
securable_id: ordersDatasetId,
column_id: ordersTenantColumnId,
expression: '? = ?',
value: 'tenant-123',
},
{
clause: 'where',
origin: 'global',
securable_id: invoicesDatasetId,
column_id: invoicesTenantColumnId,
expression: '? = ?',
value: 'tenant-123',
},
]EmbedFilterGroup or account_overrides payloads without fetching the exact API docs.account_overrides for connection overrides.EmbedFilterGroup resources (only one per organization is allowed).https://developer.luzmo.com/llms.txt, https://developer.luzmo.com/llms-full.txthttps://developer.luzmo.com/api/{action}{Resource}.mdhttps://developer.luzmo.com/guide/*.mdhttps://developer.luzmo.com/flex/charts/{type}.mdIf content exists on developer.luzmo.com, link — do not duplicate specs here.
5ee6b03
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.