← Sales Scribe
CASE STUDY / SALES SCRIBE
Working in six weeks.
Trusted with the books
in five months of work.
Sales Scribe is an invoicing and double-entry accounting application for UK small businesses. I built it on my own, part-time, with AI doing most of the typing and me doing all of the deciding. This is the honest account: the constraints, the calls I made, the things the AI got wrong, and what I’d do differently next time.
What this means for your business.
Sales Scribe handles multi-currency transactions with automatic exchange-gain and loss accounting, and tracks Stripe payments through fees, payouts, refunds and chargebacks. Accountancy practices can manage access for each member of staff and each client, with mandatory two-step verification and an audit trail of their actions. A working version took six weeks; further review and testing brought it to public staging and structured user testing. It remains pre-launch, with a rehearsed backup restore required before paying customers use it.
For your project, the same approach starts with one agreed workflow and a fixed price. I review the code, test real business scenarios and database behaviour, and require evidence that fixes work. Security checks and a proven recovery path are part of release readiness, with source code and documentation handed over so another developer can maintain what you own.
6weeks to a working versionfirst commit to a posting ledger
22weeks of actual workpart-time, about 25 hours a week
2,100+automated testsrun against a real database
2adversarial code reviews1 Critical, 12+ High findings — all fixed
01 / THE BRIEF
A real product, not a demo.
The aim was never a prototype. It was software a UK business could actually run on: issue invoices, take payments, keep books that an accountant would sign off, and stay on the right side of VAT and the Companies Act. That meant a proper double-entry general ledger underneath everything, not an invoicing tool with a “reports” tab bolted on.
I chose it as the first product because it is unforgiving. Money has to balance to the penny, historical documents must never silently change, and one customer must never see another’s data. If the way I work with AI holds up here, it holds up for most business software.
Three numbers, so nobody feels spun
Six weeks from the first commit on 17 November 2025 to a working system: invoices, credit notes, purchase documents, payments, PDFs, email and a posting double-entry ledger, all committed by 26 December. The dates are in the timeline below and in the repository. That is the “rapid” part, and it is what a client’s first version looks like—except a client’s first version is one workflow, not an accounting suite.
Twenty-two weeks of actual work, part-time—about five months. Everything after week six was about making the software trustworthy enough to hold other people’s financial records: review, hardening, testing, compliance. That is the part most quick builds skip, and the part I won’t.
Ten months on the calendar, because for nearly five of them—mid February to early July—Sales Scribe waited while I built two other products. One person, 25 hours a week: things queue. I’d rather say so than pretend otherwise—and be clear about what it means for a client: I run one client build at a time, and client work takes priority over my own products. That is why those queued, and yours won’t.
The constraints
- One person, about 25 hours a week. Design, code, review, testing, documentation, operations—and two other products competing for the same hours. No second pair of eyes on the team — which is why external-style review became part of the process (below).
- Correctness before launch, not after. Financial records can’t be patched up later. Nothing ships to a paying customer until backups are proven by a rehearsed restore.
- Mainstream technology only. ASP.NET Core on .NET 10 (LTS), EF Core, MariaDB, Stripe. Anything a competent .NET developer could pick up without decoding something unusual.
- Multi-tenant from the start. Many businesses on one system, with accountants able to work across their clients — so tenant isolation is a security property, not a feature.
03 / THE DECISIONS
Five calls that mattered.
01 ↗︎The ledger is the only source of truth.
Early on there were two records of what a customer owed: the general ledger, and a separate table of “account entries” for statements and balances. Two records will drift. In December 2025 I deleted the second one and derived statements and balances from ledger lines instead, adding the customer and supplier references the ledger needed to make that work.
Why it matters: when a customer asks why their statement doesn’t match their invoices, there is exactly one place to look, and it is the same place the accountant looks.
02 ↗︎Issuing is atomic; issued documents are immutable.
Found during testing: an invoice could be marked “Issued” even when its ledger posting failed, leaving an invoice with no accounting behind it. The fix was to wrap issuing in one database transaction — either the status changes and the postings exist, or nothing changes. From then on, issued documents are frozen; corrections happen by reversal, and the PDF as issued is archived at that moment.
Why it matters: this is the property an auditor cares about most, and the review in July confirmed it held: no normal path can silently alter a historical document or posted ledger.
03 ↗︎Tenant isolation moved from “remember to check” to “cannot forget”.
For the first eight months, most queries carried a hand-written “where organisation = this one” clause. The July review found three services where that had been missed — a cross-tenant hole. Rather than patch the three, I rebuilt isolation as a global query filter that fails closed (no organisation context means zero rows, not all rows) plus an interceptor that validates every write before it reaches the database. Done in small waves, each with its own tests against a real MariaDB.
Why it matters: a security property that depends on a developer remembering something will eventually be forgotten. The second review, looking hard for one, found no exploitable cross-tenant hole.
04 ↗︎Ship the safe version first, prove the better one later.
Un-allocating a supplier payment that had already posted to the ledger left the books drifting. Two options: block it (small, safe, symmetric with the customer side) or build a full reversing ledger event (better, but only provable against a real database, which that session couldn’t run). I shipped the block the same day and the full reversal two days later, once seven integration tests on a real MariaDB proved it nets to zero.
Why it matters: the order was chosen by what could be proven at each point, not by what was most impressive.
05 ↗︎Backups before customers. A wedge before breadth.
The August competitive analysis was clear: the incumbents have a decade of accreted features and I cannot out-list them. So the plan targets one country, one customer type, and one complete loop — sign-up to issued invoice to online payment to bank reconciliation to a VAT return filed with HMRC — done better than they do it. And the first item on that plan is not a feature: it is nightly encrypted backups with a scheduled restore rehearsal that asserts a known invoice’s PDF byte-matches. A backup never restored is a hope, not a backup.
Why it matters: you cannot hold someone’s financial records without a proven recovery path. It goes first and is never skipped.
04 / WHERE THE AI WAS WRONG
What the reviews caught, and who caught it.
This is the section a cautious buyer should read twice. AI writes fast and confidently; that is the point, and also the risk. The value I add is the review. Here is what it found.
The Critical finding.
The invoice edit path trusted a client-posted “amount paid” field with no server-side validation. A draft could be issued showing money received that had never been received, breaking agreement between the ledger and the customer’s account. Found by the July review; fixed by making the settlement engine the sole owner of paid and balance figures and recomputing at issue.
Secrets and access.
Live API keys had been written into a configuration file and committed. A diagnostics endpoint, protected only by “must be logged in”, would let any user of any organisation read the mail configuration and overwrite a process environment variable. Both were the kind of thing an AI produces when told to “make it work” and nobody asks the security question. Rotated, removed, and a source-scan guard added so it can’t recur.
“Complete” that wasn’t.
The second review was told not to trust the wave reports. It found that a promised set of architecture guard tests had never been written; that a tax-calculation test suite exercised a re-implemented private copy of the logic, not the production code; that the database schema had drifted from the model so two unique indexes existed only in memory; and that the new two-factor authentication feature was silently non-functional because a provider was never registered. Each was documented as done. None was.
The remediation was applied surface-by-surface, not invariant-by-invariant.
— Second code review, 16 July 2026. The single most useful sentence anyone wrote about this project.
The small ones that would have shipped.
- False-green builds. The front-end bundler had failed with three syntax errors; the AI’s output filter matched nothing and it reported “bundles built” several times while the app served stale JavaScript. Caught only because I checked the actual page in a browser.
- A wrong first diagnosis. A payments bug was “fixed” for individual customers, but my test customer was a company. Correcting that one fact led to the actual cause — a third code path nobody had mentioned.
- Four attempts at one bug. Optimistic concurrency on customers and suppliers silently let one user overwrite another. Three plausible fixes failed. The root cause was a single missing configuration line; the fix that worked was the simplest of the four. The documents are titled, in order, “fix”, “v2 final”, “v3 final solution” and “v4 actual final”. I keep them as a reminder.
- An assumption not checked against live data. In September a billing feature was built on the assumption that each account had exactly one payment profile. It didn’t. The lesson in my own notes: “should have checked the live row before shipping.”
And one that went the other way: a profit-and-loss report that “didn’t reconcile”. I queried the ledger directly. It reconciled exactly — draft documents don’t post, and goods for resale go to the inventory asset, not expenses. Not a bug. Knowing when not to change code is part of the job.
05 / WHAT I’D DO DIFFERENTLY
Four things, honestly.
- Establish the cross-cutting invariants on day one, globally. Tenant isolation, secrets outside the repository, currency minor units, personal data out of logs. Each was eventually done properly; each cost more because it was retro-fitted across ninety entities instead of being the shape of the first ten.
- Bring in adversarial review at month two, not month eight. The July reviews were the most valuable two days of the project. Nothing about them required waiting.
- Never accept a completion report as evidence. A document saying “complete” is a claim. A test that exercises production code, running in CI, is evidence. I now require the second before I believe the first — and the second review found CI had been effectively off.
- Prove database behaviour against the real engine from the start. The concurrency saga and the supplier-reversal work both turned on how MariaDB actually behaves, not how a mock does. Real-database integration tests are now the norm, not the exception.