Projects / software / punchline
punchline
A consulting time card and invoicing app built for exactly one user. Clients, time, invoices and reconciliation, and nothing else.
Consulting time tracking is a solved problem with no good answer at my size. The tools that actually work are priced for agencies and carry a decade of features I will never open: approvals, team utilisation dashboards, resource planning, payroll integrations. The affordable ones are affordable because they are rigid, and the rigidity always shows up at the exact moment you need to do something slightly unusual with an invoice.
So I built the one I want. Four things and no fifth: client management, time management, invoice tracking, and reconciliation. It's finished, and it's what I use.
The part that was actually new
I'd wanted this for years and never built it, because the honest cost-benefit never worked. A few weekends to replace a subscription I mildly resent is a bad trade, especially since the thing I'd build would be worse than what I'd cancel.
That maths has moved. Scaffolding a typed React app, a Workers backend and a schema is no longer the expensive part, which means the calculation stops being "is this worth several weekends" and becomes "do I know exactly what I want." For this, I did — I'd been describing it to other people's software for a decade and having it say no.
Whether that's a good reason to build software or a very good way to acquire maintenance work, I now get to answer from the other side. The status on this page says maintained, which is the honest answer so far: it works, I use it, and it is mine to keep working.
The stack
React on the front, Cloudflare Workers on the back, D1 for storage. Static frontend, edge runtime, no server to keep alive and nothing to patch on a Sunday. For an app with one user and a few hundred records a year, that's about as much infrastructure as the problem deserves.
Why D1 and not KV
I started out planning to use Workers KV, because it's the obvious default on this platform and the data looked simple enough. It isn't, and the reasons are worth writing down.
The data is relational, and pretending otherwise costs more than the schema does. Clients have engagements, engagements have time entries, invoices have line items drawn from those entries, and payments settle against invoices. That's five tables and four foreign keys. It is not a shape you store as blobs and hope.
The queries are the application. What's unpaid and for how long. Hours by client by
month. Everything billable between two dates that isn't already on an invoice. How much
of a retainer is left. Every one of those is a WHERE, a GROUP BY and a SUM — one
line of SQL each, and in KV every one becomes a key layout I'd have to design, maintain,
and keep consistent by hand. That path ends at a worse database than the one I got for
free.
No transactions was the real disqualifier. Issuing an invoice writes the invoice, writes its line items, and marks a batch of time entries as billed. If that isn't atomic, the failure mode is billing a client twice for the same hour, or dropping hours entirely. For financial records, "usually consistent" is not a tradeoff, it's a defect.
And read-your-writes matters here. KV is eventually consistent — a write can take up to a minute to propagate. Log an hour, look at today's total, see the old number, and I'd never trust the tool again.
The case for KV is throughput at edge scale, which is a real and genuinely impressive benefit that this app was never going to collect. One user. A few thousand rows a year. SQLite is so far inside its comfort zone here that the interesting engineering was entirely elsewhere.
Next: drive it from a prompt
The core four are done and in daily use. Next on the list is a basic MCP server, so the whole thing is reachable from a Claude prompt — deliberately not a fifth feature, but a second interface to the four that already exist.
The argument for it is sitting in my own success criterion below: log an hour in under five seconds. No form achieves that. Opening a tab, finding the client, picking the engagement, typing a duration and a description, saving — that is thirty seconds of overhead on a two-minute task, and it's the action I take a dozen times a week. Whereas "log 90 minutes to Acme for the migration call" is one sentence, typed where I already am, in the tool I already have open.
That suggests a clean split rather than a bolt-on. Prompt for writing, screen for reading. Time entry, the highest-frequency and lowest-ceremony action, belongs in a sentence. Invoice states, reconciliation and what's owed belong on a screen, because those are things you need to see laid out, compared, and totalled.
It also costs almost nothing to build here. The backend is already a Worker with a typed data layer; an MCP server over the same handlers is a thin surface, not a parallel implementation.
What done meant
Log an hour in under five seconds. Generate an invoice without editing anything by hand. Tell at a glance what's owed and how long it's been owed.
That was the bar, and it's the bar the thing now clears. What's left is the discipline part: every week it suggests a fifth feature, and every week the answer is no.