SecurityTenant isolation

Isolation the database enforces, not the code

Every multi-tenant TMS promises that the carrier down the road cannot see your containers. In most of them that promise is a filter in application code, and it holds until the afternoon somebody ships a query without it. In DrayBase the boundary lives inside PostgreSQL, one layer below anything a developer can forget.

Where this page standsDrayBase has not launched, so no carrier's operational data is being processed yet. What follows is not a security programme — it is a description of the mechanism that is already written, and a list of the controls that are not. Nothing here is aspirational: if it is on this page in the present tense, it is in the repository.

01The mechanism

One tenant per transaction

Your company id is not a parameter the browser sends. It is written on the session record on our side, applied to the database transaction that serves the request, and gone the moment that transaction commits — so a connection handed back to the pool cannot carry the last carrier's scope into the next request.

tenant_isolationRow-level security
In the repo
Transaction roledraybase_app · no superuser, no bypassrls
Scope keyapp.current_company_id · set per transaction
Covered tablesEvery table holding a company_id, plus the company row itself
Enforcementforce row level security · owner included
No scope set0 rows · fails closed, never open
ReappliedOn every migration · any new company_id table is covered

Summary of the isolation contract exactly as it is written in the repository. Not a screenshot, and not a diagram of something we intend to build.

A boundary you have to remember to apply is not a boundary.

Three decisions make this hold up, and each one is there because the obvious version of it does not work.

The role, not the queryThe application drops to a dedicated database role before it reads a single row. That role has no superuser bit and no BYPASSRLS, which is what makes the policies apply to it at all.
One door for carrier dataEvery query that reads a load, a container or a driver goes through the helper that has already fixed the tenant on the transaction. The unscoped handle is still reachable from an authenticated request, and exactly two lookups use it — your own user record and the list of carriers you belong to, both outside the tenant boundary by design. Taking it off the request context altogether is the next thing on this list.Planned
Membership, every timeSwitching between carriers checks membership on our side before the session changes, and the role behind each request is re-read from the membership rather than trusted from the cookie — so revoking someone takes effect on their next click, not when their session expires.

02Proof

Written right, and still leaking

The first time this ran, the policies were correct and one tenant could still read the other's customers. The reason is a trap worth knowing about if you are evaluating anyone else's multi-tenant claims: a PostgreSQL superuser skips row-level security entirely, and the default database user handed out by most hosting panels is exactly that.

Row-level security under a superuser is decoration.

The fix is the dedicated role above. The lesson is that a security control nobody tested is a security control nobody has, which is why the check below exists.

A test that fails

The development seed builds two carriers, writes a record into the second and queries it as the first. If a single row comes back, the run aborts with a tenant leak error. It is not decorative and it does not get relaxed.

Re-applied, not remembered

The migration runner re-applies every policy each time the schema moves, because the way isolation breaks in practice is not a bad policy — it is a new table nobody remembered to cover. The leak check itself runs with the seed; wiring it into a pipeline that runs on every commit is the next step.Planned

Errors stay inside

A database error never reaches your browser verbatim. Constraint names, table names and values are replaced with a generic message before the response leaves the API.

03Access

Eight roles, twenty-two permissions

Tenant isolation answers “can the other carrier see this”. Roles answer the question your own office asks: the dispatcher who moves the freight has no business approving an invoice, and the controller who approves it has no business reassigning a driver. The matrix is one explicit table, and the check that reads it is a middleware wrapper that runs ahead of the handler instead of inside it. No endpoint wears that wrapper yet: the board is read-only, so today every read is open to anyone holding a membership in the carrier. The first write path ships with it.Planned

#RoleWhat it can reachPermissions
01OwnerEverything, including company settings and user management22 / 22
02AdminEverything except company settings21 / 22
03DispatcherLoads, containers and equipment; reads drivers and customers. Touches no money12 / 22
04CSRLoads, containers and customer records; reads invoices, cannot approve one10 / 22
05BillingInvoices and customer records; reads loads and containers8 / 22
06SettlementsDriver pay and settlements; reads loads, drivers and equipment6 / 22
07DriverReads loads and containers. Writes nothing2 / 22
08ViewerRead-only across the operation6 / 22

Two honest footnotes. First, the dispatch board is read-only today, so every permission that authorises a write is a door onto a room that has not been built yet — they are defined now so that no module can ship without one. Second, the membership row already carries per-person overrides, so somebody will be granted or denied a single permission without inventing a new role for them, which is how real offices actually work. The column and the resolver are written; the screen to edit them is not.Planned

04Surface

What is actually exposed

A pre-launch product has one real advantage: there is very little of it facing the internet. Nothing operational can be written at all yet — the board is read-only — and the endpoints that answer a visitor with no session are the two forms on this website and the authentication routes.

Public forms

Waitlist and contact carry a honeypot field, a rate limit per address and strict validation. Your address is stored as a salted hash so abuse can be traced without keeping the address itself, and the real one is read from a header the proxy sets, not one a client can forge.

Sign-in

Passwords are hashed and kept in a table separate from the user record, with a twelve-character minimum. Session cookies are http-only and secure in production, and the active company on a session can only be written by the server.

Where it runs

One server in Dallas, Texas, with Cloudflare in front of every hostname and TLS on all of them. The full list of parties that touch your data is on the privacy page, and it has three names in it.

Administration

The hypervisor console and shell access do not answer from the public internet; they are reachable only over a private network. The web ports accept connections from Cloudflare's ranges and nothing else.

05Not claimed

The controls we do not have

This list is here because it is the part of a security page a buyer cannot check and every vendor is tempted to blur. Each line stays until the thing is real and can be shown to you.

SOC 2
No report, and no audit engaged. Nobody is “in the process” of anything.
Pen test
No third-party penetration test has been run against the platform.Planned
At rest
Database encryption at rest is not in place. In transit is covered; on disk is not.Planned
Backups
The server writes to a two-drive mirror, which is redundancy against a dead disk and nothing more. Offsite backups and a tested restore are not in place, and a backup nobody has restored is a rumour.Planned
Audit log
The table is in the schema with actor, action, entity and a before/after diff, and nothing writes to it yet — the board is read-only, so there is nothing to record. The rule we hold ourselves to: no module ships a write path until that write is audited.Planned
Staff 2FA
There is no enforced second factor for our own people, and none for your users either. Neither is SSO, SAML or SCIM.Planned

The same list appears in section 7 of the privacy policy, worded the same way. If one of them ever gets quietly upgraded and the other does not, treat both as untrustworthy.

QuestionsStraight answers

What security teams ask us

Could another carrier ever see our loads?

Not through the database. Every table that carries a company_id has row-level security enabled and forced, with a policy that compares the row against the company id set on the current transaction. Every transaction that touches carrier data drops to a database role that has neither superuser nor BYPASSRLS, so the policy applies to it without exception. A query that forgets its filter returns nothing instead of somebody else's containers.

What happens if one of your developers forgets a WHERE clause?

The query comes back empty. That is the whole point of putting the boundary in Postgres: the failure mode of a mistake is a blank screen and a bug report, not a data leak nobody notices for six months. Every query that reads operational data goes through the helper that fixes the tenant on the transaction before the first read. Two lookups do not, because they sit outside the tenant boundary by design — your own user record, and which carriers you belong to — and taking the unscoped database handle out of an authenticated request altogether is still on the list.

How do you know the isolation still works after a schema change?

The development seed creates two carriers, writes a record into the second one, and then queries it as the first. If that query returns anything at all, the seed throws and the run fails. The policies are also re-applied by the migration runner, so a table created next month with a company_id on it cannot ship without one. What does not exist yet is a pipeline that runs the leak check on every commit rather than when somebody rebuilds the seed.

Do you have SOC 2?

No, and there is no audit under way to report on. There is also no penetration test, no encryption at rest and no offsite backup yet. We would rather lose the deal than imply a certificate we do not hold — and a buyer's security team can have a call about what is actually there instead.

Who at DrayBase can reach our data?

The people who operate the platform have administrative access to the server, the way they do at every self-hosted vendor. What we can tell you today is that the administrative surfaces are not exposed to the public internet, that the product has no impersonation feature — reaching a carrier's data means holding a membership row in that carrier — and that we will sign a DPA before you send us anything real.

Is our operating data used to train AI models?

The agent features are not built, so nothing is being sent anywhere today. When they ship, they will run against your own tenant and your data will not be used to train models that serve other customers. Any contract with a model provider has to forbid training on your content before a single record reaches it.

Bring your security team

Ask us the hard version of the question

We will walk through the schema, the policies and the deployment with whoever your side wants on the call, and sign a DPA. If something on your checklist is missing, you will hear it from us before you find it yourself.