No description
  • JavaScript 50.6%
  • OCaml 20.6%
  • CSS 16.5%
  • HTML 9.3%
  • Nix 3%
Find a file
ant 3757916150 Add optional manual distribution when adding an expense
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.
2026-08-12 22:26:55 +02:00
bin Add Road trip Bretagne seed group with 23 expenses 2026-08-12 19:07:27 +02:00
lib Initial commit — Zencount self-hosted expense tracker 2026-08-12 15:32:00 +02:00
static Add optional manual distribution when adding an expense 2026-08-12 22:26:55 +02:00
tests Add optional manual distribution when adding an expense 2026-08-12 22:26:55 +02:00
.gitignore Fix modal centering; add Playwright visual tests 2026-08-12 19:38:15 +02:00
dune-project Initial commit — Zencount self-hosted expense tracker 2026-08-12 15:32:00 +02:00
flake.lock Initial commit — Zencount self-hosted expense tracker 2026-08-12 15:32:00 +02:00
flake.nix Fix modal centering; add Playwright visual tests 2026-08-12 19:38:15 +02:00
package-lock.json Fix modal centering; add Playwright visual tests 2026-08-12 19:38:15 +02:00
package.json Fix modal centering; add Playwright visual tests 2026-08-12 19:38:15 +02:00
playwright.config.js Fix modal centering; add Playwright visual tests 2026-08-12 19:38:15 +02:00
README.md Rename to ZenCount (uppercase C) 2026-08-12 21:55:43 +02:00
zencount.opam Initial commit — Zencount self-hosted expense tracker 2026-08-12 15:32:00 +02:00

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).