How do I keep client data separate?

There are three levels: a filter in the application, a rule inside a shared database, or a database per client. They fail very differently.

An admin approves every new account by hand. Nothing is created until then. We reply by email; no newsletter, no sequence.

app.salescrew.io/inbox
The unified reply inbox with classified threads

The short answer

  • There are three levels of separation, and they are not variations on one idea: a filter written in application code, a row-level rule enforced inside one shared database, or a separate database per client.
  • Application-level filtering is the weakest because a single query that forgets its filter exposes another client's records, and nothing below the application is there to stop it.
  • Row-level security is genuinely stronger because the rule lives in the database and applies to every query, but it is still one database, so a policy written wrongly is still a cross-client exposure.
  • A database per client removes the failure mode rather than defending against it: there is no shared table for a query to reach across, the export is the whole database, and the storage region is chosen per client.

Why the three levels fail differently

Start with what "separate" is protecting you from. Not a hacker, usually. The realistic risk is an ordinary mistake: a query that forgot a filter, a permission set copied from the wrong template, a support tool that lists more than it should. What decides the damage is how far a mistake travels before something stops it.

With application-level filtering, every client's rows are in the same tables and the only thing keeping them apart is code that remembers to say which client is asking. It usually works. When it does not, one missing condition returns another client's records, and nothing underneath the application is there to catch it.

Row-level security moves the rule into the database, so it applies to every query whether the developer remembered or not. That is a real improvement and it is what a careful multi-tenant product does. The limit is structural: the rule and the data live in the same place. A policy written too loosely, or a credential with enough privilege to bypass policies, still reaches every client at once, because every client is there to reach.

A database per client changes the shape of the problem. There is no shared table, so there is no query that could cross between two clients even if someone wrote one. A mistake made in one client's project affects that client. The blast radius is one account, by construction rather than by policy.

The three levels, compared on what actually differs

Read the failure row first. It is the one that decides the other four.

Filter in the appRow-level rule in a shared databaseA database per client
Where the rule livesIn application code, re-applied on every query a developer writesIn the database, applied to every query automaticallyNowhere. There is no shared data to write a rule about
What one mistake exposesAnother client's records, with nothing below the app to stop itPotentially every client, if the policy itself is wrong or is bypassedOne client's own account, because that is all the project contains
What an export containsFiles pulled out of larger shared tablesFiles pulled out of larger shared tablesThe whole database, relationships intact
Data residencySet for the whole deployment, not per clientSet for the whole deployment, not per clientChosen per client when their project is created
The real costCheapest to run and fastest to open a new clientCheap to run, and a well-built version of the common approachOne set of infrastructure per client, so pricing is per instance rather than per seat

What to do about it as an agency

Decide first whether you need the third level at all. If your clients are small, do not hold regulated data and have never asked where their records live, the second level is a reasonable answer and it costs less. Be honest about which situation you are in rather than buying isolation as insurance you cannot explain.

If you do need it, verify it with the three questions rather than a marketing page. Do two of my clients' records ever sit in the same table. Does an export give me a database or a folder of files. Can one client's data live in a different region without moving everyone. A vendor who separates properly answers all three quickly, because the answers come from the architecture rather than from a policy document.

Disclosure: SalesCrew is our product and it is built on the third level. Each client instance is its own Supabase project, meaning its own Postgres database, its own file storage and its own edge functions, with the region chosen when the project is created. Access control inside an instance uses row-level security plus a three-axis scope model applied identically to the interface and to MCP tokens, and every tool call is written to an audit log with the actor named. The cost of this is real and worth stating: one set of infrastructure per client is why pricing is per instance, and a change you want on every client is a change applied to each of them.

Branding and isolation are unrelated features

A product can be fully rebranded per client, different logo, different domain, different colours, while every client's rows still sit in one shared database. Seeing a white-labelled login screen in a demo tells you nothing about where the data lives. They are separate decisions made by the vendor, and confirming one does not confirm the other.

Questions

Is row-level security enough to keep clients separate?
It is a real control and much stronger than filtering in application code, because the rule sits in the database and applies to every query. Its limit is that it is still a rule inside one database: it protects against a query that forgets a filter, not against a policy written wrongly or a credential with enough privilege to bypass it. Separate databases remove that class of mistake instead of guarding against it.
Does data residency work differently on a shared platform?
Usually it is a platform-wide or region-wide setting rather than a per-client one, because every client in that deployment shares the same database. If one client needs their data in a specific region, a shared platform can only answer by running a whole separate regional deployment. With a database per client, the region is chosen for that client when their project is created.
What should I ask a vendor to confirm how separated my clients are?
Ask three questions and write down the answers. Do two of my clients' records ever sit in the same database table. If a client asks for an export, do I get their database or a set of files pulled out of a larger one. And can I choose a different storage region for one client without changing anything for the others. The answers to those three tell you the architecture regardless of what the marketing page says.