The product holds other people's books. The only version of a tenancy guarantee worth anything is one that fails a build when it stops being true. This page describes what is enforced, how it is tested, and, in equal detail, what is not.
There has been no third-party penetration test. What exists is an internal audit of the source and the database carried out in August 2026, every finding of which was fixed, verified in code and covered by a test. A formal external test is planned and has not happened yet. Until it has, this page will keep saying so.
A firm is an accountancy practice. A client is a set of books. The two questions "which firm are you in" and "which of that firm's clients may you touch" are answered separately, and both have to pass.
Row-level security is enabled on all 233 public tables, with 491 policies. Anything reaching the database as an authenticated user, including the assistant integration, is filtered by those policies before a row is returned. A policy holds even if the application above it has a bug.
A subset of server handlers query with the service role for operations that legitimately cross a row-level boundary, and those handlers enforce authorisation in code against the caller's session. That is a real design trade-off and worth being precise about, because a policy test cannot see it.
Row-level security is the outer wall and it covers everything. For the handlers that use the service role, the boundary is the check written in that handler. In the August 2026 audit this is exactly where the single most serious finding was: a cross-firm access path that was live until it was fixed. The response was to fix it, add a regression test, and adopt a standing discipline that every new service-role handler is reviewed for the same trap. It is a discipline, not a solved problem, and it is a real reason the first engineering hire matters.
Both run against a real PostgreSQL database in continuous integration, impersonating the exact role and session claims that a browser request produces, inside a single transaction that is always rolled back. If the plan does not run to completion with zero failures, the build fails.
Builds two client companies in one firm, a billing-only client, a second firm and a set of bank accounts, transactions, splits, journals, journal lines, invoices and expenses. Then it plays five personas: a member of client A, a member of client B, a firm owner, staff scoped to assigned clients only, and an unrelated authenticated user. It asserts positive counts for each persona's own data and zero for the other tenant across every table, that cross-tenant updates and deletes affect no rows, and that a cross-tenant insert is refused outright. The suite was extended in September 2026 with assertions covering the restricted platform access tier and the founder lock, so a change that weakened either would fail the build.
Targets the elevated database functions reachable over the API. It proves that propagating a chart, generating a recurring invoice or writing an audit row refuses to act on another firm's client, recounting with security disabled to prove nothing was written, and that the functions which should not be callable by an ordinary user are not.
An accountancy platform is judged on whether it can show what happened. Every sensitive action writes to a dedicated table, and the client can see the ones that concern their own books.
| Log | What it captures |
|---|---|
| Activity log | Every bookkeeping create, update, delete and reclassification, with a full snapshot, the reason, the actor and a restore path |
| Deletion log | Deleted bank transactions with their snapshot, so a deletion can be explained and reversed |
| Guard log | Every integrity rejection, from the application or the database, insert-only, no edits and no deletions |
| Credential access log | Every time a stored credential is viewed or revealed |
| Assistant tool log | Every assistant tool call, with arguments masked and the result status recorded |
| HMRC call log | Operation, path, masked headers, payloads, status, correlation id, attempts and duration for every call to HMRC |
| Payments audit | Payment operations, with the actor stamped from the session so it cannot be forged, and no insert policy at all for ordinary users |
| Protection log | Attempts to remove protected members or otherwise cross a privileged boundary |
A firm-facing data access log unions the activity, deletion, assistant and credential logs behind a single gated function, so a client can be shown who looked at what and when. There is deliberately no platform-operator override on that view: it was removed as part of the August 2026 audit.
The audit write and the action it records are not in a single database transaction. A failed audit write raises loudly, but that is not atomicity, and the code says so in as many words rather than letting a future reader assume otherwise.
Scheduled work calls the application over HTTP with a secret read from the database vault. Without the secret the endpoint refuses. The target host is a vault value, not a hardcoded string.
Each inbound webhook is verified against its own provider's scheme before anything is written, and unverified traffic is rejected rather than parsed.
An audit found that the transactional send endpoint accepted a caller-supplied recipient, body and download link, which made a domain-signed phishing vector. It now sends only allowlisted templates, is rate limited, and the logs confirmed no prior abuse.
Seven rounds of read-only review followed by fixes, driven by a single rule: never accept a claim that something is secure, make the tooling paste the actual value or the actual code. Every finding below came from that discipline.
One item was parked deliberately rather than quietly: a settings table that is still a single global row and will need a firm key before the second firm signs. It is written down, and it is the sort of thing this page exists to disclose.
| Item | Status | Plan |
|---|---|---|
| Third-party penetration test | Not done | Planned. Nothing on this site claims otherwise. |
| Service-role handlers | Standing review discipline | Every new handler reviewed for the authentication-is-not-authorisation trap. A second engineer materially improves this. |
| One privileged helper still callable directly | Deliberate, with a compensating control | Fourteen of the fifteen public policy helpers are closed to ordinary users and the isolation gate now asserts it. One remains callable because a Self Assessment screen calls it directly. It needs its own tenant check, which is scheduled work rather than an oversight. |
| Token key derivation salt | Known limitation | Change with a re-encryption migration, not in place. |
| Single-row settings table | Parked | Needs a firm key before the second firm onboards. |
| Break-glass access | Disclosed, not defended against | Controls in the application defend against escalation by a team member. They do not defend against whoever holds the database service credentials. Today that is the founder. It is a governance question that a second person on the team starts to answer, and we would rather write it here than wait to be asked. |