- Just 100%
|
Some checks failed
sites / pages (push) Failing after 57s
Follow-up to |
||
|---|---|---|
| .agents/skills | ||
| .claude | ||
| .forgejo/workflows | ||
| assets | ||
| deploy/thor-k3s | ||
| docs/superpowers | ||
| openbooks-agent@90872e2be6 | ||
| openbooks-api@dfea074e34 | ||
| openbooks-cli@1afff12607 | ||
| openbooks-desktop@ff3983b301 | ||
| openbooks-docs@a0327abbdd | ||
| openbooks-landing@b536b21107 | ||
| openbooks-web@a03975e7ca | ||
| .gitignore | ||
| .gitmodules | ||
| .mcp.json | ||
| .sops.yaml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CODE_OF_CONDUCT.md | ||
| CONTRIBUTING.md | ||
| justfile | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
| skills-lock.json | ||
OpenBooks is bookkeeping for a family or a club: track a bank account, cash, and expenses, then print the statements for the monthly, quarterly, or annual meeting. Underneath it is a proper double-entry ledger — but nobody using it ever sees a debit or a credit, just money in, money out, and transfers.
Warning
Early proof of concept. It has authentication now, but no RBAC, no audit trail, and no backup story. Please don't keep the only copy of your club's books in it. See SECURITY.md.
🎬 See it
Money in, money out, what's left over — for a month, a quarter, or a year. Where money sits stays pinned to the sidebar, and the record form is docked along the bottom of every page: a date, an amount, an account, a category. The two legs get composed for you.
📊 The report you read out at the meeting
Pick the period, hit Print, and hand it round. No line on it says "debit".
⌨️ The same books in a terminal
just cli gives you a TUI over the same API — four tabs, money on hand always in the
header:
┌ OpenBooks ───────────────────────────────────────┐┌──────────────────────────┐
│ Dashboard │ History │ Reports │ Record ││ on hand $7,577.50│
└──────────────────────────────────────────────────┘└──────────────────────────┘
┌ Every account ───────────────────────────────────────────────────────────────┐
│ACCOUNT BALANCE│
│Cash Box $766.00│
│Checking Account $6,811.50│
│Donations $3,250.00│
│Dues $10,120.00│
│Field Trips $1,260.00│
│Food $1,888.00│
│Fundraising $2,690.00│
│Rent $5,875.00│
│Unpaid Bills $160.00│
└──────────────────────────────────────────────────────────────────────────────┘
[Tab]view [j/k]move [r]efresh [?]help [q]uit
Give it a subcommand instead and it turns into a quiet, scriptable CLI.
✨ Features
- 📖 A real double-entry ledger — signed integer cents, and a transaction is balanced only when its entries sum to zero
- 🔒 Enforced by Postgres, not by hope — that invariant is a deferred constraint trigger in the database, so no API bug can write crooked books
- 🙈 No accounting vocabulary on screen — money in, money out, transfers; the client composes both legs
- 🧾 Statements for a meeting — income statement, balance sheet, and account balances, over any period, with the signs already flipped so every line reads positive
- 🖥️ Five ways in — a web UI, a native desktop app, a terminal TUI, a scriptable CLI, and an agent plugin, all against one API
- 🤖 Bookkeeping in plain language — openbooks-agent drives the API's MCP endpoint, so any coding agent can be the treasurer
- 🔑 Passwords or passkeys — session cookie for the web app, bearer token for everything else, WebAuthn if you'd rather not type a password
- 🌱 A demo you can actually click —
just seedposts a club's worth of deterministic activity across 2025 and the first half of 2026 - ⚡ One command to run all of it —
just runsequences Postgres, migrations, the API, and the web dev server
🚀 Quick Start
git clone --recurse-submodules https://github.com/hatchertechnology/openbooks
cd openbooks
just run # postgres + api + web, in the background
just user-add you@example.com # prints a generated password, once
just seed # a club's worth of sample data
Open http://localhost:38080 and sign in. You need just,
Docker, Rust, and Node 20+.
Contents
- Why
- Installation
- Usage
- Testing
- The API
- How the books stay straight
- Layout
- Contributing
- Support
- License
- Acknowledgements
🤔 Why
Someone gets volunteered as treasurer for a scout troop, a booster club, a supper club, or a household. They have a bank account, a cash box, and a meeting once a month where somebody asks what we spent on the field trip.
The tools available are all wrong for that job. A spreadsheet drifts — the totals stop adding up and nobody notices for a quarter. Real accounting software wants a chart of accounts, journal entries, and a working knowledge of which side of the ledger a debit lives on. Personal finance apps track your own money, not an organization's, and can't print a statement for a meeting.
OpenBooks keeps the part that actually matters — a ledger that cannot silently go wrong — and hides the part that puts people off. A treasurer records an amount, an account, and a category. The double entry happens underneath, and the database refuses to store it if it doesn't balance.
📦 Installation
Everything goes through the justfile at the repo root. Don't hand-roll
docker compose, cargo run, or npm run dev — just run waits for Postgres to be
healthy before running migrations, installs web dependencies on first run, and tracks pids
so things can be stopped cleanly.
Prerequisites
just |
every task goes through the justfile |
| Docker | Postgres runs in a container |
| Rust (stable) | the API, the desktop app, the CLI |
| Node 20+ | the web UI |
| Deno | only for the documentation site |
jq |
only for just smoke |
Ports
| http://localhost:38080 | web UI |
| http://localhost:38081 | API |
localhost:38083 |
Postgres |
Already cloned without the submodules?
git submodule update --init --recursive
Running it without just
cd openbooks-api && docker compose up -d && cargo run # migrations run on startup
cd openbooks-web && npm install && npm run dev
You lose the health-check sequencing and the pid tracking, so start them in that order.
🛠️ Usage
just run |
start postgres, api, and web in the background |
just desktop |
run the native desktop app, starting postgres and the api first if they're down |
just cli |
the terminal UI; arguments pass straight through |
just docs |
serve the documentation site |
just stop |
stop all three |
just restart |
stop, then start; recorded data survives |
just status |
what's up right now |
just logs |
follow the api and web logs |
just seed |
load sample data, 2025 through mid-2026 |
just reset |
rebuild from scratch, come back up with sample data |
just reset-clean |
rebuild from scratch, stay down, empty database |
just smoke |
end-to-end ledger and report check (needs jq) |
just test |
the API's test suite, against a running postgres |
just user-add <email> |
create a user, printing a generated password once |
just users |
list who can sign in |
just mint-token <email> |
print a bearer token for scripts and clients |
Both resets drop the database, node_modules, and target, and prompt first; -y skips
the prompt. Prefer just restart when you only need fresh processes — a reset costs about
a minute.
The servers run in the background, so nothing prints to your terminal. Output goes to
.dev/api.log and .dev/web.log. Check just status before assuming something is down.
From a script
The CLI speaks JSON, so a meeting report is one command:
just cli income --from 2026-01-01 --to 2026-03-31 --json
just cli balances --json
just cli balance-sheet --as-of 2026-06-30
just cli record in --amount 45.00 --into "Cash Box" --from "Dues" --description "March dues"
Amounts are parsed to integer cents with string arithmetic, never a float, and report totals are printed exactly as the API returned them.
Sample data
just seed posts about 83 transactions across 2025 and the first half of 2026 — monthly
dues and hall rent, bake sales and donations, field trips, insurance, and an invoice
bought on credit and partly paid off, so the balance sheet has something under "what we
owe". Every month and quarter has activity, and the numbers are deterministic, so reports
are stable to compare between runs. Running it again replaces what it posted before rather
than doubling it.
Testing
just test # the API's unit and integration test suite, against a running postgres
just smoke # end-to-end: posts real transactions and asserts the reported totals
The smoke test is the one that matters most. It checks Q1 totals, asserts the balance
sheet actually balances, and confirms the API rejects unbalanced and single-legged posts.
Run both before opening a pull request, and run just smoke any time you touch the ledger
or the reports.
🔌 The API
Full reference: the endpoints page.
Authentication is required on everything except /health — a session cookie for the web
app, a bearer token for every other client.
GET /accounts |
the chart of accounts |
GET/POST /transactions |
list, and post a balanced transaction |
DELETE /transactions/{id} |
remove one |
GET /reports/balances |
balance of every account |
GET /reports/income-statement |
money in, money out, net for a period |
GET /reports/balance-sheet |
what we hold and owe, as of a date |
GET/PUT /settings |
organization settings |
POST /auth/login, /auth/logout |
password sessions |
/auth/webauthn/* |
passkey registration and sign-in |
POST /mcp |
the same reports and posting, as MCP tools |
/mcp exposes record_transaction, list_transactions, list_accounts,
account_balances, income_statement, and balance_sheet. Those tools call the HTTP
handlers' own functions rather than reimplementing the queries, so the two interfaces
cannot drift apart.
🧮 How the books stay straight
Entries carry a signed amount in integer cents — debit positive, credit negative — and a transaction is balanced when its entries sum to zero. There are no floats anywhere in the money path, in any client.
That invariant is a deferred constraint trigger in Postgres, not a check in application code. The API validates too, but only so it can return a friendly 400; the database is what actually guarantees the books stay straight regardless of an API bug.
Reports flip the sign on income, liability, and equity accounts so every line on a
statement reads as a positive number. AccountType::sign() is the single place that
decides this.
just smoke asserts real numbers — Q1 totals, a balancing balance sheet — and rejects
unbalanced and single-legged posts. Run it after touching the ledger or the reports.
🗂️ Layout
Six submodules, each with its own README:
| openbooks-api | Rust (axum + sqlx), Postgres — the ledger, the reports, and the MCP endpoint |
| openbooks-web | Nuxt UI |
| openbooks-desktop | Native app (Rust, ply-engine); caches locally and goes read-only when the API is unreachable |
| openbooks-cli | Terminal UI (ratatui), plus a text/JSON mode for scripts |
| openbooks-agent | Cross-harness agent plugin driving the API's MCP endpoint |
| openbooks-docs | The documentation site, published here (Starlight on Astro, run with Deno) |
🤝 Contributing
Contributions are welcome. CONTRIBUTING.md has the dev setup, the test commands, and the two rules that are part of the work rather than follow-ups: a behavior change updates the matching documentation page in the same commit, and a new feature extends the sample data in the same commit.
Conventional commits, signed. Commit inside the submodule first, then bump the pointer here. By taking part you agree to the Code of Conduct.
Found a security problem? SECURITY.md — please report it privately.
💬 Support
- Questions and ideas — GitHub Discussions
- Bugs and feature requests — open an issue, or use the tracker on the submodule the problem lives in
- Documentation gaps — the docs tracker,
labelled
documentation - Security — privately, via SECURITY.md
Please include what you ran, what you expected, and what .dev/api.log said.
📄 License
MIT.
🙏 Acknowledgements
Standing on: axum and sqlx for the API, Nuxt for the web UI, ratatui for the terminal client, Starlight on Astro for the documentation, just for holding the whole dev environment together, and Postgres for refusing to store books that don't balance.
The plain-language framing owes a debt to every volunteer treasurer who has been handed a shoebox of receipts and a spreadsheet that no longer adds up.