Build Everhour
YESreplaces $10/mosaves $120/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/ledger.db | Entries, rates, invoices and credit notes. The irreplaceable part. |
DOCUMENT_DIR | required | ./data/documents | Readable and structured invoices, credit notes and receipts. |
INVOICE_SYNTAX | required | ubl | The structured syntax you emit. Recorded on every invoice row as well. |
INVOICE_NUMBER_PREFIX | required | 2026- | Prefix only. The sequence itself is allocated at issue and never reused. |
HEARTBEAT_SECONDS | required | 15 | Bounds how much recorded time a crash can cost. |
BASE_CURRENCY | required | GBP | Totals are presented in this. Each project keeps its own currency. |
RETENTION_YEARS | required | 6 | Check your own jurisdiction's published requirement and record the source beside this. |
BACKUP_DIR | required | ./data/backups | Dated copies. The restore test reads from here. |
The build, in order
A timer that survives a closed laptop
Elapsed time is measured, and a timer running through a suspend does not bill the gap.
Wall clock subtraction bills the hours the machine spent asleep.
Files
src/timer/elapsed.tstests/suspend.test.tsIt bounds the loss from a crash to something you will not notice.
Files
src/timer/heartbeat.tsOnly the person knows whether they were working.
Files
src/timer/recover.tssrc/views/recover.tsx
done when · tick each as it passeswatch 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.
Rates that belong to a period
Raising a rate changes nothing about work that has already happened.
The rate is a property of the week, not of the project.
Files
src/rates.tstests/historic.test.tsSomeone will work across a rate change on the first of the month.
Files
src/rates.tsREADME.mdRounding at entry time rounds the same minute twice.
Files
src/invoice/round.ts
done when · tick each as it passeswatch 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.
Invoices that stop being editable
Issuing is irreversible, the sequence has no gaps, and corrections are credit notes.
A draft that is never issued should leave no trace in the sequence.
Files
src/invoice/issue.tstests/sequence.test.tsInterface-level protection is protection until the next feature.
Files
migrations/003_lock.sqlBoth leave the trail somebody will ask to see.
Files
src/invoice/void.tssrc/creditnote.ts
done when · tick each as it passeswatch 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.
An invoice another system can read
Every issued invoice produces a structured document as well as a readable one.
A structured document produced only on request is produced never.
Files
src/invoice/structured.tstests/structured.test.tsAn invalid file that exists is worse than no file.
Files
src/invoice/validate.tsWhatever reads it downstream will break silently otherwise.
Files
src/export/ledger.tstests/columns.test.ts
done when · tick each as it passeswatch 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.
Handing it over without retypingproduct builder
The next system reads your files directly, and a restore reproduces every total.
So the handover is one thing rather than six attachments.
Files
src/export/bundle.tsIt is the answer to 'did you send me February'.
Files
src/export/log.tsA backup nobody has restored is a hope with a filename.
Files
tests/restore.test.tsscripts/backup.sh
done when · tick each as it passeswatch out- The revenue guidance on digital record keeping states that it 'does not consider the use of cut and paste or copy and paste to select and move information' to be a digital link between software, and attaches a daily penalty to failing to maintain one; check the current figure in that notice rather than trusting a number from anywhere else. An export somebody has to retype into a return is therefore not merely tedious, it is the exact point where your figures and the ones filed stop matching, and nothing in either system will notice.
That is the whole plan for Everhour. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Bank feeds and payment rails.
- Automatic tax determination.
- Multiple users with roles and approvals.
- Project tool integrations.
- bank, payment, tax, and reconciliation integrations
- live bank feeds and payment rails
- tax and accounting rule updates
- audited security and recovery
- multi-user controls and accountant workflows
- Recurring invoices, which are worth doing only once the sequence and immutability rules are proven
- A second structured syntax behind the same serialiser interface, which is how you find out the abstraction was real
- A reminder for unpaid invoices built from the credit note and payment state rather than from a spreadsheet
Need the files? The project pack on the verdict page hands your agent the whole brief · more finance & accounting.