Build Everhour

YESreplaces $10/mosaves $120/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 billing ledger whose hard problems are measuring elapsed time on a machine that sleeps, pricing work at the rate that was in force when it happened, and producing an invoice a receiving system can process rather than a picture of one.

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

RuntimeNode with TypeScript and SQLite, single user, bound to localhostTimingElapsed measured from a monotonic source, with wall clock start and stop stored beside itMoneyInteger minor units with a currency, and rates that are valid over a periodInvoicesIssued invoices are immutable and every later change is a credit noteExportA structured invoice document beside the readable one, and a stable column contract for the ledger

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 ledger, the timer and the export.

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

    Verify node --version prints v22 or higher

  2. decidefree

    Why The semantic standard permits more than one, and the receiving side usually accepts one of them, so picking after you have written the serialiser means writing it twice.

    Get it Ask whoever receives your invoices which format they accept, read the current specification for it, and write the choice and the date into the repository.

    Verify A sample invoice in that syntax passes the standard's validation artefacts

  3. installfree

    Why Emitting something that looks right and is rejected on receipt is the failure this whole phase exists to prevent.

    Get it Use the validation artefacts published alongside the standard for your chosen syntax.

    Verify A deliberately broken invoice fails and a good one passes

  4. have readyfree

    Why Rate periods, rounding and the sequence rules only bite against data that already has a history, and a clean fixture hides all three.

    Get it Take a month of your own tracked time and an invoice you have already issued.

    Verify The ledger reproduces that invoice's total exactly

  5. decidefree

    Why The retention default should be a figure you looked up rather than one that felt sensible, because it is the one operational setting with an outside authority behind it.

    Get it Read your own jurisdiction's published record-keeping requirement and note the figure, the source and the date.

    Verify The configuration records the period, the source and the date checked

Data model

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

- clients(id, name, currency, address_json, tax_id) and projects(id, client_id, name, budget_kind, budget_minor, budget_hours, rounding_rule, closed_at)
- rates(id, project_id, person, rate_minor, currency, valid_from, valid_to) · periods, so last quarter stays priced at last quarter's rate
- entries(id, project_id, task, started_at_utc, stopped_at_utc, elapsed_ms, source, note, locked_by_invoice_id) · elapsed is measured rather than derived from the two timestamps
- timer_state(id, project_id, started_wall, started_monotonic, heartbeat_at) · written on a heartbeat, so a crash costs seconds instead of an afternoon
- invoices(id, client_id, number, issued_at, currency, subtotal_minor, tax_minor, total_minor, state, readable_path, structured_path, rounding_rule)
- invoice_lines(invoice_id, ordinal, description, quantity_milli, unit_price_minor, amount_minor, entry_ids_json) · quantities in thousandths so a rounding rule has something to round
- credit_notes(id, invoice_id, number, issued_at, amount_minor, reason) and expenses(id, project_id, at, amount_minor, currency, category, receipt_path, billable)
- Once an invoice is issued its lines and the entries behind them are locked. Everything after that is a credit note, because the alternative is two parties holding different versions of the same document number.

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/ledger.dbEntries, rates, invoices and credit notes. The irreplaceable part.
DOCUMENT_DIRrequired./data/documentsReadable and structured invoices, credit notes and receipts.
INVOICE_SYNTAXrequiredublThe structured syntax you emit. Recorded on every invoice row as well.
INVOICE_NUMBER_PREFIXrequired2026-Prefix only. The sequence itself is allocated at issue and never reused.
HEARTBEAT_SECONDSrequired15Bounds how much recorded time a crash can cost.
BASE_CURRENCYrequiredGBPTotals are presented in this. Each project keeps its own currency.
RETENTION_YEARSrequired6Check your own jurisdiction's published requirement and record the source beside this.
BACKUP_DIRrequired./data/backupsDated copies. The restore test reads from here.

The build, in order

  1. A timer that survives a closed laptop

    Elapsed time is measured, and a timer running through a suspend does not bill the gap.

    1. Wall clock subtraction bills the hours the machine spent asleep.

      Files src/timer/elapsed.tstests/suspend.test.ts

    2. It bounds the loss from a crash to something you will not notice.

      Files src/timer/heartbeat.ts

    3. Only the person knows whether they were working.

      Files src/timer/recover.tssrc/views/recover.tsx

    done when · tick each as it passes
    watch out
    • Computing elapsed time by subtracting the start timestamp from the stop timestamp hands you an entire afternoon of billable time the first time the machine sleeps with a timer running, and quietly loses or gains an hour whenever the clock steps, and both errors travel straight onto an invoice with nothing to flag them. Measure from a monotonic source, heartbeat while running, and on restart show the gap and ask rather than assuming either answer.
  2. Rates that belong to a period

    Raising a rate changes nothing about work that has already happened.

    1. The rate is a property of the week, not of the project.

      Files src/rates.tstests/historic.test.ts

    2. Someone will work across a rate change on the first of the month.

      Files src/rates.tsREADME.md

    3. Rounding at entry time rounds the same minute twice.

      Files src/invoice/round.ts

    done when · tick each as it passes
    watch out
    • Storing one current rate per project and raising it reprices the whole of last quarter the next time anybody opens a report, so a client who has already paid appears to owe more and your own totals stop agreeing with your bank statement. Give rates a validity period, price each entry from the rate in force when the work happened, and write a test that raises a rate and asserts every historic total is unchanged to the penny.
  3. Invoices that stop being editable

    Issuing is irreversible, the sequence has no gaps, and corrections are credit notes.

    1. A draft that is never issued should leave no trace in the sequence.

      Files src/invoice/issue.tstests/sequence.test.ts

    2. Interface-level protection is protection until the next feature.

      Files migrations/003_lock.sql

    3. Both leave the trail somebody will ask to see.

      Files src/invoice/void.tssrc/creditnote.ts

    done when · tick each as it passes
    watch out
    • Allocating the invoice number when the draft is created and then deleting the draft leaves a gap in the sequence, which is the first thing anybody reviewing your books asks about, and reusing the number instead produces two documents claiming the same identifier at two different clients. Allocate at issue, make issue irreversible, and void by keeping both the number and the document rather than by removing them.
  4. An invoice another system can read

    Every issued invoice produces a structured document as well as a readable one.

    1. A structured document produced only on request is produced never.

      Files src/invoice/structured.tstests/structured.test.ts

    2. An invalid file that exists is worse than no file.

      Files src/invoice/validate.ts

    3. Whatever reads it downstream will break silently otherwise.

      Files src/export/ledger.tstests/columns.test.ts

    done when · tick each as it passes
    watch out
    • A document emailed to a customer as a picture is not an electronic invoice under the rules now arriving: the European definition requires an invoice 'issued, transmitted and received in a structured electronic format which allows for its automatic and electronic processing', German law has required businesses to be able to receive such invoices since 1 January 2025 with issuing phased in from 1 January 2027, and the European package makes structured invoicing the default for covered intra-Union transactions from 1 July 2030. Checked on 15 September 2026. Emit the structured document from the first release; retrofitting it into a model that only ever produced a picture is the expensive version.
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.