Build Tallymeter

KINDAreplaces $10.38/mosaves $124.56/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A solo time tracker and invoicer whose hard problems are durations that stay correct across a clock change, a rounding rule chosen once and recorded on every invoice it produced, and a number sequence that survives concurrency, cancellations and the year somebody audits you.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with Express, server-rendered pages on the loopback addressStorageSQLite with amounts as integer minor units and durations as whole secondsNumberingA series table whose next number is claimed inside the issuing transactionRenderingPDF generated from the invoice snapshot, never from a live joinAccessScoped tokens for anything automated, with the actor recorded on every entry

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the tracker and the renderer.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Invoices are documents other people print, and a renderer that reflows differently between versions is a reprint that does not match.

    Get it Install a headless browser package and pin its version.

    Verify The same invoice renders to the same checksum twice

  3. have readyfree

    Why The number sequence, the required fields and the retention period all come from these, and getting them from a blog post rather than the tax authority is how an invoice ends up invalid.

    Get it Read the tax authority's own guidance on invoice contents and record keeping, and note the date. open ↗

    Verify The required fields list and the retention period are recorded in the README with their source

  4. have readyfree

    Why These are snapshotted onto every invoice, so having them wrong on day one means every invoice issued before you notice is wrong in a way you cannot edit.

    Get it Collect the exact wording your bank and tax authority expect.

    Verify A test invoice shows them exactly as they should appear

  5. decidefree

    Why Per-line and per-total rounding give different totals, and choosing after you have issued invoices means two eras of arithmetic in the same ledger.

    Get it Read the rule that applies to you, choose one, and record the choice and the source.

    Verify The configured rule matches the README and appears on every issued invoice

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- clients(id, name, address_lines, tax_number, country, created_at)
- projects(id, client_id, name, rate_minor_per_hour, currency, active) · the current rate, which is not what old invoices read from
- entries(id, project_id, description, started_at_utc, ended_at_utc, seconds, source, actor_token_id, invoice_id, frozen_at) · seconds as an integer, instants in UTC, and the actor recorded
- invoice_series(code, prefix, next_number, last_issued_at) · the number is claimed inside the issuing transaction and nowhere else
- invoices(id, series_code, number, issued_at, client_snapshot_json, currency, rounding_rule, tax_rate_ppm, subtotal_minor, tax_minor, total_minor, state, void_reason, voided_at, pdf_path, pdf_checksum)
- invoice_lines(invoice_id, ordinal, description, seconds, rate_minor_per_hour, amount_minor) · snapshotted values, never joined back to projects
- api_tokens(id, label, scope, created_at, last_used_at, revoked_at) · so an automated agent logging its own hours is attributable on the invoice
- An issued invoice references nothing that can change. Every figure printed on it is stored on the invoice or its lines.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/tally.dbClients, projects, entries and invoices. The only record of your billing history.
INVOICE_DIRrequired./invoicesRendered PDFs, named by series and number. Backed up with the database.
INVOICE_SERIESrequiredINV-2026The default series prefix. Additional series are rows, not configuration.
ROUNDING_RULErequiredper_totalRecorded on every invoice it produces. Changing it does not alter issued invoices.
TAX_RATE_PPMrequired200000Parts per million, snapshotted onto each invoice at issue.
RECORD_RETENTION_YEARSrequired6Deletion inside this window is refused by the code, not just discouraged.
BACKUP_DIRrequired./backupsNightly copies, restored into a scratch database on a schedule.
API_TOKEN_SALTsecretrequiredredactedHashes scoped tokens so the database never holds a usable one.

The build, in order

  1. One timer, and durations that are not opinions

    Time is recorded as UTC instants and whole seconds, exactly one timer runs, and a clock change does not alter a duration.

    1. A duration derived from wall-clock times is wrong twice a year.

      Files migrations/002_entries.sqltests/dst-duration.test.ts

    2. Two running timers is a billing dispute waiting to happen.

      Files src/timer/start.tstests/single-timer.test.ts

    3. Once billed, the entry is evidence rather than a note.

      Files src/entries/edit.ts

    done when · tick each as it passes
    watch out
    • Storing a duration as decimal hours loses information immediately, because three hours and twenty five minutes is not representable as a terminating decimal, and a week of those rounded figures produces an invoice total that disagrees with the entries it came from. Store whole seconds, compute decimal hours only when rendering a line, and never store the rendered figure.
  2. Choosing the rounding rule once

    The way money is rounded is a decision recorded on every invoice, not an accident of the order things were summed.

    1. Decimal money in the application layer is how pennies vanish.

      Files src/money/minor.tstests/money.test.ts

    2. Next year you will not remember which one you chose.

      Files migrations/003_invoices.sql

    3. If your fixture cannot tell them apart it is not testing anything.

      Files tests/rounding.test.ts

    done when · tick each as it passes
    watch out
    • Rounding each line to the penny and summing gives a different total from rounding the sum, and since both look right in isolation the disagreement only appears when a client's accounts payable system computes it the other way and short-pays you by two pence. Pick one rule, store it on the invoice, and write a test with a set of lines where the two approaches genuinely differ.
  3. A sequence with no holes and no collisions

    Numbers are allocated at issue, inside one transaction, and a cancelled invoice stays as a voided record.

    1. Select-then-update is the collision, drafts are the gap.

      Files src/invoices/issue.tstests/concurrent-issue.test.ts

    2. The record of a mistake is part of the record.

      Files src/invoices/void.ts

    3. An entry billed on one invoice must not appear on another.

      Files src/invoices/issue.tstests/no-double-bill.test.ts

    done when · tick each as it passes
    watch out
    • Allocating the number when a draft is created leaves a hole in the sequence every time you abandon one, and the hole is exactly the thing an inspector asks about, while allocating it with a select followed by an update lets two invoices issued in the same second take the same number. Claim inside the issuing transaction, and never allocate to anything that has not been issued.
  4. A PDF that still says the same thing next year

    Reprinting an old invoice reproduces the original exactly, byte for byte where possible.

    1. A join to a live table is a time bomb with a client's name on it.

      Files src/invoices/render.tstests/reprint-stability.test.ts

    2. So a changed reprint is loud rather than silent.

      Files src/invoices/render.ts

    3. The accountant's first two questions, answered on the page.

      Files src/invoices/template.html

    done when · tick each as it passes
    watch out
    • Rendering the PDF by joining back to clients and projects means a client who moved office or a project whose rate went up silently changes what last year's invoice says, and you will not find out until somebody compares your copy with theirs. Snapshot everything the document prints onto the invoice at issue, and render only from that.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more finance & accounting.