Finloop
A double-entry ledger with envelopes, bills, projects and a month-end statement.
Finloop answers four questions, in order of how often they get asked: what do I have, where did it go, what do I owe whom, and was that job worth doing.
The ledger underneath everything
Every movement of money is a transaction with postings and lines.
- Postings say which accounts moved and by how much. They must sum to zero.
- Lines say what the money was for — the category breakdown.
- The invariant is
sum(postings) == sum(lines), checked on every write.
LedgerService.post() is the only code path that changes an account balance. Not one of several —
the only one. A balance that can be written from two places is a balance that will eventually
disagree with its own history.
A TRANSFER may carry zero lines: moving money between your own accounts is not spending and has
nothing to categorise. Every other kind of transaction requires at least one category line.
Read more: the ledger.
Accounts, including the ones that are not money
Seven account types. Three of them are not spendable and exist to model obligation rather than cash:
| Type | What it is | Spendable |
|---|---|---|
CASH, BANK, MOBILE_MONEY, SAVINGS | actual money | yes |
DEBT | what you owe someone | no |
RECEIVABLE | what someone owes you | no |
PREPAID_VENDOR | a balance sitting with a vendor | no |
Modelling a debt as an account rather than a separate table means repaying it is an ordinary transfer, and its history is the same history everything else has (ADR-0010).
Envelopes
Virtual, over a single pooled balance — not separate accounts. An envelope is a claim on money you already have, so filling one moves nothing and cannot fail. What it changes is what the app is willing to call free to spend (ADR-0012).
Read more: envelopes.
Bills and repayments
A recurring bill produces a pending item on its due date and nags until you confirm it. It never posts the money itself — the system must not claim money moved when it did not (ADR-0014).
A debt repayment plan is a recurring bill that settles an account. Rent and pay Beth 20,000 on the 5th are the same shape: due on a date, nags until you say you paid. So they share one nag mechanism, one escalation job and one confirm dialog. The difference is what gets posted — a bill posts an expense with a category line; a repayment posts a transfer with no lines at all.
That distinction is load-bearing. A repayment booked to a category would inflate every month's spending by the size of your debts, and nothing on screen would look wrong. A test pins it.
Projects
A project is a client job with an agreed value, so the screen can answer was this worth doing rather than merely what did this cost. Profit is shown twice — cleared so far, and expected on completion.
There is no cached profit column. Profit is derived from the postings on every read, so a project cannot drift from the ledger.
Installments are the parts of a project: agree 900,000 in three, and each part becomes an expected income row. Because the allocation wizard already reserves against expected income, installments become obligation-aware for free.
The statement
One page per calendar month, computed live from the postings: opened with X, earned Y, spent Z by category, closed with W, what the envelopes did, and how it compares with last month.
- There is no stored statement table, so it cannot drift from the ledger.
- Closing is derived as
opening + movement, not queried separately — a page that says "opened with X, Y moved, closed with Z" then cannot print three numbers that fail to add up. - The residual is shown, not swallowed. Money leaves your spendable accounts for reasons that are not spending: a debt repayment, an opening balance, money into savings. Without its own line the month would appear to lose money nobody spent.
- The comparison is uncoloured on purpose. Spending less is not automatically good and earning less is not automatically a failure. Arrows say which way it went; the judgement is yours.