---
title: "Keep each customer's data apart · Agent Wranglers"
url: https://agentwranglers.se/playbook/data/multi-tenancy/tenant-isolation
id: DAT-7
level: Required
applies: accounts, client-db
risk: data
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1266
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Keep each customer's data apart

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Multi-tenancy](https://agentwranglers.se/playbook/data/multi-tenancy.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: DAT-7
- Level: Required
- Chapter: [Multi-tenancy](https://agentwranglers.se/playbook/data/multi-tenancy.md)
- Rule: When one database holds data for many customer accounts, tag every row with its account and let the database refuse rows from other accounts.
- Check: Connect as the app's role with the setting for account A and select from every shared table. No row from account B comes back. With a browser client, do the same with the public key, signed in as a user of account A. The query of roles with `BYPASSRLS` lists only admin and server roles, never the app's or the browser's role.
- Risk: Data leaked or lost
- Applies when: The project has user accounts, or lets the browser query the database directly.
- How: Add a `NOT NULL` account column to every shared table, and include it in keys and indexes. In PostgreSQL, turn on row-level security with a policy that compares that column with a setting the app sets per request. Set it with `SET LOCAL` inside the transaction, so a pooled connection never carries it to the next request. Connect as a role that does not own the tables and has no `BYPASSRLS`, because owners and superusers skip the policies. `FORCE ROW LEVEL SECURITY` applies the policies to the owner too. When only your server queries the database, app checks are the first defence, as the [object access rule](https://agentwranglers.se/playbook/accounts/roles-and-permissions/object-access.md) says. When the browser queries it with a public key, as with Supabase's anon key, these policies are the only defence. [Turn on row-level security for every table](https://agentwranglers.se/playbook/data/multi-tenancy/row-level-security.md) shows the policies for that case. Supabase's `service_role` key connects as a role with `BYPASSRLS`, so it skips every policy. Use it only in server code that checks the account itself.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### SQL example

```sql
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY invoices_by_account ON invoices
  USING (account_id = current_setting('app.account_id')::bigint);
-- Per request, inside the transaction
SET LOCAL app.account_id = '42';
-- Check: roles that skip every policy. Expect admin and server roles only.
SELECT rolname FROM pg_roles WHERE rolbypassrls OR rolsuper;
```

## Sources

- [OWASP Multi-Tenant Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html)

## Related rules

- DAT-67 [Choose the isolation model before the first customer](https://agentwranglers.se/playbook/data/multi-tenancy/tenant-isolation-model.md) in Data and databases
- ACC-77 [Check ownership on every record](https://agentwranglers.se/playbook/accounts/roles-and-permissions/object-access.md) in Accounts and identity
- DAT-83 [Turn on row-level security for every table](https://agentwranglers.se/playbook/data/multi-tenancy/row-level-security.md) in Data and databases
- INF-33 [Force encryption on every managed service](https://agentwranglers.se/playbook/infrastructure/networking-and-dns/enforce-tls-on-services.md) in Infrastructure and environments
- ACC-37 [Give roles inside an organisation](https://agentwranglers.se/playbook/accounts/roles-and-permissions/org-scoped-roles.md) in Accounts and identity
- ACC-41 [Let organisations own the shared data](https://agentwranglers.se/playbook/accounts/organisations-and-teams/organisation-owns-data.md) in Accounts and identity
- DEN-31 [Filter shared dashboards by customer inside the warehouse](https://agentwranglers.se/playbook/data-engineering/access-and-bi-tools/dashboard-row-security.md) in Data engineering
- SRH-10 [Filter search results by access inside the query](https://agentwranglers.se/playbook/search/access-and-limits/search-access-filter.md) in Search

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/data/multi-tenancy/tenant-isolation">Agent Wranglers data and databases playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Choose the isolation model before the first customer](https://agentwranglers.se/playbook/data/multi-tenancy/tenant-isolation-model.md)
- Next: [Turn on row-level security for every table](https://agentwranglers.se/playbook/data/multi-tenancy/row-level-security.md)
- [Multi-tenancy](https://agentwranglers.se/playbook/data/multi-tenancy.md)
