A local-first personal finance app. No bank linking, no scraping, no telemetry. Data sits in AsyncStorage, backups are JSON files the user owns, and the insights engine runs entirely on device.
Personal-finance apps either scrape your bank (and sell aggregated data) or give up on automation. The brief was to find a third path: manual entry that doesn’t feel manual, and insights that don’t require a server.
The app had to feel fast on a 3-year-old phone and work in airplane mode — because that’s where most people actually log expenses: on a train, in a shop, with two bars.
Local-first is an engineering constraint, not a marketing word. Every feature had to degrade gracefully when offline, every piece of state had to survive a backup round-trip, and every network call had to be explicit enough to list on one hand.
A pure function takes (accounts, txs, scheduledTxs, rates, settings, now) and returns a fully-shaped insights object: a 12-month trend per category, a 6-month monthly pace, a 3-month anomaly window. No debouncing, no background sync, no spinner — the math runs in single-digit milliseconds on a phone.
Multi-currency is honoured at the transaction timestamp: each tx exchanges through the rates cache as of when it was entered, so historical balances never silently drift.
Each scheduled template computes its own occurrences via a pure recurrence module (weekly by weekday, monthly by day-of-month, yearly). At boot, the store syncs the 90-day window, materialising only occurrences that aren’t already stamped with a matching {scheduledId, occurrenceAt} key.
A hard safety cap of 100 auto-created txs per run means that even a pathological state (clock skew, corrupt pattern) can’t pollute the ledger. Notifications follow the same law: 8 per template, 48 total.
runScheduledSync(now) {
window = [now - 90d, now]
for (s of scheduledTxs):
for (ts of occurrences(s, window)):
key = ${s.id}:${ts}
if key in existingMeta: skip
else: materialise tx
if created++ > 100: break // safety cap
syncScheduledNotifications({
horizon: 90d,
perTemplate: 8, total: 48,
trigger: "day - 1 @ 08:00 local",
});
}
Most finance apps feel like an accountant’s Excel rendered in neon. Môney takes the opposite bet: warm cream, brown ink, one gold accent — an interface that rewards quick glances and gets out of your way.
Both light and dark themes share the same ink-on-paper logic. Numbers use the primary family, labels use the secondary. No drop shadows on money values. No bright red for expenses — just a slightly warmer brown.
“Local-first isn’t a marketing word. It’s a discipline: every feature has to earn the right to make a network call, and most features never will.”