Security
How your data is isolated
This page describes what is actually implemented, in enough detail that a technical reviewer can check the claims. Where something is a foundation rather than a finished control, it says so.
Tenant isolation
Every tenant-owned table carries a tenant identifier and is protected by PostgreSQL row-level security with FORCE enabled. Policies specify both a read predicate and a write predicate — a policy with only a read predicate still permits inserting a row belonging to another tenant, which is a common and quiet mistake.
The application connects as a database role that is not a superuser, does not hold BYPASSRLS, and owns no tables. Those are the two ways row-level security is silently defeated in practice: PostgreSQL exempts superusers, and it exempts table owners unless FORCE is set. Both are closed here, and both are asserted by tests that run on every commit.
Tenant context is set with SET LOCAL inside the request transaction, so it is discarded on commit and cannot leak into whichever request borrows that pooled connection next.
Absence of context fails closed. A query with no tenant bound raises an error rather than returning zero rows. Zero rows is indistinguishable from a genuinely empty table and would ship unnoticed; an error cannot be mistaken for anything else.
Authentication
Passwords are hashed with Argon2id, which is memory-hard. bcrypt is not, and it silently truncates input beyond 72 bytes, turning a long passphrase into a shorter secret than the user chose.
Sessions are opaque server-side records, not JWTs, so revocation is immediate. Only a SHA-256 hash of each session token is stored: reading the database yields hashes, not usable sessions. The session identifier is rotated on sign-in, so an attacker who fixes a pre-authentication session does not retain it afterwards.
Session cookies use the __Host- prefix with Secure, HttpOnly, and SameSite=Lax. The prefix forbids a Domain attribute, which is what prevents a subdomain from writing the cookie.
Sign-in is rate limited per IP address and per account independently. Per-IP limiting alone does not stop credential stuffing distributed across many hosts, where each address makes a single attempt and no per-IP threshold is ever crossed.
Sign-in takes the same time whether or not the account exists: a real hash verification runs against a dummy value when there is no matching account, so response timing does not reveal which addresses are registered.
Verification and password-reset tokens are single-use, hashed at rest, short-lived, and redeemed with an atomic operation so two concurrent redemptions cannot both succeed. Resetting a password revokes every existing session for that account.
Authorisation
Nine tenant roles and four platform roles resolve through a permissions table rather than through comparisons against role names. What a role can do is data, so changing it is an administrative action rather than a deployment. A test scans the codebase and fails the build if any file makes an authorisation decision by comparing a role name.
Platform administration is structurally separate from tenant membership. Cross-tenant access by platform staff requires an explicit, time-boxed elevation naming exactly one tenant, with a mandatory written reason. The elevation record must exist before the data can be read — the audit entry is the key, so it cannot be skipped. Elevated access is read-only.
Application and transport
A Content Security Policy is enforced with a per-request, cryptographically random nonce and strict-dynamic. unsafe-inline is not used anywhere: it would permit any injected inline script, which is the attack the policy exists to prevent.
Secure headers are set on every response, uploads are validated by content rather than by declared type, and parsing runs without network access and with bounded memory, time, and decompression ratio.
The database and cache have no published network ports. Only the reverse proxy is reachable from outside the host.
AI safety
The assistant has no ability to query the database directly. It can call a fixed set of deterministic functions with validated arguments, and the tenant identifier for every call is taken from the authenticated request rather than from anything the model produced.
Retrieved document text is delimited as untrusted content. Instructions found inside an uploaded document are reported to you rather than followed — and even a fully subverted model has no tool that could reach another tenant, because retrieval is filtered before the prompt exists.
Model output is rendered as data. It never becomes SQL, a shell command, a file path, or unescaped markup.
Auditing and backups
The audit log is append-only at the database grant level: the application holds insert and select permissions and nothing else, so a compromised application cannot rewrite the trail. Sign-in, sign-out, failed sign-in, privilege changes, exports, and administrative access are recorded.
Backups are encrypted client-side before leaving the host, written with append-only credentials so a compromised host cannot destroy backup history, and the pruning credential is held separately. Restores are verified by checking that isolation policies are present afterwards, not only that rows came back.
What we do not claim
MazeCFO is not certified against SOC 2, ISO 27001, or any comparable standard. We build the technical foundations that such an audit examines — access controls, audit trails, retention policies, data export and deletion, incident response and backup documentation — but we have not been audited, and we will not imply otherwise.
Two-factor authentication exists as a foundation rather than an enforced policy. Single sign-on is available on Enterprise as a foundation.
If you need evidence for a vendor review, ask on the contact page and we will describe the current state accurately rather than send a badge.