No description
- JavaScript 50.6%
- OCaml 20.6%
- CSS 16.5%
- HTML 9.3%
- Nix 3%
Toggle '+ Custom amounts' in the split section reveals per-person amount inputs pre-filled with equal shares. Live remainder indicator turns green when amounts sum to the total. Stored as a 'splits' map in the expense event; existing equal-split expenses are unaffected. |
||
|---|---|---|
| bin | ||
| lib | ||
| static | ||
| tests | ||
| .gitignore | ||
| dune-project | ||
| flake.lock | ||
| flake.nix | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.js | ||
| README.md | ||
| zencount.opam | ||
ZenCount
Self-hosted group expense tracker. No accounts, no app — just share a link.
Built as a Tricount replacement: one OCaml binary, plain HTML/CSS/JS, expenses stored as JSON files on disk.
Features
- Create a group, share the URL with everyone
- Add / edit / delete expenses with optional notes and receipt photos
- Automatic debt simplification (minimises number of transfers)
- Click a "who owes what" row to pre-fill a reimbursement expense
- Event-sourced journal — every action is recorded, the current state is computed by replaying it (like git commits)
- Group picture banner
- QR code share + copy-link button
- Export / import group as JSON
- French / English UI with locale-formatted dates
- Recent groups remembered in the browser
Run
nix run github:ant/zencount
Starts on http://localhost:3000. Data is written to ./data/.
Deploy (NixOS)
# flake.nix inputs
inputs.zencount.url = "github:ant/zencount";
# nixosConfigurations
{ inputs, ... }: {
imports = [ inputs.zencount.nixosModules.default ];
services.zencount = {
enable = true;
port = 3000; # default
};
}
The service runs as a dynamic user. Group data is stored under /var/lib/zencount/.
Dev
nix develop # enter shell with ocaml, dune, dream
dune build # compile
dune exec zencount # run with ./data and ./static
# Or, hot-reload static files without rebuilding:
DEV=1 STATIC_DIR=./static DATA_DIR=./data ./_build/default/bin/main.exe
DEV=1 seeds two example groups at startup.
Data format
Each group lives at $DATA_DIR/groups/<id>.json:
{
"id": "xK7mN2p3Qr",
"name": "Weekend trip",
"state_version": 2,
"journal": [
{ "id": "…", "happened_at": "2026-08-10T14:00:00.000Z", "action": "member_added",
"member_id": "m1", "member_name": "Alice" },
{ "id": "…", "happened_at": "2026-08-11T19:30:00.000Z", "action": "expense_added",
"expense_id": "e1", "description": "Dinner", "amount_cents": 6000,
"paid_by": "m1", "split_among": ["m1", "m2"], "date": "2026-08-11" }
]
}
happened_at is the immutable system timestamp. date is the user-entered expense date. The current list of members and expenses is derived by replaying journal events — member_added, expense_added, expense_edited, expense_deleted.
Amounts are always stored in cents (integers).