Every multi-tenant HR product will tell you your data is isolated from other customers’ data. They are all telling the truth about their intent. The interesting difference is which layer of the system enforces it, because that determines what happens on the day someone writes a query that forgets a filter.
Three places isolation can live
In practice there are three architectures, and they fail in very different ways.
A database per customer
The strongest separation and the most operational pain. A forgotten filter cannot reach another customer’s data because the data is not in the same place. The cost is everything else: migrations run N times, a schema change can leave customers on different versions, and operational tooling has to know how to reach every database. Many vendors who start here end up with a hybrid, which reintroduces the problem they were avoiding.
One database, filtering in application code
The most common approach and the weakest. All customers share tables, and every query is expected to include a tenant filter. It works perfectly as long as every query, in every code path, written by everyone who ever worked on the system, includes that filter.
That is a stronger assumption than it sounds. Consider the places a query gets written: request handlers, background jobs, admin tooling, data migrations, a reporting endpoint added in a hurry, an engineer debugging in a console. Each one needs the filter. The filter is invisible when it is missing — the query runs fine, it just returns more than it should. And nothing in the system objects.
The failure mode is not malice. It is a Tuesday afternoon, a new endpoint, a reasonable-looking join, and a code review where two people were thinking about the feature rather than the filter.
One database, isolation enforced by the database
Postgres has a feature for exactly this: Row Level Security. With it enabled on a table, you attach a policy describing which rows a given user may see, and the database applies that policy to every query against that table. Not the queries you remembered to protect. Every query.
The forgotten filter stops being a data leak and becomes an empty result. That is the whole point: the boundary holds for code nobody reviewed carefully, because the boundary is not in the code.
How OneclickHR is built
OneclickHR runs on one Postgres database with Row Level Security enabled on every single table, without exception, and every tenant-scoped table carries a non-null tenant id. "Without exception" is doing real work in that sentence — one table with RLS switched off during a migration and never switched back is exactly how this architecture fails in practice.
A signed-in user’s organization and role travel in their auth token, which the database can read. So when a request arrives, the database itself answers the question "may this person see this row?" rather than trusting the application to have asked it.
The same mechanism handles the boundary inside an organization. An employee can see their own attendance, their own payslips and their own documents — not a colleague’s. That is the same kind of policy, at a finer grain, enforced in the same place.
Enforcement you can test is enforcement you can trust
A security property that is not tested is an intention. Policies are code, and code drifts: a new table, a policy with a subtly wrong predicate, a role whose definition changed. The only way to know the boundary still holds is to try to cross it.
OneclickHR’s isolation tests run against a real database rather than mocks, because a mocked database will happily confirm whatever the test author believed. They cover:
- Cross-organization reads, in both directions — not just A trying to read B, but B trying to read A, because an asymmetric policy bug is easy to write and easy to miss.
- Cross-organization writes, in both directions. Being unable to read another workspace’s data is not the same as being unable to modify it.
- Direct access by id with no filter at all, which is the scenario the whole architecture exists for: the query that should have been scoped and was not.
- Employee-to-employee isolation within a single organization, so being a legitimate member of a workspace does not mean seeing everyone in it.
- Immediate effect of deactivation, verified rather than assumed.
- Workspace suspension, so a suspended organization is actually suspended at the data layer.
The token problem
There is a subtlety worth drawing out, because it is where a lot of otherwise careful systems leave a gap. If a user’s role and organization are carried in a signed token, and tokens are valid for an hour, what happens in the hour after you deactivate someone?
In many systems: nothing. Their existing token still asserts they are an active member with a role, the signature is still valid, and access continues until the token expires. For an offboarding — someone leaving on bad terms, an account that may be compromised — an hour is a long time.
OneclickHR re-checks access against the database on every request rather than trusting the token’s claims about status. Deactivation takes effect on the next request, which is the behavior anyone clicking "deactivate" reasonably expects.
Questions worth asking a vendor
If you are evaluating any multi-tenant system that will hold your employees’ pay, documents and personal data, these four questions separate architecture from marketing.
- Which layer enforces tenant isolation — the application, or the database? If the answer is the application, ask what happens when a query omits the filter.
- Is row-level enforcement on every table, or most of them? Ask how they know, and whether anything would fail if a new table shipped without a policy.
- Do isolation tests run against a real database, and do they include unfiltered access by id? Mocked isolation tests prove only that the mock agrees with the author.
- How long after deactivation can a user still read data? If the answer is the token lifetime, that is the real offboarding window.
A boundary enforced only by remembering to enforce it is a boundary that holds until the first busy afternoon.
None of this is exotic. Row Level Security has been in Postgres for a decade. The difference between a system that uses it on every table and one that filters in application code is not sophistication — it is a decision made early, when it was still cheap, about where the boundary should live.

