TaxChad Technical Architecture · Security
01 Security and tenancy

Provable beats promised.

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.

Stated plainly, up front

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.

Security01 / 09
02 The tenancy model

Two independent boundaries, not one.

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.

Firm layer
A user holds a seat in a firm as owner or staff, and that seat carries a scope: all clients, or only assigned ones.Assigned scope is enforced in the database, not by hiding menu items.
Client layer
A user is a member of a client with a role: owner, manager, bookkeeper, viewer or accountant. Capability follows role rank.
Protected members
Every firm can mark members as protected so a client cannot accidentally remove the accountant who files their return. Protection is visible to the client, and the client can ask for removal.Attempts to remove a protected member are written to a protection log rather than silently failing.
Platform operators
A separate platform-operator tier exists for the founder's own operational screens: backups, error events, the subprocessor register, reference data. In September 2026 a second, restricted appraisal tier was added alongside it, with money-movement screens, revenue close, role granting and third-party personal data reserved to the owner tier, plus a founder record that cannot be stripped from inside the application.Firm isolation is not relaxed for either tier. A platform operator sees no client books outside firms they hold a seat in.
The founder's own access
The founder's stated design requirement is that TaxChad cannot read another firm's books or client names, and that this is provable rather than promised. The billing view a platform operator sees is aggregate: counts, seats, tier and status, not client rows.
Tenancy02 / 09
03 Where enforcement lives

Two layers, described honestly.

Layer one

Row-level security in PostgreSQL

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.

Layer two

Authorisation in the server functions

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.

The honest version

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.

Enforcement03 / 09
04 The isolation gates

Two suites that fail the build if a tenant boundary moves.

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.

Gate one · 83 assertions

Tenant isolation

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.

Gate two · 37 assertions

Privileged function isolation

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.

What these gates do catch

  • A policy edited, dropped or weakened so another firm's rows become visible
  • A new table shipped without the right policy set
  • An elevated database function that can be pointed at another tenant
  • A privileged helper accidentally made callable by ordinary users
  • A regression in scoped staff access

What they do not catch

  • Anything outside PostgreSQL, including the service-role handlers described above
  • Application-layer authorisation bugs
  • Storage bucket policy mistakes
  • Session and token handling
  • Any table or function not named in the fixtures
Gates04 / 09
05 Evidence

Reads are recorded, not just writes.

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.

LogWhat it captures
Activity logEvery bookkeeping create, update, delete and reclassification, with a full snapshot, the reason, the actor and a restore path
Deletion logDeleted bank transactions with their snapshot, so a deletion can be explained and reversed
Guard logEvery integrity rejection, from the application or the database, insert-only, no edits and no deletions
Credential access logEvery time a stored credential is viewed or revealed
Assistant tool logEvery assistant tool call, with arguments masked and the result status recorded
HMRC call logOperation, path, masked headers, payloads, status, correlation id, attempts and duration for every call to HMRC
Payments auditPayment operations, with the actor stamped from the session so it cannot be forged, and no insert policy at all for ordinary users
Protection logAttempts 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.

A precision we insist on

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.

Audit05 / 09
06 Secrets and data handling

What is encrypted, where keys live, who is in the chain.

In transit and at rest
TLS in transit, provider encryption at rest, UK region in normal operation. Backups are encrypted independently of the provider.
Application-level encryption
HMRC tokens and stored client credentials are encrypted by the application on top of the provider's encryption, using authenticated encryption with a version tag on every value.Known limitation, recorded rather than glossed: the token key derivation uses a fixed salt. It is on the list to change, with a re-encryption migration, not a silent rewrite.
Secrets
No secrets in the repository, enforced by a lint-time check. Credentials live in the platform secret store and in the database vault. Environment files are ignored by version control.
Storage
Eight private buckets. No public bucket holds client documents, and downloads are brokered by a server function rather than by a public URL.
Subprocessors
A formal register is maintained in the product itself, split into parties that process client personal data and tooling that must not. It is reconciled against the privacy policy every release, and a party that breaches its conditions is promoted to the processing register and the policy is updated.In August 2026 that reconciliation caught three services reached by production code that were missing from both the register and the policy. All three were added.
Two standing tests
No client personal data in the repository, in any form, including fixtures, dumps, screenshots and logs. No production call carrying client data to a tool that is not covered by a data processing agreement.A payee lookup that breached the second rule was found and disabled behind a flag that defaults to off, rather than left running because it was useful.
Data06 / 09
07 Automated paths

Anything that runs without a human fails closed.

Scheduled jobs

Secret on every trigger

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.

Webhooks

Verified per provider

Each inbound webhook is verified against its own provider's scheme before anything is written, and unverified traffic is rejected rather than parsed.

Email

No caller-supplied content

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.

Automation07 / 09
08 The August 2026 audit

Five real findings, every one surfaced the same way.

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.

Finding
Resolution
Cross-firm accountant access, a genuine tenant isolation breach, live until it was fixed
Fixed in code, verified, and now covered by the isolation gate that runs on every push
The transactional email endpoint accepted caller-supplied recipient, body and link, a domain-signed phishing vector
Caller-supplied fields removed, templates allowlisted, rate limited. Logs confirmed no prior abuse and no notifiable incident
A delete path could remove any user, including a platform owner
Scoped to the firm, hard-blocks protected members and the platform owner, and writes an audit row before deleting
A share link advertised a 30-day expiry that was not enforced anywhere
Expiry is now mandatory on the record and enforced at all three endpoints
Two tables trusted a paid-access check alone, without also requiring membership
Membership is now required as well, and checkout binds the client to a real member

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.

Audit08 / 09
09 Open items

The list we would want to see if we were you.

ItemStatusPlan
Third-party penetration testNot donePlanned. Nothing on this site claims otherwise.
Service-role handlersStanding review disciplineEvery new handler reviewed for the authentication-is-not-authorisation trap. A second engineer materially improves this.
One privileged helper still callable directlyDeliberate, with a compensating controlFourteen 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 saltKnown limitationChange with a re-encryption migration, not in place.
Single-row settings tableParkedNeeds a firm key before the second firm onboards.
Break-glass accessDisclosed, not defended againstControls 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.
Next

Build and run

The CI pipeline gate by gate, backups and disaster recovery, monitoring, and incident response.

Also

Integrations

HMRC, fraud prevention headers, open banking and payments.

Open items09 / 09
TaxChad is built and operated by TaxChad Ltd, company number 17346680, registered in England and Wales. This page describes the platform as built on 19 September 2026. Counts of tables, migrations, tests and lines of code were read from the live database and the repository on that date and move as the product does. Anything described as in build or planned is a statement of intent rather than a commitment. Regulated accountancy services are provided by PowerStone Accountants Ltd, a separate company, under its own licence and insurance. Nothing here is tax, legal or investment advice.