ALTER TABLE package ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON package
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
Three layers between one agency and another.
Your travellers' passports sit in the same database as other agencies' — and the database itself refuses to hand them over. Filtering in application code is the first layer, not the only one.
- 01
The application filters
Every table that belongs to an agency is read through a manager that adds the agency to the query automatically. No screen and no endpoint writes that filter by hand, so none of them can forget it.
- 02
The database refuses
Every one of those tables carries a row-level security policy, and the agency for the current request is set inside the transaction. A query that forgot its filter returns nothing at all rather than someone else's rows.
- 03
The connection cannot opt out
The application connects as a role that is not permitted to bypass those policies. The one role that can is used only by platform operations, and every use of it is on the audit trail.
The setting is scoped to the transaction, not the connection — which is what lets one pooled connection serve every agency safely, and is the difference between a pool that is fast and a pool that leaks.
Three things that are not scoped to one agency.
Shared reference data
Countries, airlines, room sharing types, plans, themes and apps belong to nobody. They carry no agency and are the same list for everyone.
The hostname lookup
Finding which agency a domain belongs to has to happen before there is an agency to scope to. It is the one cross-agency read in a normal page request, and it reads exactly one column.
A database of your own
If you need physical separation, an agency can be moved to a dedicated database without a code change, because every access already goes through the same key.
Conventions, not promises.
| Convention | What it means for you |
|---|---|
| Unguessable ids | Records are identified by random ids, so nobody can walk your bookings by counting |
| Nothing is really deleted | Deletes are marked, not erased — a mistake is recoverable, and history stays intact |
| Every change is logged | Who, what changed, from where, and the request it belonged to — append-only |
| Secrets are encrypted | Gateway credentials, SMTP passwords, webhook secrets and two-factor secrets, with a key per agency |
| Money is exact | Stored as decimals with an explicit currency, never as floating point |
| Files sit under your own prefix | Logos, brand assets, traveller documents and exports, uploaded straight from the browser with a signed URL |
Staff see only their part
Roles are per agency and permissions are per module, so a counter agent can work leads without reaching your billing. The last owner can neither be removed nor demoted.
Support cannot quietly look
If we need to see your account to help you, the session is read-only and recorded — every impersonation ever started is listed, with who started it and why.
Your storefront sets no cookies
Public pages carry no session and no tracking of ours. It is what makes them cacheable, and it means a traveller browsing your packages is not being followed by us.