No description
Find a file
Kerry Hatcher 951f1fa61f
Some checks failed
sites / pages (push) Failing after 57s
chore: bump submodule pointers for the CLAUDE.md GitHub-link fix
Follow-up to c2f0a05: line 3 of every submodule's own CLAUDE.md still
pointed the superproject link at the stale GitHub mirror
(github.com/hatchertechnology/openbooks, frozen at b08f1f0 with a
.gitmodules pointing every submodule back at GitHub) rather than
code.kerryhatcher.com/hatchtech/openbooks. Caught in review after the
prior commit, missed because I fixed the docs-URL and gh-issue lines in
each file but not this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGisjtKSjUcVGw1jX1Hf8M
2026-08-07 13:39:39 -04:00
.agents/skills feat: just run to start postgres, api, and web together 2026-08-03 13:47:53 -04:00
.claude chore: enable the hyperskills plugin 2026-08-07 00:17:12 -04:00
.forgejo/workflows feat: build and deploy both sites to cloudflare pages 2026-08-07 11:46:16 -04:00
assets docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
deploy/thor-k3s fix(deploy): guard Namespace and PVCs against ArgoCD prune; fix stale CLAUDE.md docs pointers 2026-08-07 13:36:13 -04:00
docs/superpowers fix: stop the plan's nginx config downgrading https to http 2026-08-07 02:49:22 -04:00
openbooks-agent@90872e2be6 chore: bump submodule pointers for the CLAUDE.md GitHub-link fix 2026-08-07 13:39:39 -04:00
openbooks-api@dfea074e34 chore: bump submodule pointers for the CLAUDE.md GitHub-link fix 2026-08-07 13:39:39 -04:00
openbooks-cli@1afff12607 chore: bump submodule pointers for the CLAUDE.md GitHub-link fix 2026-08-07 13:39:39 -04:00
openbooks-desktop@ff3983b301 chore: bump submodule pointers for the CLAUDE.md GitHub-link fix 2026-08-07 13:39:39 -04:00
openbooks-docs@a0327abbdd fix(deploy): guard Namespace and PVCs against ArgoCD prune; fix stale CLAUDE.md docs pointers 2026-08-07 13:36:13 -04:00
openbooks-landing@b536b21107 chore: bump landing and docs submodules for custom-domain canonicals 2026-08-07 12:56:11 -04:00
openbooks-web@a03975e7ca chore: bump submodule pointers for the CLAUDE.md GitHub-link fix 2026-08-07 13:39:39 -04:00
.gitignore feat: build and deploy both sites to cloudflare pages 2026-08-07 11:46:16 -04:00
.gitmodules chore: move submodules to code.kerryhatcher.com/hatchtech 2026-08-07 02:15:46 -04:00
.mcp.json refactor: fixed, uncommon dev ports and a guard against drifting 2026-08-03 15:43:43 -04:00
.sops.yaml fix(deploy): real off-site backup via restic/B2, SOPS-encrypted credentials 2026-08-07 12:42:11 -04:00
CHANGELOG.md docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
CLAUDE.md fix(deploy): guard Namespace and PVCs against ArgoCD prune; fix stale CLAUDE.md docs pointers 2026-08-07 13:36:13 -04:00
CODE_OF_CONDUCT.md docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
CONTRIBUTING.md docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
justfile chore: add just user-add, users, and mint-token 2026-08-03 20:36:47 -04:00
LICENSE Initial commit 2026-08-03 13:10:43 -04:00
README.md docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
SECURITY.md docs: rebuild the README and add the missing project files 2026-08-04 12:25:21 -04:00
skills-lock.json feat: just run to start postgres, api, and web together 2026-08-03 13:47:53 -04:00

OpenBooks — double-entry bookkeeping for a family or a club

Docs deploy status Documentation MIT license Version 0.1.0-dev Status: early proof of concept

Rust: axum and sqlx Nuxt web UI Postgres ledger

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

The OpenBooks overview for a quarter: money in, money out, and what was left over; where the money came from and what it went to; a bar chart of the last twelve months; and a record form docked along the bottom

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

The treasurer's report for Q3 2026: what came in and went out, itemised by category, totalling to what was left over

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 languageopenbooks-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 clickjust seed posts a club's worth of deterministic activity across 2025 and the first half of 2026
  • One command to run all of itjust run sequences 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

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 devjust 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

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.