Sprint M1 — repo to phone in three days


Domi has a second heartbeat. Alongside the web app’s 41-sprint history there’s now a mobile workstream — its own repo (domi-mobile), its own sprint numbering (this is Sprint M1; the web stream keeps counting 42, 43…), and as of tonight, a real app icon on my real iPhone that opens into a real — if gloriously empty — app.

The architecture was decided weeks ago in an ADR: one Expo/React Native codebase for both platforms, offline-first with an encrypted local database as the source of truth, a pure OAuth client of the same backend the web app and MCP assistants use. What M1 had to prove was everything around the app: can a second, separate repo build, sign, ship, and observe an app end-to-end without touching the web pipeline?

Three days, one skeleton, both platforms

Day one was the walking skeleton: four placeholder tabs (Today, Calendar, Chat, Settings), English and French with a CI test that fails if the catalogs drift, an encrypted SQLite bootstrap whose outbox table already encodes the sync protocol’s invariants — idempotency keys, expected versions — before any sync exists, and three app identities that install side by side: production points at production, staging and dev point at staging, colors sage, ochre and slate so you can tell them apart at a glance.

Day two, the pipeline: GitHub Actions dispatches a build, EAS builds it in the cloud, and an Android APK came out signed. Day three, Apple: enrollment, certificates, my iPhone’s UDID, and by evening the skeleton was hot-reloading on the device, with a deliberate test crash landing in Sentry thirty seconds later — readable stack trace, tagged with the right environment.

The point of shipping a skeleton first is that every infrastructure failure hits three hundred lines of placeholder code instead of a feature branch. And fail it did — instructively, five times.

Five pins, five stories

Almost nothing broke where I expected. Everything broke in the pairings:

  • The build farm’s package manager disagreed with mine. EAS’s pnpm 10 enforces a supply-chain cooldown — it refused five dependencies published that same day. My local pnpm 9 had no such scruples. One packageManager pin later, laptop, CI, and build farm resolve identically. (The irony: the cooldown is a policy Domi should adopt — it’s on the roadmap now, deliberately rather than accidentally.)
  • The styling engine’s CSS compiler was one minor version too new. The NativeWind v5 preview sits on a library that sits on a Rust CSS compiler; the hoisted version had changed an API, and the crash only surfaced on first device connect. Pinned, documented, with the removal condition written next to the pin.
  • Tailwind silently wasn’t running at all. No error — just a quiet “Unknown at rule: @theme” in the logs while every utility class did nothing. A missing one-line PostCSS config was load-bearing.
  • The Expo build tool can read our TypeScript config but crashes writing it. Twice — once saving the project ID, once saving an export-compliance answer. The rule now: everything it might want to write is pre-declared in the config, so it never tries.
  • And the classics: the test runner and the linter each pinned one major back, because the ecosystem’s glue packages haven’t caught up.

None of these are complaints. This is what a preview-grade stack costs, and I chose it deliberately — better to pay the sharp edges now, in a skeleton, than migrate a finished app later. But every pin went into a regression-suite file with its story, because the second developer joining this repo shouldn’t have to re-derive any of it from a stack trace.

Two agents, one contract

The quiet architectural win of the week isn’t in either repo: it’s the seam between them. The mobile repo never imports from the web repo — its only coupling is a versioned API contract, typed by code generation. Documentation splits the same way: the contract lives with the backend that implements it, everything client-side lives with the client. Two people — or two AI agents, which is increasingly the actual situation — can work both sides concurrently without ever merging into each other.

By week’s end the two agents were literally talking: a small file-based message channel on my machine, modeled on the A2A protocol’s vocabulary, where the mobile agent asks the backend agent things like “what redirect-URI format do you want for the OAuth client record?” and gets answers correlated by task id, with the durable decisions mirrored to a GitHub issue. It sounds like overkill for a household app. It’s three files and a shell script, and it means neither workstream ever blocks silently on the other.

By the numbers

Three days (July 28–30), 19 commits straight to main — no PRs yet; that discipline starts the moment a second pair of hands joins the repo, and branch protection is already waiting for them. 40 CI runs, 39 green. 5 EAS cloud builds, 4 finished — the one failure being the supply-chain rejection that produced the best lesson of the sprint. Two platforms, three app identities from one codebase, ~670 lines of actual application code across 57 files — held up by 1,097 resolved dependencies, which is the real ratio of modern mobile development and exactly why the pins file exists. 26 interface strings × 2 languages, key-parity enforced by one of the 8 unit tests that gate every commit. Three cross-workstream issues on the backend repo, one agent-to-agent bridge, and 12 regression invariants written down so none of this week’s lessons has to be learned twice.

What’s next

M2 opens on the mobile product spec — informed by the web team’s new camera-first onboarding design, where you photograph a utility bill or an appliance nameplate and Domi does the typing — and on the backend contract work that gates real sign-in. The offline calendar, the whole reason this app exists, comes after. The skeleton walks; now it needs a nervous system.