A double-entry accounting ledger backed by SQLite, with CLI, TUI, and HTTP server modes. Follows IFRS chart of accounts and enforces balanced transactions at both application and database level.
- Double-entry enforcement — Every transaction must balance (per-currency sum of debits and credits = 0), enforced by SQLite triggers and application logic
- IFRS Chart of Accounts — Predefined account categories: Assets (1xxx), Liabilities (2xxx), Equity (3xxx), Revenue (4xxx), Expenses (5xxx)
- Multi-currency — Amounts stored as integers in minor units (cents), per-currency balance within transactions
- System accounts — Internal accounts prefixed with
~(e.g.,~fees,~suspense,~float,~fx) auto-created on first run - Four interfaces — HTTP server, CLI, interactive TUI (Bubble Tea), and browser-based web terminal (Ghostty WASM)
- Immutable transactions — Once finalized, transaction entries cannot be modified (enforced by triggers)
# Build
make build
# Start the server
./miniledger serve
# In another terminal, create accounts and transactions
./miniledger account create --id 1010 --name "Cash" --code 1010
./miniledger account create --id 2020 --name "Customer Accounts" --code 2020
./miniledger transaction create \
--description "Customer deposit" \
--entry "1010:+50000:USD" \
--entry "2020:-50000:USD"
# View balance sheet
./miniledger balance
# Launch interactive TUI (starts embedded server automatically)
./miniledger tui
# Or run in the browser via Ghostty WASM terminal
./miniledger web
# Open http://localhost:8833cmd/ CLI commands (cobra)
internal/
ledger/ Pure domain types (Account, Transaction, Entry, Currency)
store/ SQLite persistence with migrations and triggers
server/ HTTP API (chi router)
client/ HTTP client for CLI/TUI to talk to server
tui/ Bubble Tea terminal UI
web/ Browser terminal (Ghostty WASM + WebSocket + PTY)
CLI/TUI → HTTP Client → HTTP Server (chi) → Store (SQLite)
↓
SQLite Triggers
(balance enforcement)
miniledger serve [--addr :8888] [--db ledger.db] Start HTTP server
miniledger tui [--server http://localhost:8888] Launch TUI
miniledger web [--port 8833] [--host localhost] Launch TUI in browser
miniledger account create --id --name --code [--currency USD]
miniledger account list [--category assets]
miniledger account get <id>
miniledger account balance <id>
miniledger transaction create --description "..." --entry "acct:amt:ccy" [--entry ...]
miniledger transaction list [--account <id>]
miniledger transaction get <id>
miniledger balance Balance sheet
miniledger balance trial Trial balance
Entries use the compact format account_id:amount:currency:
- Amount is in minor units (cents for USD)
- Positive = debit, negative = credit
- Example:
1010:+50000:USDdebits Cash $500.00
Run the full TUI in your browser using the Ghostty WASM terminal emulator:
./miniledger web # http://localhost:8833
./miniledger web --port 9000 # custom port
./miniledger web --host 0.0.0.0 # listen on all interfacesEach browser tab gets its own independent TUI session via a PTY subprocess connected over WebSocket.
The TUI has three main views (switch with Tab):
| View | Description |
|---|---|
| Accounts | List all accounts, press Enter to view details |
| Transactions | List all transactions, press Enter for details |
| Balance Sheet | Formatted balance sheet report |
| Key | Action |
|---|---|
Tab / Shift+Tab |
Switch between views |
Enter |
Select / drill into detail |
Esc |
Back to list |
n |
New account (wizard) |
j/k or Up/Down |
Navigate |
q / Ctrl+C |
Quit |
Press n in the Accounts view to launch the wizard:
- Select IFRS category (Assets, Liabilities, Equity, Revenue, Expenses)
- Enter account code (validated against category range)
- Enter account ID
- Enter account name
- Select currency
- Confirm and create
All endpoints under /api/v1:
| Method | Path | Description |
|---|---|---|
POST |
/accounts |
Create account |
GET |
/accounts |
List accounts |
GET |
/accounts/{id} |
Get account |
GET |
/accounts/{id}/balance |
Get balance |
GET |
/accounts/{id}/entries |
List entries |
POST |
/transactions |
Create transaction |
GET |
/transactions |
List transactions |
GET |
/transactions/{id} |
Get transaction |
GET |
/reports/balance-sheet |
Balance sheet |
GET |
/reports/trial-balance |
Trial balance |
GET |
/chart |
IFRS chart reference |
curl -X POST http://localhost:8888/api/v1/transactions \
-H "Content-Type: application/json" \
-d '{
"description": "Customer deposit",
"entries": [
{"account_id": "1010", "amount": 50000, "currency": "USD"},
{"account_id": "2020", "amount": -50000, "currency": "USD"}
]
}'| Code | Name | Category |
|---|---|---|
| 1010 | Cash and Cash Equivalents | Assets |
| 1020 | Accounts Receivable | Assets |
| 1030 | Inventory | Assets |
| 1040 | Prepaid Expenses | Assets |
| 1050 | Property, Plant & Equipment | Assets |
| 2010 | Accounts Payable | Liabilities |
| 2020 | Customer Accounts | Liabilities |
| 2030 | Accrued Expenses | Liabilities |
| 2040 | Loans Payable | Liabilities |
| 3010 | Retained Earnings | Equity |
| 3020 | Common Stock | Equity |
| 4010 | Service Revenue | Revenue |
| 4020 | Interest Income | Revenue |
| 5010 | Operating Expenses | Expenses |
| 5020 | Cost of Goods Sold | Expenses |
| 5030 | Salaries and Wages | Expenses |
| 5040 | Depreciation | Expenses |
| ID | Name | Purpose |
|---|---|---|
~fees |
Fee Income | Fee income from customer charges |
~suspense |
Suspense Account | Temporary holding for unclassified entries |
~float |
Float Account | Cash in transit |
~fx |
FX Conversion | Cross-currency transaction intermediary |
Each account has a designated currency. Entries must match the account's currency. For cross-currency transactions, use the ~fx account (accepts all currencies):
# Convert 100 EUR to 110 USD
curl -X POST http://localhost:8888/api/v1/transactions \
-d '{
"description": "FX: EUR to USD",
"entries": [
{"account_id": "1010", "amount": 11000, "currency": "USD"},
{"account_id": "~fx", "amount": -11000, "currency": "USD"},
{"account_id": "~fx", "amount": 10000, "currency": "EUR"},
{"account_id": "1011", "amount": -10000, "currency": "EUR"}
]
}'Transactions use a two-phase commit:
- Insert transaction row (
finalized=0) - Insert all entry rows
- Set
finalized=1— SQLite trigger fires, verifying per-currencySUM(amount) = 0 - If trigger rejects (unbalanced), entire SQL transaction rolls back
Additional triggers prevent:
- Adding/modifying/deleting entries on finalized transactions
- Entry currency mismatching account currency
make build # Build binary
make test # Run tests
make run-server # Build and start server
make run-tui # Build and start TUI
make run-web # Build and start web terminal
make clean # Remove binary and database