Context
Personal finance apps assume a bank feed. Libyan banks offer a PDF statement and nothing else, and the people who would use the app are rightly unwilling to upload their statements to someone’s server. The product only works if the parsing and the ledger both happen on the device.
Decisions
Everything financial stays local. The ledger is SQLite compiled to WebAssembly, persisted in the browser’s origin private file system, and the backend stores identity and preferences only. That rule is written into the database schema so it cannot be relaxed quietly.
Double entry, because it makes imports self-checking. Every account is an asset, liability, income, expense or equity account and every event is a balanced transaction, so savings, loans, debts and spending categories are one primitive. An imported statement is reconciled against its own running balance column, which means a misread column cannot pass silently.
Money as integer minor units, with the Libyan dinar at a thousand per unit. Floating point has no place in a ledger.
The parser, the ledger and the money math are pure packages that touch no host environment. Each one typechecks without the DOM library, so the rule is enforced rather than remembered. The web app and the command-line parser fill two seams, the database and the PDF engine, and a React Native app reuses everything else unchanged.
Consequences
Adding a bank is an adapter file with column matchers, checked against a real statement from the command line without opening the browser. The mobile app inherits the entire ledger for free. The price is that every framework convenience that reaches for the window object is off limits inside the packages.