← All workCase study · Local-first finance PWA

MiaFinanza

My personal finance app, for my Mac and my iPhone. It works offline and keeps the data in my own Google Drive. I decided what to build and approved each step. Claude Code agents drew the screens and wrote the code, and review agents checked every change before hooks let it be committed.

Role
Product owner; Claude Code agents wrote the code and the design
Timeline
Oct – Nov 2026
Team
Just me and a team of agents
Live
finance-app-indol-chi.vercel.app

Problem

I tracked my money in Cashew, a local-first app on the phone. I wanted the same accounts on the Mac and the iPhone, usable offline, without a paid server and without a database of mine holding the data.

Other people may use it one day, so it also needed a real Google sign-in and a session that lasts. The storage layer had to be ready to move to an API later without a rewrite.

Architecture

The app is a PWA. It keeps everything in IndexedDB and works with no network. After sign-in, each device writes a gzip snapshot of its data to the appDataFolder of Google Drive, a hidden folder only this app can read, and keeps 30 daily backups there. On sync, a device reads the other devices’ snapshots and merges them record by record.

The only server code is four Vercel Functions for the Google sign-in. They run the OAuth flow with PKCE, keep the refresh token in an encrypted HttpOnly cookie and give the app a short-lived access token. The old Cashew backup is a SQLite file, and the browser reads it with sql.js. Without an account, the app runs as a guest: the data stays in the browser tab and is gone when the tab closes.

The PWA keeps the data in IndexedDB and syncs one snapshot per device with the Google Drive appDataFolder. Four Vercel functions handle only the Google sign-in and hand the PWA short-lived tokens. Cashew backups are imported in the browser.MiaFinanza PWAReact 19 · Panda CSSIndexedDB · sync engineworks offlineAuth BFF4 Vercel FunctionsOAuth PKCE · CSRFencrypted cookieGoogle OAuthsign-in · tokensscope drive.appdatacookieshort tokenPKCErefreshCashew backupthe old app's .sql fileread with sql.jsGoogle DriveappDataFoldera snapshot per device30 daily backupsVercelhosts the PWAand the functionsimportsnapshotsbuilt by me, with agentsowned by others
The PWA and the four auth functions are mine, and agents wrote their code. Google owns the sign-in and the Drive folder where the data lives. Vercel hosts both parts, and the functions never see the financial data.

How it was built

I started from the stack and the agent setup I use on Kiwici and GUT Music. The doctrine, the rules the agents follow, lives in the repo: an AGENTS.md of 225 lines, plus six rule files of 841 lines in all, each scoped to one part of the code. The gates are 16 shell hooks, and they have tests of their own.

An agent draws the screens on a design canvas. Each roadmap step gets a plan with a doctrine section, and an auditor checks the plan before any code. Agents write the code with the doctrine and lint applied on every edit, a reviewer checks each hunk, and hooks block a commit without approvals or a version bump. A push to main is the release. I decide and approve at each step.Medirection · answers to the plan questions · approval of the boards, the plans and the commitsDesign canvasan agent drawsthe boardsRoadmapphases D, 0 to 8a plan per stepPlandoctrine sectiondated decisionsPlan auditfe-spec-auditorno code before a passbackCodedoctrine and linton every editHunk reviewcode-reviewerfe-spec-auditorCommit gatesapproved hunks onlyversion bumpPush = releaseVercel builds mainbackmeagents and their hooksa gate sends the work back
I set the direction and approve. Agents draw, plan, write and review, and hooks stop each step until the one before it has passed.

The design canvas

An agent drew the screens as boards on a design canvas: Home, Movements, Accounts and Summary, on desktop and mobile, plus the dialogs for a new movement and a transfer. I asked for changes until I approved the design. The agents then turned the boards into code: their colors and sizes became theme tokens, and the tokens became component recipes. The code names its source board in 98 places. Below, three of the boards sit next to the app as it shipped; both sides show made-up data.

DesignHome, desktop: the design board
ShippedHome, desktop: the shipped app
Home, desktop
DesignMovements, mobile: the design board
ShippedMovements, mobile: the shipped app
Movements, mobile
DesignNew movement, mobile: the design board
ShippedNew movement, mobile: the shipped app
New movement, mobile

Plans written against the doctrine

The roadmap split the work into phases, D for the screens the canvas lacked (sign-in, settings) and then 0 to 8, with the riskiest part first. On day one, before any data layer, a spike confirmed that Google sign-in from the app installed on the iPhone home screen comes back to the app and not to Safari.

Each step then got its own plan. A plan lists the files it will touch, the rules that apply to them, the open questions and what the gates will cover. The agents asked me 187 questions over the week, and my answers went into the plans as dated decisions. An auditor agent checks the plan against the rules, and a hook keeps the work in plan mode until the plan passes.

Review and commit gates

While an agent writes, hooks put the relevant rules in front of it and lint every file it edits. Then two review agents read the change, one for the code and one against the rules. Either can send it back. A commit goes through only when both have approved every hunk in it, and another hook refuses a commit that changes the shipped code without a new version number. Every push to main is a release on Vercel.

Scope

Mine

  • The product: what to build, in what order, and the answers to the agents’ questions
  • The architecture calls: local-first, the data in Drive, no paid server, Vercel for hosting
  • Approval of the design, of each plan and of each commit
  • The spike on my own iPhone

The agents

  • The design canvas and its boards
  • The roadmap and the plans
  • All the code: the PWA, the four auth functions, the sync engine, the Cashew import
  • 2,066 unit tests and the end-to-end suite, with a fake Drive that plays several devices
  • The review of every change

Key decisions

Local-first, with Drive as the sync

Why. The data lives on the device, so the app opens and works offline, and there is no database for me to run or pay for. Each device writes only its own snapshot; no file on Drive ever has two writers. Every record carries a hybrid logical clock made of the time, a counter and the device. For each record the highest clock wins, and the merge gives the same result in any order, however many times it runs.

Trade-off. The newest version wins the whole record. If I edit the same movement on both devices while offline, one of the two edits is lost. Sync also needs a Google account, so a guest gets no sync and no backups.

A backend only for sign-in

Why. A refresh token kept in the browser can be read by any script on the page. The four functions keep it in a cookie the page can’t read, check for CSRF on the token endpoint and store no data of their own. The sync code talks to an interface, with Drive as one implementation, so an API can take its place later and the same cookie can sign its requests.

Trade-off. The app is tied to Google. Sign-in and sync go through the same account, and the app has to catch a user who unticks the Drive permission on the consent screen and send them back.

Gates in hooks, not in prompts

Why. An agent can skip a rule written in a prompt, and after a long session the rule may not be in its context anymore. A hook runs on every edit and every commit, whatever the agent remembers. The hooks also decide what a review looks at. A model asked to judge “the files of the work” never converges: each fresh run rereads everything, reads the rules a little differently and finds something new on lines it had already passed. So the reviews judge hunks. An approved hunk goes into a registry, and the commit gate compares the diff with that registry instead of trusting an agent’s last answer.

Trade-off. The hooks only see commits that an agent runs through its shell tool, and the first scaffold never went through them. They also cost time. About one verdict in three sends the work back, and every round means more writing and another review.

In code

The clock that orders every write across devices:

hlc.tsillustrative
// `<ISO ms>-<counter>-<deviceId>`: fixed-width parts, so string order is causal order.
export function nextHlc(last: string | undefined, nowMs: number, deviceId: string) {
  const previous = parseHlc(last);
  // The wall clock can stall or go back: never fall behind the last clock.
  const ms = Math.max(nowMs, previous?.ms ?? nowMs);
  const counter = previous && ms === previous.ms ? previous.counter + 1 : 0;
  // Out of counter values: borrow the next millisecond.
  return counter > COUNTER_MAX
    ? formatHlc(ms + 1, 0, deviceId)
    : formatHlc(ms, counter, deviceId);
}

Results

68
end-to-end tests, each run in desktop Chrome and on an iPhone 15 profile
7
days from the first commit to the 34th
1 in 3
review verdicts sent the work back (28 of 81)

MiaFinanza is in production, installed on my iPhone’s home screen, and anyone can try it as a guest. The doctrine and the hooks are in the repo with their tests; the newest gate, the version check, went in on the last day.