Post-mortem · 18 August 2026

Why the app closed

Pressing ▶ or ⏱ on a routine killed the app outright, with no error and no red screen. Nothing was wrong with your database, and nothing was wrong with the app's code.

A deploy failed, and nobody noticed.

The API on staging was nine hours out of date. It was missing one field the app had just started depending on. Reading that missing field threw an error, and in a release build an error like that doesn't show a red screen. It terminates the app.

So a two-minute network blip on a deploy looked, on your phone, like a broken app.

The chain

Six links. Every one is verified: the failed workflow run, the image ID on the server, the source of the deployed commit, the error on the device, and the crash report.

  1. PR #40 shipped a field and a dependency together

    It added routinePath to GET /routines/:id/chain and taught the player to read it, in the same round. From that moment the app required a server new enough to send it.

  2. The break

    That PR's staging deploy failed

    Not on code. GitHub Actions couldn't reach the VPS over SSH. It made twelve attempts across about two minutes, then it gave up.

  3. Staging kept serving the previous build

    Image 4af6259, from PR #41. Its copy of chain.ts contains the string routinePath exactly zero times.

  4. Your phone had the new app, the server had the old API

    The build on your phone was fine. It was talking to a server nine hours behind it.

  5. The missing field threw

    chain[index].routinePath was undefined, and calling .find() on it threw while the player was mid-render. That's the white flash you saw.

  6. A release build turns that into a hard crash

    React Native routes an uncaught render error to RCTFatal, which calls abort(). The process dies on SIGABRT. No red screen, because release builds don't have one.

The line itself

// lib/chain-gate.ts:29
const boundary = chain[index].routinePath.find((r) => !entered.has(r.routineId));
                              ^^^^^^^^^^^ undefined on a stale API

// what the device reported
Render Error: Cannot read property 'find' of undefined

// what the crash report showed underneath it
-[RCTExceptionsManager reportFatal:stack:exceptionId:]
RCTGetFatalHandler
objc_exception_throw  abort()   SIGABRT (signal 6)

Why it was so hard to see

Release builds don't explain themselves

The same error that shows a red screen with a file and line number in development just closes the app in release. All the diagnostic information exists, but it simply isn't displayed.

The simulator couldn't reproduce it

The simulator ran a dev build against your local API, which was up to date and shows errors. Two reasons it stayed healthy while the phone died.

The breakthrough was pointing a dev build at staging: same data as the phone, but a build that could still speak. That produced the red screen naming chain-gate.ts:29.

What the stale API was missing

The deployed server returned nine fields. The app expected twelve.

FieldSent by stale APIEffect on the app
routinePath No Crash. undefined.find() during render.
taskType No Silent. A dump step stopped being recognised as one.
routineName No Cosmetic. The sub-routine crumb rendered blank.

Only the first one was fatal, but one is enough.

What changed

Staging redeployed

Now running 67cb974, which is master. routinePath is served again. This alone fixed your phone. No rebuild was needed.

The app survives a missing field

Both reads now go through a guard. No path means no sub-routine gate, so the step simply plays. A stale server is a smaller feature set now, not a crash.

The second fix matters more than the first. The app and the API deploy on separate schedules, so an app meeting an older server is ordinary, not exceptional. TypeScript can't catch it either: routinePath crosses the network, so the type describes what you hope arrived.

The thing worth fixing next

The deploy is the fragile part, not the code.

Widening that window is a small change. It's the one that stops this shape of problem recurring.