Two exports in, a signed three-way trust reconciliation packet out.
A bookkeeper uploads a trust bank statement and a client ledger. Every line is matched, the three balances are proved against each other, and the output is the four-part packet a bar rule asks for — plus an exceptions page naming the overdrawn client, the stale cheque and the bank charge nobody posted. That exceptions page is the part Clio and MyCase stop short of.
Built for one person running the monthly close for eight to fifteen law firms.
MIT licensed. Runs on one machine with no external services — Node, SQLite, and nothing else.
This is a calculation tool, not legal or accounting advice. It arithmetically proves three balances against each other and prints what it found. It does not know your rules, it cannot certify compliance, and it is not a substitute for the professional who reviews and signs the packet. Verify the retention period and report format your jurisdiction actually requires.
The reconciliation engine is well covered — 86 tests, including the cases that decide whether a packet can be trusted: uncleared cheques carried across months, unassigned transactions that must break agreement rather than hide, bank charges flagged instead of absorbed, and an empty or partially-read file that refuses to call itself reconciled.
What has not happened: no real firm has run a real export through this. Column mapping is tested against the documented header shapes of Clio, PracticePanther, QuickBooks, Xero, Chase, Bank of America and Wells Fargo, but documented is not the same as observed. No bar reviewer has looked at the packet. Nobody has been billed for it.
If you use it on real books, the first thing worth doing is checking the packet against a reconciliation you already trust.
npm install
npm start # http://127.0.0.1:3000Open it and press Try the sample files — the demo carries three deliberate problems so the
output shows what the tool is for. /?demo=1 walks the whole flow automatically.
TRUSTPACKET_PASSWORD='a long passphrase' \
TRUSTPACKET_SECRET="$(openssl rand -base64 32)" \
HOST=0.0.0.0 npm startWithout TRUSTPACKET_PASSWORD the server refuses to bind anything but loopback and exits 1.
Client trust records would otherwise be one unauthenticated request away.
| Variable | Default | Meaning |
|---|---|---|
PORT |
3000 |
Listen port |
HOST |
127.0.0.1 |
Interface. Anything else demands a password |
TRUSTPACKET_PASSWORD |
unset | Operator password. Unset means open, loopback only |
TRUSTPACKET_SECRET |
derived | Session signing key. Set it explicitly so sessions survive a restart |
TRUSTPACKET_DB |
data/trustpacket.db |
Database file, resolved against the install directory |
The demo is deliberately open so a prospect can put their own file through it without an account. Anything that reads or writes stored records needs a session:
| Open | Requires sign-in |
|---|---|
| The sample files | Firms, the portfolio, history |
| Reconciling an uploaded pair | Saving a reconciliation |
| Downloading that packet and CSVs | Backup and restore |
Naming a firmId on any request makes it a stored-data request.
Trust records are a multi-year obligation, so nothing here is a one-way door.
npm run backup # dated snapshot into data/backups, keeps the last 30
node scripts/backup.mjs --keep 90 # or wherever, for however longRestore is a merge, not a replace: a firm already present by name keeps its identity and only
gains the periods it is missing, so running the same restore twice changes nothing the second
time. GET /api/backup.json and POST /api/restore do the same over HTTP.
A cron line, if you want it unattended:
17 3 * * * cd /opt/trustpacket && /usr/bin/node scripts/backup.mjs --keep 60 >> /var/log/trustpacket-backup.log 2>&1
Retention wording on the packet follows the firm's configured jurisdiction. California, New York and Florida were checked against their own rules; Texas is from secondary summaries and says so on the document. Anything else asks you to set a period rather than inventing one.
The jurisdiction is frozen into each saved reconciliation, so a firm that later moves does not retroactively change a packet somebody already signed.
None of this is legal advice. The packet states what the firm configured, and a human reviews and signs it.
deploy/trustpacket.service is a systemd unit. On the server:
sudo cp deploy/trustpacket.service /etc/systemd/system/
sudo systemctl edit trustpacket # set the password and secret here, not in the unit
sudo systemctl enable --now trustpacket
curl -s localhost:3000/health # {"ok":true,"auth":true}Put it behind a TLS terminator. The session cookie only sets Secure when the request arrives
over HTTPS, so terminate properly and forward X-Forwarded-Proto.
npm test # 86 unit and integration tests
npm run test:ui # browser pass against a running server on :3000
npm run test:ui:locked# browser pass with a password set, on its own instance
npm run test:a11y # contrast, names, keyboard, live regions — measured, not assumed
npm run check # all four
npm run sample # regenerate the demo files and packetsscripts/ui-check.mjs drives real Chrome and fails on any console error. It has caught bugs
the unit tests could not — a timezone fault that filed records under the wrong day, and a
dropped click handler — so run it before believing a green suite.
| Path | What lives there |
|---|---|
src/reconcile.js |
The three-way reconciliation and the matcher |
src/money.js |
Money and dates. Everything is integer cents |
src/parse.js · src/xlsx.js |
CSV and XLSX ingestion, column guessing |
src/store.js |
SQLite: firms, periods, carry-forward, overrides, backup |
src/packet.js · src/detail.js |
The PDF packet and the working CSVs |
src/jurisdictions.js |
Retention rules, with sources |
public/ |
The browser app |
Money is integer cents everywhere. The product's promise is that the packet foots to the cent; floats cannot make that promise.
Never new Date(someDateString). The constructor reads "06/30/2026" as local midnight,
which east of UTC serialises back as the 29th. That silently filed records under the wrong day
once already. Use parseDate from src/money.js.
The parts most worth improving, roughly in order of how much they would help someone:
Export formats. tests/realformats.test.js pins the header shapes of the systems this was
built against. If your practice management software or bank exports something it cannot read,
a fixture in that file plus an alias in src/parse.js fixes it for everyone. A failing fixture
is a perfectly good contribution on its own.
Jurisdictions. src/jurisdictions.js carries retention periods with a source and a checked
date. California, New York and Florida were read from their own rules; Texas is from secondary
summaries and says so on the printed packet. Adding a state means citing where the number came
from — an unsourced entry is worse than an absent one, because it prints as though it were
authoritative.
The packet itself. It is built to a general reading of a monthly three-way reconciliation. If your bar expects a specific arrangement or an additional schedule, that is worth knowing.
Run npm run check before opening anything: unit tests, two browser passes, and an
accessibility audit that has to come back with zero findings.
The monthly three-way trust reconciliation is required by rule in every US state and, for a bookkeeper carrying eight to fifteen firms across four different accounting systems, is still largely done by hand. The tools that do it well assume the firm's books live inside them and charge per firm or per seat, which does not fit somebody serving ten firms they do not own the subscriptions for.
Whether that gap is worth paying to close was never established — nobody was ever asked. The code is here because the problem is real even if the business was not proven.
MIT. See LICENSE.