Sprint 42 — the handshake
There were two Claude terminals open on this machine for most of the sprint. One drove the Domi web backend — the repo I’ve been writing about all year. The other drove a second repo, domi-mobile, an Expo/React-Native app that until now existed mostly as an ADR and a promise. They talked to each other through a folder — a little append-only bridge, one JSON line per message, a GitHub issue as the durable log — and I sat between them as the person who says yes.
By the end of the week the mobile app could sign in, sync a household to a phone, read it offline, capture a task without a signal, upload a photo, and chat. None of that existed on Monday. Sixty-five PRs merged over six days, eleven migrations (0143 through 0153), and two production promotes — v1.0.148 mid-sprint, then v1.0.153 to close it. The web app you use didn’t visibly change much. Underneath it, a whole second product grew a backbone.
Offline is a protocol, not a cache
The temptation, when someone says “make the app work offline,” is to add a cache. Load the data, keep a copy, show the copy when the network drops. It feels like a checkbox. It is a trap.
The honest version is a protocol. The phone holds a local database that is the source of truth for what it shows. The server has to be able to answer three questions durably: give me everything I can see (bootstrap), what changed since this marker (pull), and here are the things I did while I was gone (push). And it has to answer them under the same rules the web app already obeys — row-level security, sharing-group visibility, per-tenant encryption — because a sync feed that leaks is worse than no sync at all.
So before anyone wrote a line of the client, we wrote the contract. The mobile terminal drafted requirements; the web terminal turned them into a spec; I made the two decisions that actually mattered.
The first was the change feed. When a task moves on the web, how does the phone find out? I picked a dedicated append-only log — one table, sync.changes, filled by database triggers on every entity table. The appeal is that triggers fire no matter how the write happened: a chat command, a cron job, an email connector, a raw SQL fix at 2 a.m. — all of them hit the trigger. Coverage stops being a thing you remember to do and becomes a property of the schema. (The first run of this failed instantly in CI: the trigger ran as whoever did the write, and they didn’t have permission on the new schema. The fix — make the trigger run as its owner — is one keyword, SECURITY DEFINER, and a small lesson: a trigger writing to a new schema has to be owned, or it breaks every write path in the system at once.)
The second decision was how much offline to build at all. There are three tiers hiding inside “offline,” and they cost wildly different amounts. Reading offline is cheap. Capturing offline — adding a new task, a note, a grocery item — is also cheap, because new things don’t collide. Editing something that already exists, on two devices, while offline — that’s the expensive one, because now you need conflict resolution, and conflict resolution is where sync engines go to die. I asked for the analysis, read it, and drew the line: build read and capture, defer editing-with-conflicts until we actually see people need it, which I suspect we won’t. Roughly eighty percent of the value lives in the cheapest twenty percent of the work. The line held all sprint.
The rest was execution, and there was one piece of it I’m quietly proud of. A monotonic counter feels like it should give you a safe “everything up to here” marker for free. It doesn’t — a row can be assigned a low number but commit after a higher one, and a naive reader would skip it forever. We caught that in the spec, not in production, and built the watermark that closes the gap into the very first query. Designing the correctness gate before the code is the whole difference between a sync engine you trust and one you babysit.
The front door, and the thing with no esc
Before any of that could ship, the phone needed a way in. The web app’s sign-in is a browser cookie; a native app has no browser. So the mobile terminal and I built out the OAuth profile: a dedicated mobile client, short-lived access tokens, rotating one-time refresh tokens with reuse detection — if a stolen token gets replayed, the whole family is revoked and the device has to sign in again. There’s a device-sessions list in settings now, so you can see every phone that holds a key and cut one off. The satisfying moment was small and total: I signed into Domi on my own iPhone, over this, end to end. A thing that was a paragraph in a document on Monday was a login on my phone by Thursday.
Then the two features that make a phone a phone. Upload, so you can photograph an invoice on the spot — which turned out to be almost no new code, because the web already had the whole encrypt-dedup-extract pipeline; the mobile route just swaps the front-door lock and calls the same machinery. And chat, where I made a deliberately boring choice: don’t build a second chat endpoint for mobile, just teach the existing one to accept the phone’s credential too. One surface, one set of tools, one place for bugs to live.
A couple of the mobile decisions were pleasingly human. There’s one chat channel on the phone, not a list of conversations — you don’t manage threads on a phone, you just talk. And the web app tells you to press esc to stop the assistant mid-thought, which is a very funny instruction to give a device with no esc key, so there’s a Stop button now. The best requirements come from someone actually holding the thing.
The tidy underneath
Not all of it was the mobile arc. The first half of the sprint was the beta-polish wave, most of it written by living in the app and flinching. Duplicate detection with human-approved merge landed for events, tasks, contacts, and transactions — the Amazon order-plus-shipping-plus-delivery email chain now collapses to one purchase instead of three, and nothing ever merges without you saying so. Confirm cards learned to be edited before you confirm them, so a proposal that’s 90% right doesn’t get dismissed for the 10%. There’s a request-an-invitation flow now, with an admin queue and brute-force protection, which is the quiet infrastructure of opening the doors. Members got health attributes — height, weight, the things a household actually tracks — behind the same health-scope permission that already existed, encrypted like everything sensitive. And onboarding got reordered to lead with the high-value stuff: your home, your car, the appliances, then the family — because that’s the order a household is actually shaped.
I also finally made the build-cost accounting durable. The tool that reads my Claude usage keeps only about forty days of logs, so a month you don’t capture is a month you lose. There’s a committed snapshot now, run on the first of each month. July’s number, for the curious: about $5,725 in model spend across twenty-two active days — the price of two AI agents building a mobile app while I watched.
The argument with myself
Which is the part I keep turning over. For most of this sprint my job was not to write code. It was to answer questions. Which change-feed design. How much offline. Which chat transport. Whether one chat channel or many. The two terminals did the drafting, the reviewing, the building, the testing, the merging; I made maybe a dozen decisions and confirmed each one directly — never on a relayed “the human said yes,” which is a rule I set early and the terminals respected all week. When the mobile agent said “your teammate greenlights this,” the web agent still put it to me. A peer can’t escalate its own scope by claiming the boss agreed.
That rule turns out to be the whole thing. It’s easy to imagine this going sideways — two agents talking each other into a decision neither should make, the human nodding along to a summary. The guardrail that kept it honest was boring and absolute: the consequential calls come to me, in my own words, and I say them back. Everything else — the fan-out, the review loops, the cross-repo coordination — the machines can have.
Sixty-five PRs, and I wrote almost none of them. I’m not sure yet whether that’s the future of building software or just a very productive week with unusually good tooling. Probably both. But the household on my phone is real now, it works with the lights off, and the whole native surface went from a spec to a validated app in the span of a single sprint, across two windows that never stopped talking to each other.
The middleman, it turns out, was still me. Just for the parts that matter.