All work

Fin-trak

A local-first ledger that reads Libyan bank statements on the device.

Role
Solo
Status
Work in progress
When
2026
Stack
  • Vue 3
  • TypeScript
  • Pinia
  • SQLite-WASM
  • pdf.js
  • React Native
  • NestJS
  • PostgreSQL
Links
fintrak.ly

Libyan banks expose no public API, so Fin-trak parses their PDF statements on the device and builds a double-entry ledger that never leaves it.

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.