TaxChad Technical Architecture ยท Ledger and data
01 Ledger and data model

Arithmetic you cannot break.

Most accounting software treats double entry as a convention the application is trusted to honour. TaxChad treats it as a property of the database. If a journal does not balance, it does not get written, and no amount of application code, refactoring or future carelessness can change that.

233
Tables
262
Triggers
426
Foreign keys
198
Check constraints
Ledger01 / 08
02 The guarantee

Nine truths the database refuses to let you violate.

These are not validations in a form. They are constraints and deferred constraint triggers inside PostgreSQL. They fire at commit time, they apply to every write path including imports, migrations and any future integration, and they cannot be turned off by application code.

GuaranteeHow it is enforced
A journal balances, in both currencies
Debits equal credits in the transaction currency and in the home currency, to within half a penny
trg_journal_lines_balance
deferred, runs at commit
The whole client ledger balances
The entire trial balance is re-totalled at commit and rejected if it does not foot. This is the balance sheet equation, enforced.
trg_client_trial_balance
deferred, deduped per transaction
A journal cannot be emptytrg_journal_has_lines
A line cannot be both a debit and a creditjournal_lines_single_side
No negative amounts
Direction is expressed by side, never by sign
journal_lines_non_negative
The home-currency amount sits on the same side as the originaljournal_lines_base_side_matches
A journal line cannot point at another client's nominal
A tenancy guarantee expressed as an accounting one
trg_journal_lines_client
One nominal code per clientchart_of_accounts_client_code_key
Opening balance journals are dated before the period startstrg_opening_journal_before_period
Why this matters commercially

A bookkeeping product's worst failure is not an outage. It is a set of books that quietly stops adding up, is filed, and is discovered months later by HMRC. Every guard above exists because that class of bug was caught in this codebase, understood, and then made impossible rather than merely fixed.

Guards02 / 08
03 The promotion policy

Prevent where you can. Detect where you cannot. Never pretend.

Every invariant in the system carries an enforcement level, and there is a written policy for deciding it: if a rule is a truth about a single row, it belongs in the database. If it is an emergent property of a whole ledger, or legitimate mid-workflow state, it belongs on a dashboard as a warning and must never be forced into a constraint.

Promoted to hard database guards

  • Journals balance, in both currencies
  • Trial balance foots for the whole client
  • One side per line, no negatives, matching base side
  • Unique line position within a journal
  • Nominal ownership matches the journal's client
  • Unique nominal code per client

Deliberately not promoted, and why

  • VAT control account cleanliness. Live clients legitimately carry balances there. Reporting semantics, not a write-time fact.
  • FX rate coverage. Many rows resolve conversion at read time from the rate table. A constraint would break every import.
  • Uncategorised transactions. Correct, temporary workflow state in the middle of a bookkeeping job.
  • Opening journals before period start as a blanket rule across history. The period start moves each year and would retroactively invalidate correct data.

Some client books carry a genuine, explainable trial balance difference from uncategorised bank movements. The invariant is not that the figure is zero. It is that the figure is pinned, reported and explainable. Ledger Health surfaces it; the test suite asserts it matches the recorded value exactly.

Policy03 / 08
04 When a guard fires

Every rejection is evidence, not just an error.

01

The application pre-checks

Before any insert, the posting core mirrors the database guards in plain TypeScript so the user gets a readable sentence rather than a raw Postgres error. The pre-check is a courtesy. The database is the safety net, and the code says so in as many words.

02

The failure is logged before it is thrown

A blocked posting is written to the guard log even if the user simply closes the dialog, so an attempt that never reached the database is still visible.

03

Database rejections are captured too

The insert path is proxied so that a rejection raised by Postgres is recorded with the same rule code as the application-side block. One rule, one code, whichever layer caught it.

04

The accountant can read the log

The guard log is insert-only: no edits, no deletions, visible to the firm. It is the difference between "the software stopped me" and "here is exactly what was attempted, by whom, when, and which rule refused it".

Guard log04 / 08
05 Multi-currency

Two currencies on every line, from the first entry.

Multi-currency was not retrofitted. Every journal line carries both the transaction amount and the home-currency amount, and both must balance. That single decision is what makes period-end retranslation a report rather than a rescue operation.

Rates

Ingested daily, with a fallback

A primary and a secondary rate source, a nightly ingest job and a separate freshness monitor that raises an alert if rates go stale. HMRC's own monthly rates are refreshed on the first of each month for the returns that require them.

Period close

FRS 102 retranslation

Monetary items retranslate at the closing rate, non-monetary items do not. The close posts a reversing journal, splits realised and unrealised differences to separate nominals, and blocks items that are out of scope rather than silently including them.

Opening balances

Never converted

An opening balance is a home-currency fact and is never FX-converted. Differences arising from multi-currency opening positions post to a dated adjustment, not into the opening figure.

The regression suite runs seeded, offline books that no real client has produced yet: a sterling-home client with dollar and euro bank accounts and a dollar payment gateway, a euro-home client, VAT registered and not. Each scenario is also deliberately corrupted to prove that every invariant still fires.

Currency05 / 08
06 Chart of accounts

One chart. Entity type is a flag.

Company, sole trader and landlord share a single chart of accounts. VAT registration is a boolean on the business record, not a different chart. This is the decision that makes the sole trader and landlord market reachable without a second product.

Tax box on every code
Every income and expense code carries the tax box it lands in, so a quarterly Making Tax Digital update is a grouping of the ledger rather than a separate data entry exercise.Self-employment trade and property each map to their own box set, verified against the published guidance.
Sources stay separate
A trade and a property business are distinct tax sources with their own income, expenses, losses and box set. They are never pooled, and foreign property is separate again.The data model carries an income source on every transaction and split, so the separation is structural rather than a reporting filter.
VAT threshold
Control accounts exist for every business from day one and sit dormant until registration. Crossing the threshold flips one flag. There is no chart migration and no re-coding of history.
Cash and accruals
Cash basis is the default for sole traders, which hides the accruals-only codes; accruals is opt-in. The chart does not change, only what the pickers offer.
Non-negotiable rules
Drawings are not an expense. Capital introduced is not income. Entertaining and depreciation are disallowable add-backs, not deductions. Input VAT on entertaining is blocked. Insurance carries insurance premium tax, never VAT.These are engine rules, not guidance text. They are baked into the seed and the posting logic.
Governance
A master chart is maintained centrally and propagated to clients. Protected control codes cannot be edited or deleted by a client, and post-migration pruning removes only genuinely unused codes.
Chart06 / 08
07 The shape of the data

233 tables, and what they are for.

Core ledger

Journals, lines, chart, banks

Journal headers and lines, chart of accounts, bank accounts, bank transactions and multi-line splits, fiscal years and period locks.

Trading

Sales and purchases

Sales invoices and lines, recurring schedules, purchase invoices, customers and suppliers, payment allocations, credit notes, receipts and expenses.

Tax

Returns and computations

VAT periods and submissions, Corporation Tax computations, Self Assessment sources and disposal events, CIS deductions, dividends, fixed assets and depreciation schedules.

Tenancy

Firms, clients, members

Firms, firm members with scope, clients, client members with role, protected members, groups, invitations and onboarding progress.

Evidence

Audit and logs

Activity log with full snapshots and revert, deletion log, guard log, credential access log, assistant tool call log, HMRC call log, payments audit, protection log.

Operations

Platform and compliance

Backup runs, error events, content security policy reports, usage metering, subprocessor register, consent records, data requests, retention rules, incidents, support tickets.

Four views, 246 database functions and 595 indexes. Reporting is derived from the ledger on read rather than maintained as a second set of stored balances, which is the standard way for numbers to diverge.

Model07 / 08
08 Change control

555 migrations, applied in order, never edited after the fact.

Forward only

Schema changes are files

Every schema change is a numbered migration in the repository, applied in order, recorded in the database. A backup manifest records the exact migration set a snapshot was taken at, so a restore rebuilds the schema before loading a single row.

Self-testing

A guard proves itself in its own migration

The convention is that a migration creating an integrity guard also demonstrates the guard rejecting the thing it is meant to reject, inside the migration. A guard that has never refused anything is not a guard.

Protected paths

Accounting code is fenced

Ledger, VAT, HMRC, tax, FX, payroll and golden-fixture paths are listed in a protected paths file. Touching them requires a real commit message and an entry in an accounting change ledger, and both are checked in CI.

Frozen evidence

Golden fixtures are off limits

Captured real client books are replayed through the reporting core on every push. Regenerating or rebaselining them is forbidden, including to make a failing test pass. A sign-off manifest is checked by CI.

Next

Security and tenancy

How one firm is kept out of another firm's books, what the isolation gates assert, and the gaps we have not closed.

Also

The engine

How a transaction is coded, and where extraction models are and are not allowed near the data.

Migrations08 / 08
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.