Say this first: One row filter driven by a governed mapping from principal to tenant, applied at the table, with pipeline identities explicitly scoped and the mapping itself audited.
Remember these
- One shared table with a row filter beats a table per tenant
- Resolve tenant from governed group membership, not hard-coded names
- Give pipeline identities an explicit branch and review it hardest
- Audit the mapping and the reads, because the mapping is the real control
Why: A table per tenant does not scale past a few dozen and makes every schema change a migration, so a shared table with a row filter is the right shape. The filter should not hard-code tenant names: it resolves the caller's tenant from a governed mapping — group membership per tenant, or a lookup the function consults — so onboarding a customer is a membership change rather than a code change. Pipeline and service identities get an explicit branch so the ETL reads the full grain, and that branch is the thing you review most carefully because it is the bypass. Sensitive columns get masks driven by tags rather than per-table ALTER, so the policy survives new tables. Finally, make the mapping auditable: who is in which tenant group, who changed it, and a periodic report from system.access.audit that no principal ever read rows outside its tenant.
CREATE FUNCTION sec.tenant_filter(tenant_id STRING) RETURN
is_account_group_member('platform_etl')
OR is_account_group_member(concat('tenant_', tenant_id));
ALTER TABLE app.events SET ROW FILTER sec.tenant_filter ON (tenant_id)
Senior depth: The architecture is only as good as the weakest write to the mapping, so I design around that: tenant groups provisioned from the identity provider rather than created by hand, change review on the policy function, and a very small set of principals who can ALTER the table's filter. I would also insist on a detective control alongside the preventive one — a scheduled check that joins audit reads against the tenant mapping and alerts on anything inconsistent — because in multi-tenant systems the cost of a single wrong group membership is a disclosure incident, not a slow dashboard.
Common mistake: Building one view or one table per tenant, then discovering every schema change is a hundred migrations.
Follow-up they will ask: What is the single change that would expose one tenant's rows to another?