Security

Isolation is not a policy.
It is the architecture.

No organization here can reach another's data, because the rule that says so lives in the database rather than in application code. This page explains exactly how that works, in enough detail that the most sceptical person on your side can check it.

Row-level security on every org tableRestrictive policy, never widenedMembership checked live, not cachedFull data export, any plan, any time
The tenancy fence

The boundary lives
inside the database.

Most multi-tenant software keeps its customers apart in application code. Every query is supposed to carry a filter, and safety depends on every engineer remembering it, every time, forever. We moved the boundary down one layer — to the only component that sees every query that will ever be run.

Two organizations reading from one database. A policy gate between them passes each organization through to its own rows, and refuses any request that reaches across into the other organization’s rows.The isolation policyevaluated by Postgres on every single statementOrganization AA signed-in sessionwith one membershipOrganization BA signed-in sessionwith one membershipOne databaseRows belonging to Areturned to A onlySame tables. Same rows.One column apart.Rows belonging to Breturned to B onlyA reaching for B’s rowsrefused
Both organizations read the same tables. The gate is a rule Postgres evaluates itself, on every statement, before a single row is returned. A request that reaches across is refused at the database — not filtered out afterwards by code that had to remember to do it.
I

Every table carries the fence

A table that holds organization data has a non-null organization column, row-level security switched on, and a policy comparing that column to the caller’s own organization. The comparison happens inside Postgres, before any row is handed back. Application code cannot forget it, because application code is not the thing doing the checking.

Enforced in the database

II

Restrictive, so nothing can widen it

The isolation policy is declared restrictive, which means the database ANDs it against every other rule on the table. Other policies can narrow what a person sees — a resident sees less than a manager. Nothing can widen it past their own organization: not a new feature, not a mistaken query, not a policy written years from now by someone who never read this page.

Restrictive, not permissive

III

Membership is read live, not remembered

For a signed-in person, the current-organization check is a database lookup performed on each statement rather than a value cached in their session token. End a membership and the next query that person makes is already refused. Nobody waits for a token to expire, and nothing has to be revoked in a second place.

Immediate revocation

IV

Our own code goes through the same gate

Server code reaches your data through clients that carry an organization identity and stay subject to the same policies a browser session is. One unscoped client exists that bypasses row-level security — for things like webhook traffic arriving before an organization is even known — and its call sites are enumerated in a file that CI checks against the codebase.

Allowlisted in CI

The practical difference is what happens when someone makes a mistake. A forgotten filter in application code returns another organization’s rows and looks like a working feature. A forgotten filter here returns nothing, because the database was never going to hand those rows over in the first place.

Identity and access

Who someone is,
and where that ends.

Access is not a flag on a user account. It is a membership in one organization, carrying the roles that person holds there and, if you want it, a date on which it stops.

I

Membership, not a global role

Access is a row that names one person, one organization, the roles they hold there, and when that ends. There is no account-level role that grants reach everywhere, and no way to be half-inside an organization.

II

Roles are assigned server-side

Nobody can change their own role. The membership table refuses writes from the browser outright. Roles move through organization setup, an accepted invitation, or an operator action — all of them server-side paths with their own checks.

III

Revocation takes effect immediately

Because the organization check is a live lookup rather than a cached claim, removing a membership stops the next query that person makes. There is no window in which a still-valid session keeps working.

IV

One organization per session

Someone who belongs to more than one organization acts inside exactly one of them at a time. Switching is a server action that re-verifies the membership against the database before it will move them.

Portability

Your data stays yours.
Including on the way out.

Export is on every plan, at any time. It is not a retention lever, and we have not built it to feel like one.

The way most software makes leaving expensive is rarely a clause in the contract. It is an export that takes six weeks, arrives incomplete, and needs three calls to chase. Ours is one file, produced on request, containing the tables your organization owns.

One archive, every table

The export asks the database which tables carry your organization column, at the moment it runs, and dumps each of them. There is no hand-maintained list to fall out of date, so a table added last month is in the file without anyone remembering to add it.

On every plan, at any time

Every tier, whenever you ask. Not a feature of the top plan, not an add-on, and not something you have to be leaving in order to request.

Handed over privately

The archive is written to private storage and delivered as a short-lived signed link — not a public URL left sitting somewhere indefinitely after the handover.

Stopping payment does not erase you

If a subscription ends, the organization moves to a read-only state: portals stay readable, automations and new charges stop. Your records are still there while you work out what happens next.

Deletion is a request, answered by a person

We do not run an automatic purge, and we are not going to imply that we do. If you want your data destroyed, ask, and we agree the scope and the timing in writing before anything is touched.

How we ship changes

The checks that run
before anything ships.

A security property that depends on people remembering it is not a property. These are the gates that fail the build instead of trusting anyone's memory — ours very much included.

Migrations are written by hand and reviewed

Schema changes are hand-written SQL, reviewed like any other code, applied in order. A migration that has already run is never edited — a correction is a new migration stacked on top. The history of the database is a record rather than a draft.

The schema check asks the database, not a list

Before a build passes, a check queries Postgres for every table carrying an organization column and fails if any of them lacks row-level security, lacks the isolation policy, or lacks the index that keeps the check cheap. It cannot go stale, because there is no list to keep in sync.

The unscoped client is enumerated

The single database client that bypasses row-level security has its call sites listed in a file CI reads back against the source. Adding one is a visible decision inside a pull request, not an import that arrives quietly.

The application refuses to start unsafely

On boot, the app asks the database what role it is actually connected as and throws if that role is able to bypass row-level security. A misconfigured connection fails loudly at startup instead of turning every policy into a silent no-op.

Tests assert the negative case

Isolation tests do not only prove that an organization can read its own rows. They prove that a second organization, querying the same table, gets nothing back. A fence tested only from the inside has not been tested.

No shortcut for local development

There is no development-only authentication bypass anywhere in the codebase, and a lint stops one being reintroduced. What runs on an engineer’s laptop is the same authentication path that runs in production.

Plainly

What we have not
claimed here.

We do not hold SOC 2, ISO 27001, or any other third-party security attestation.

We are not going to imply one either — not with a badge, not with a trust page borrowed from a vendor, and not with a sentence written to be misread. If that changes, it will say so here, and it will be dated.

Everything above this section is a description of how the software is built, and all of it is checkable. Bring your engineer or your counsel and we will walk through the actual policies, the actual build checks and the actual tests, rather than returning a questionnaire with the boxes ticked.

Card payments run through Stripe. Card numbers do not reach our systems — which is a statement about where the data goes, not a certification we are claiming to hold.

If a specific certification is a hard requirement on your side, say so early. We would rather hear it as a requirement than guess at it as a marketing exercise.

Security review

Send us the questionnaire.
We will answer it.

Security review before a contract is normal, and we would rather do it early than late. You get specifics and a person on the call, not a PDF and a wait.

Every claim on this page maps to something in the codebase. Ask, and we will show you where.