Actual Budget and the Quiet Art of Local-First Sync
Most budgeting apps ask you to trust a server. Your transactions live in someone else’s database, your spending history is a row in a multi-tenant table, and the app on your phone is a thin window onto that remote truth. Actual Budget inverts the whole arrangement: the truth lives on your device, the sync server is a dumb relay, and your data stays yours even if the project disappears tomorrow. That single decision — local-first, not cloud-first — is what makes this repo worth opening, and it’s also what makes it harder to build than it looks.
On the surface, Actual is an envelope-budgeting app. You give every dollar a job, money moves between categories, and the app tracks where it all went. It’s MIT-licensed, written almost entirely in TypeScript, and ships as a self-hostable web app, a Docker image, and standalone desktop builds for Windows, Mac, and Linux. The monorepo splits cleanly into loot-core (the platform-agnostic engine), desktop-client (the UI), and desktop-electron (the app shell). You can run the whole thing on a $1.40/month PikaPod and never touch a vendor’s servers again.
But the interesting part isn’t the budgeting. It’s what happens when you open the same budget on your laptop and your phone, edit both while offline, and expect them to agree later without losing anything. That’s a distributed-systems problem wearing a personal-finance costume, and the answer lives in a small, almost academic corner of the codebase.
The clock that doesn’t trust your clock
Inside packages/crdt, the maintainers ship a Hybrid Logical Clock — they call it a HULC, a Hybrid Unique Logical Clock — implemented in a file that reads like a translation of the original University at Buffalo HLC paper into production TypeScript. Every change in Actual isn’t stored as “the value is now X.” It’s stored as a message: a tuple of dataset, row, column, value, and a timestamp. The timestamp is the clever bit. It’s a 46-character collatable string built from three parts — wall-clock milliseconds, a monotonic counter, and a node identifier — that looks like 2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF.
Why bother? Because device clocks lie. They drift, they jump backward when NTP corrects them, two phones can produce the “same” millisecond. The counter and node ID exist precisely to break those ties deterministically. When a device generates a change, it calls send() to mint a timestamp that’s guaranteed to be later than anything it’s seen. When it receives a change, recv() merges the remote clock into the local one, preserving causality and refusing anything outside a configured maximum drift. The result is that every change across every device has a total, agreed-upon order — without any central authority deciding what “now” means.
Merkle trees as a sync shortcut
The second trick is how Actual figures out what to sync. Naively, two devices comparing histories would have to exchange every message they’ve ever seen. Instead, the sync module in loot-core maintains a Merkle tree of message timestamps. To check whether two clients agree, they compare root hashes; if those differ, merkle.diff() walks down the tree to find the earliest point where histories diverge and syncs only from there. A candid comment in the sync loop admits the ergonomics — “This is a bit wonky, but we loop until we are in sync with the server” — but the underlying idea is the same one Git and Dynamo use: hashes let you detect disagreement cheaply and pinpoint it without shipping the whole dataset.
Put the two together and conflict resolution mostly disappears. Because every change is an independently-ordered, idempotent message, applying them is last-writer-wins by timestamp, duplicates are skipped, and any two devices that have seen the same set of messages end up in the same state. That’s a CRDT in the practical sense: the data structure converges no matter what order the updates arrive in.
Who it’s for, and what’s worth stealing
For users, the pitch is straightforward: a fast, private, genuinely free budgeting tool you can host yourself. For builders, the value is the reference implementation. Actual is one of the cleanest open examples of local-first sync you can read end to end — the timestamp code, the Merkle diffing, and the message-log design are all there, in TypeScript, with property-based tests (sync.property.test.ts) hammering the invariants. If you’ve read James Long’s “CRDTs for Mortals” and wanted to see the idea survive contact with a real, shipped product, this is that product.
There’s history worth knowing, too. Actual began as James Long’s commercial app before he open-sourced it and handed stewardship to a community that now maintains it under the actualbudget org — 5,000-plus commits, 50-plus releases, and translations crowdsourced through Weblate. It’s a rare case of a paid product becoming a community-run one without losing its technical spine.
The lesson it leaves behind is bigger than budgeting: if you store changes instead of state, give each change a clock that doesn’t trust the hardware, and let hashes tell you where you disagree, “sync” stops being a feature you bolt on and becomes a property the data already has. Actual is proof you can ship that to non-technical people who just want to know if they can afford groceries.
Actual Budget is MIT-licensed, created by James Long and maintained by the actualbudget community. Source: github.com/actualbudget/actual.