Your SaaS Handles Money With a Balance Column. That's Why Your Numbers Don't Add Up at 2 AM.

It was 2 AM and I was running raw SQL queries against a production database, trying to figure out where $47.50 went.
A mentee's balance did not match the sum of their transactions. A mentor was asking why their withdrawal came up $12 short. A session had ended but the payment had not settled. And I had no audit trail to explain any of it.
That was the last night I trusted a balance column.
This is the story of how I built NostroLedger — a double-entry accounting system inside PostgreSQL that tracks every cent flowing through mentoring.oakoliver.com. It is not a separate service. It is not a third-party dependency. It is about 400 lines of TypeScript and a handful of database tables that changed everything about how the platform handles money.
I – The Single-Balance Trap That Every SaaS Falls Into
Most platforms start the same way.
There is a user table. On that table, there is a balance column. When the mentee buys credits, the balance goes up. When a session ends, the balance goes down. When the mentor gets paid, their balance goes up. Clean. Simple.
It works until the third time it does not.
Here is what actually happens in production.
Two sessions end simultaneously for the same mentee. Both transactions read the same balance. Both apply their deductions. One of them wins. The other one's deduction vanishes. The mentee was undercharged and nobody noticed.
Or worse: the mentee's balance goes down by ten dollars, the mentor's goes up by eight, and the two-dollar platform commission just evaporates. It left one account and never arrived at another. It exists nowhere. It is ghost money — and you cannot reconcile a system when money can appear and disappear.
Then someone requests a refund. How do you reverse a session payment? Decrement the mentor's balance and increment the mentee's? What if the mentor already withdrew? What if the original session was only partially completed?
A single balance column gives you a number. It does not give you a story. It cannot tell you why the balance is what it is, how it got there, or where the money went when it left.
II – A 700-Year-Old Idea That Still Works
Double-entry bookkeeping was first described by Luca Pacioli in 1494.
The core concept is deceptively simple. Every financial transaction creates exactly two entries — a debit to one account and a credit to another. The sum of all debits must always equal the sum of all credits. If it does not, something is wrong, and you know it immediately because the books do not balance.
Money never appears. Money never disappears. It only moves between accounts.
In traditional accounting, there are five types of accounts: assets, liabilities, equity, revenue, and expenses.
For the mentoring platform, I simplified this to four virtual accounts per relevant entity.
- Mentee Credit Account — holds purchased credits, which represents a liability from the platform's perspective because we owe them services
- Mentor Earnings Account — holds earned but unwithdrawn funds
- Platform Revenue Account — holds the platform's commission
- Platform Escrow Account — holds money during active sessions, belonging to neither party until settlement
Every movement of money touches at least two of these accounts. Always balanced. Always traceable. Always auditable.
III – The Schema That Changed Everything
The ledger system has three core tables.
The first is the Ledger Account table. Each account has a name — something like "mentee:127:credits" or "platform:revenue" — a type, an optional owner, and a currency. Platform-level accounts have no owner. User-level accounts reference the user who owns them.
The second is the Journal Entry table. Every financial event — a credit purchase, a session start, a settlement, a withdrawal — creates one journal entry. It records what happened: a description, a transaction type, an external reference for correlation with things like Stripe payment IDs, and optional metadata.
The third is the Ledger Entry table. Each journal entry has two or more associated ledger entries. Each ledger entry records the account it touches, the amount, and the direction — debit or credit. The journal entry is the "what happened." The ledger entries are the "where the money moved."
A few key design decisions.
Amounts use four decimal places. The mentoring platform bills by the minute, so fractional cent precision matters during calculation. Rounding to two decimal places happens only at settlement time.
Each entry stores both a positive amount and a direction enum. This makes queries simpler — you can filter by direction without dealing with sign conventions.
Reference IDs link back to external systems. Stripe payment intents, session IDs, withdrawal request IDs. When something goes wrong, you can trace from the ledger entry all the way back to the originating event.
IV – The Invariant That Protects Everything
The heart of NostroLedger is a single function that creates a balanced journal entry.
Before touching the database, it sums all debit lines and all credit lines. If they are not equal, the function throws. It refuses to write an unbalanced entry. This is the fundamental guarantee — the one rule that makes everything else work.
The function then resolves account names to IDs, verifies every referenced account exists, and creates the journal entry with all its ledger entries atomically inside a database transaction.
This single function is the only way money moves through the system. There is no other path. There is no shortcut. Every credit purchase, every session settlement, every mentor withdrawal, every refund flows through this one function, which enforces the balance invariant before writing a single row.
If this function works correctly — and it has one job, which is to refuse unbalanced entries — the entire ledger is guaranteed to balance. Forever.
V – Following the Money Through a Real Session
Let me trace the complete lifecycle of a mentoring session to show how every cent is accounted for.
When a mentee buys fifty dollars in credits, two things happen simultaneously. The platform's cash account receives a debit — cash comes in, the asset increases. The mentee's credit account receives a credit — the liability increases, because the platform now owes the mentee services. Total debits equal total credits. Balanced.
When a session begins, money moves from the mentee's credit account to a session-specific escrow account. The mentee cannot spend those credits elsewhere. The mentor has not earned them yet. The money is in limbo — exactly where it should be during an active session.
When the session ends, escrow is settled. Say the mentee had thirty dollars escrowed and the session cost twenty-five. The escrow account is debited for the full thirty. Twenty dollars goes to the mentor's earnings account. Five dollars goes to the platform's revenue account as a 20% commission. Five dollars goes back to the mentee's credit account as a refund for the unused portion.
Thirty dollars left escrow. Twenty went to the mentor. Five went to the platform. Five went back to the mentee. Every cent is accounted for. The entry balances perfectly.
When the mentor withdraws, their earnings account is debited and the platform's cash account is credited. Cash leaves the system. Earnings decrease. Balanced.
At no point does money appear from nowhere. At no point does money vanish. It only moves between accounts, and every movement is recorded as a pair of entries that sum to zero.
VI – There Is No Balance Column
This is the part that surprises people.
There is no balance column anywhere in the database. The balance of any account is always computed from the sum of its ledger entries. Credits minus debits for liability and revenue accounts. Debits minus credits for asset accounts.
"But is not summing all entries slow?"
At the mentoring platform's current scale — hundreds of sessions per month — the sum query runs in under five milliseconds. When we need to optimize, we will add a materialized balance column that is updated atomically alongside each new entry. But the computed-from-entries approach remains the source of truth. You can always recompute the materialized balance from the raw ledger if they ever diverge.
The absence of a balance column is a feature, not a limitation. It means there is no stale cached value that can drift from reality. There is no race condition between reading a balance and writing an update. The balance is always the sum of every recorded movement. Always correct. Always current.
VII – Proving the Books Balance
The most powerful feature of double-entry accounting is the reconciliation query.
You sum all debits across the entire ledger. You sum all credits across the entire ledger. You compare them. If they are not equal, something is catastrophically wrong.
In practice, this can never happen if all entries are created through the core function that enforces the balance invariant before writing. But running this as a daily health check gives you absolute confidence in your financial data.
On the mentoring platform, we expose this as an admin endpoint. If it ever returns an imbalance, it triggers an immediate alert. It has never triggered. It never should.
That certainty is worth more than any feature. Knowing — mathematically, provably — that your books balance is the difference between "I think the numbers are right" and "I know the numbers are right."
VIII – Why "NostroLedger"?
In banking, a nostro account is an account that a bank holds in a foreign currency at another bank. From Italian — nostro, meaning "ours." It tracks money that belongs to you but is held elsewhere.
The name felt right because every account in the ledger represents money that belongs to someone but is held by the platform. The mentee's credit balance is money they gave us that we owe back as services. The mentor's earnings are money the platform holds until withdrawal.
NostroLedger is a reminder that we are custodians, not owners, of most of the money in the system.
If you are building a platform that handles money — credits, subscriptions, marketplace payouts, anything — I have spent twenty years learning these patterns the hard way. I offer deep-dive mentoring sessions at mentoring.oakoliver.com where we can design your financial architecture together. And if you are looking for a production micro-SaaS framework with billing built in, check out vibe.oakoliver.com.
IX – Lessons Carved in Scar Tissue
Start with double-entry from day one. Retrofitting it onto an existing single-balance system means reconstructing journal entries from transaction logs you probably do not have. The upfront cost is a few extra tables and a few hundred lines of code. The long-term savings in debugging, auditing, and compliance are enormous.
Use fixed-point decimal arithmetic, not floating point. JavaScript's number type cannot exactly represent values like $0.10. Four decimal places in the database. A proper decimal library in the application. No exceptions.
Every transaction must be idempotent. Use reference IDs to ensure processing the same webhook twice does not create duplicate journal entries. Check for existence before writing.
Never update or delete ledger entries. If something was wrong, create a reversing journal entry. This preserves the complete audit trail and makes forensic analysis possible. Immutability is not optional.
The database is the source of truth. Do not cache balances in application memory. Do not trust the frontend's balance display. Always compute from the ledger when making financial decisions.
X – The Result
Since implementing NostroLedger on the mentoring platform, the outcomes have been unambiguous.
Zero reconciliation discrepancies across thousands of transactions. Support tickets about missing money dropped to zero — because we can show mentors and mentees exactly where every cent went. Refund processing went from thirty minutes of manual SQL to thirty seconds of creating a reversing entry. Financial reporting became trivial — revenue, mentor payouts, refund rates, all queryable directly from the ledger.
Double-entry accounting is 700 years old. Luca Pacioli described it in 1494. It survived the invention of banking, stock exchanges, multinational corporations, and the internet.
It will survive your SaaS platform too.
The question is whether you will build your financial system on a proven foundation — or keep running raw SQL queries at 2 AM, trying to figure out where the money went.
What is the worst financial bug you have ever shipped?
– Antonio