Replacing Clockify with 200 lines of Elixir and a SQLite file
A self-hosted Clockify replacement in Elixir and SQLite — and why the single-running-timer rule lives in the database, not the application.
I used Clockify for a couple of years and my complaints were all the same complaint: it is a multi-tenant SaaS product solving a problem I have alone.
I don’t need teams, roles, or approval workflows. I don’t need an account, or a mobile app, or for my time log to live on someone else’s infrastructure. I need to press a button when I start doing a thing, press it again when I stop, and later find out where the week went.
So I wrote Punch — Phoenix LiveView, SQLite, one Docker container on the homelab box.
One timer, enforced by the database
The central rule is that exactly one timer runs at a time. This sounds trivial and it is the only part of the app I’d call load-bearing.
The tempting implementation is a check in the application: before starting a timer, look for a running one and stop it. That works right up until two requests arrive close together — two browser tabs, a phone and a laptop, a double-tap on a slow connection. Then you get two open entries, and you won’t find out until the weekly total looks wrong days later. Time tracking data is append-mostly and rarely re-read, so corruption there is quiet and long-lived.
So the constraint lives in the schema, as a partial unique index over entries where the end time is null. The database permits at most one open entry. Application-level checks give a nice error message; the index is what makes the invariant true.
This is a habit worth generalizing. If a rule matters, and the database can express it, put it there. Application code enforces rules most of the time — under concurrency, after a deploy that briefly runs two versions, when a background job races a web request. Constraints enforce them always. The check in the app is a user experience; the constraint is the guarantee.
Switching tasks follows from this: punching in on a new task while one runs closes the old entry and opens the new one in a single transaction. Not two operations that usually happen together — one atomic change. You never lose the seconds between, and you can never end up with zero running timers because the second half failed.
Start on the laptop, stop on the phone
Every connected browser sees the same state instantly, via Phoenix PubSub. Punch in at the desk, walk away, punch out from your phone on the couch, and the desktop tab updates itself.
This is the feature that made me stop reaching for Clockify, and in LiveView it’s close to free. When an entry changes, broadcast on a topic; every mounted LiveView subscribed to that topic re-renders. There’s no client state to reconcile, no polling, no websocket protocol to design. The processes are already there and already connected.
The general version: the moment a tool exists on more than one device, state sync stops being optional. A time tracker where the phone and the laptop disagree is worse than no time tracker, because you stop trusting the numbers. Getting this right is most of the difference between a toy and something you actually use.
Fixing the punch-out you forgot
You will forget to stop a timer. Everyone does — you close the laptop, or the task ends in a meeting, and the next morning there’s a fourteen-hour entry.
So the whole week’s log is inline-editable: description, category, date, start, end, or duration, edited in place in the list. Changing the duration adjusts the end time; changing the end time adjusts the duration. No modal, no separate edit screen.
The design principle is that correcting data should be cheaper than entering it. If fixing a bad entry requires navigating somewhere else, you won’t do it, and a log with known-wrong entries in it is a log you’ve stopped believing.
Task descriptions autocomplete against past entries, and picking a suggestion punches in with the category it last ran under. This matters more than it sounds: free-text task names drift — “code review”, “Code Review”, “reviewing PRs” — and by the time you run a report they’re three different categories. Autocomplete makes the consistent choice the lazy one.
Where the time went
The reporting is deliberately thin. Weekly totals tick up live on the timer page while the clock runs. A monthly view gives total hours and a per-category breakdown with share bars. CSV export for anything else.
I’ve resisted adding more. The value of a personal time tracker is almost entirely in capture fidelity — if the entries are accurate and complete, a spreadsheet can answer any question you have. If they’re not, no amount of dashboard sophistication rescues them. Every hour I might spend on charts is better spent making it harder to record a wrong entry.
SQLite, and why it’s the right call
The entire database is one file.
For a single-user application this is not a compromise, it’s the correct architecture. No separate database container, no connection pooling to tune, no network hop. Backups are a file copy.
Specifically, the backup script on my homelab uses VACUUM INTO, which
writes a consistent snapshot of a live database without stopping the app — no partial-write risk, no need to quiesce
anything. It runs nightly via a launchd agent, verifies integrity on the resulting file, copies it
to the NAS, and prunes anything older than thirty days.
That’s the whole disaster recovery story, and I’ve actually tested the restore path, which puts it ahead of most backup setups I’ve encountered professionally.
The image builds for amd64 and arm64 and publishes to GHCR on tagged releases, so deploying is pulling a tag. It’s LAN-only — reachable at home or over the VPN, not exposed to the internet. An app with no authentication should not be on a public address, and giving it one would mean building accounts, sessions, and password resets: all the multi-tenant machinery I left Clockify to escape.
Should you build this?
Probably not, if you just need a timer. Clockify’s free tier is fine and this took real weekends.
Build it if you want a small complete application you fully own — one where you can hold the whole schema in your head, where the invariants are yours, and where nobody can deprecate a feature or move it behind a paid tier. Mine has run for months without attention, which is the highest compliment I can pay software I wrote.