The third montage, from the weeks around launch: the Android alert that silently drops its fourth button, a keyboard trap hiding in one prop, an HTTP 431 caused by cookies, sessions that survived a failed refresh badly, and the day the app got a space in its name.
Two CSS systems, one cascade, and two one-line changes that each silently erased every input outline in the app. How CSS cascade layers make MUI and Tailwind coexist, why 'prepend: true' inverted the layer order, and why the bug only appeared behind the login.
The web console shares the phone app's identity pool — so a valid sign-in proves nothing about belonging there. Building the front door that sends non-admins straight back out, the deletion queue with its runbook inline, tables that share one set of defaults, and the dark-mode fix that lived in a colour-maths exception.
A support question — 'did my receipt save?' — needs an audit trail. This one carries no user text ever, logs one row per action rather than per write, never logs from inside a mutation, and introduced the repo's first secondary indexes along with the one screen that must never load everything.
Without over-the-air updates, a shipped build can't hear you. In-app announcements are the one channel left — with version targeting against what users actually run, severity that picks the shape, a force-update wall that fails open by construction, and two surprises device testing found.
The app spoke 19 languages while its emails stayed English, because two senders fire where no profile row exists. Cognito's standard locale attribute fixed that — and declaring it broke every branch deploy with 'Invalid AttributeDataType', because a user pool's schema is frozen at creation and the sandbox never said so.
From zero i18n and 660 hard-coded strings to nineteen languages in two tiers — per-user language that roams, Arabic flipping the shell right-to-left with a font swap that needed zero call-site changes, six plural categories, and a translation pipeline whose limiter is review, not tokens.
Store review turned 'file a request' into 'actually delete it'. Three deletion lanes done properly: a server-side Lambda whose order of operations is the design, a guest email round-trip that can't become an email oracle, and admin deletion that doesn't rewrite history.
Two weeks before launch, the whole backend moved to a brand-new AWS account. What moves, what can't (Cognito users), what a fresh account makes free (the table rename), and the two quiet gotchas — a webhook that kept building in the old account, and env vars that don't come along.
Canny Cart went live in the App Store and Google Play on 27 August. The two things that decided the date weren't code, the highest-consequence failure was silent, and the one rejection that arrived was about a permission button — here's the runbook, honestly.
Receipt scanning felt like homework because the app asked 'which list?' before it would save anything. The fix inverts the order: save first, link later — with a pending flag, provisional matches, and a one-time enrichment that keeps the snapshot immutable.
Cognito's default verification and reset emails look like a phishing test. One customMessage Lambda brands all of them — and three things will silently stop your emails if you get them wrong, plus the one line that keeps the trigger out of dependency trouble.
The second polish montage, with a spine: three screens sharing one hook that wasn't actually shared, an Android crash traced through logcat to a recycled bitmap, sheets that swallowed the back button, and the account-switch leak hiding in a persisted cache.
The default tab was a 12-line placeholder for months, because Home had nothing to own. It shipped as pure composition — no backend, no schema, no rebuild — held together by two rules that stop a dashboard from becoming a dumping ground.
Dev environments run on toy data or on a GDPR incident. The third option: mirror every production write into dev through a masking Lambda that fakes PII deterministically — same user, same fake identity, forever — with push tokens nulled, never copied.
Before v1 could ship, both stores handed over the same unglamorous list: a real website, real legal pages, and a real account-deletion path. What review actually requires, and the design decisions hiding inside the boring parts.
Notifications are where good apps become annoying, so every one had to pass a test: what would I thank the app for telling me? Local-first scheduling, token hygiene on shared devices, cold-start deep links, and broadcasts that can't drift from their source.
iPad support in seven small phases, built on one decision: a 640pt centered content column that no phone is wide enough to trigger — so every change was tablet-only by construction. Plus the App Store rejection code the plan caught before submission day.
npm ci failed on Missing: @opentelemetry/core@2.0.0 — four times, for a package version nothing asked for. A debugging war story about bundled dependencies, lockfile dialects, and published tarballs that are internally incoherent.
Point the camera at the handwritten list on the fridge and get editable items. Almost pure assembly — the interesting parts are which pipeline it reuses, why the photo gets deleted, and two Android traps that had nothing to do with vision.
The per-list budget from the MVP was measuring the wrong thing. Moving budgets onto a monthly map — with a carry-forward rule that stops editing the past from rewriting the present — and a headline that survives clearing your list.
Clearing the Done section felt like losing your history — but the record already existed as soft-deleted rows nothing read. One timestamp, three time semantics, and a documented exception to a house convention turned clearing into archiving.
A currency symbol is a user property masquerading as a constant. Unwinding one hardcoded £ touched state architecture (two kinds of readers, two stores, one setter), onboarding (guess, then confirm), and product honesty (relabel is never convert).
No price API exists, but after weeks of receipt scanning the user owns a dataset nobody sells. Three screens built on it — and the honesty gates, living in selectors not UI, that stop derived numbers from lying.
A barcode is an exact input — so the AI's job flips from interpreting to normalising, the lookup becomes a chain of fallbacks with 3-second fuses, and the cache becomes a shared global model that grows coverage exactly where the world's product databases are blank.
The headline money feature: photograph a crumpled receipt, extract priced line items with Claude vision, match them to your list, keep the receipt as an immutable record. Plus two war stories: an IAM AccessDenied that looked impossible, and the save that failed silently for a whole session.
The money feature didn't need a money model. One new schema field, prices as an attribute of shopping items, sums in pennies, a green/amber/red budget bar — and the offline architecture from Part 7 paying rent.
Six fixes too small to blog about, shipped as one deliberate pass: a 12-line parser instead of an LLM, merge-not-duplicate, bulk clear, brand pull-to-refresh, sticky headers that Android never had, and a bug cured by deleting a component.
Reusable 'Weekly essentials' lists shipped with one new schema field and zero new tables. The design fork (new model vs status vs flag), the copy-both-ways helper, and the three ripple effects one boolean forced.
The feature was one line of SQL: my most-added grocery items, ranked. DynamoDB doesn't have the clause. Here's the client-side aggregation that shipped instead — with explicit scan budgets, honest counts, and a documented exit ramp.
Users wanted to reorder and rename the aisle headers on their shopping list. Small problem: the aisles don't exist — they're free-text strings Claude stamps on items. The fix is an old modeling trick: split identity from display.
Drag-to-reorder on a grouped React Native list set off a cascade of Android gesture bugs — crash, refresh freeze, header overlap, janky scroll. The fix wasn't debugging harder. It was re-reading the requirement.
A grocery app has to work where signal dies — the store. Here's offline-first without a sync engine: React Query pauses, persists, and replays your mutations, made robust by keyed mutation defaults and client-generated IDs.
A real 3-way theme setting for Expo + NativeWind: one colorScheme.set() call cascades light/dark/system through your utility classes, the navigation theme, and the native tab bar. Redux holds the choice, AsyncStorage remembers it.
Speak your groceries, get a structured, categorised, editable list. The interesting part isn't the LLM — it's the review gate I put between Claude and my database so a bad parse can never silently save.
Same commit, opposite outcome: dev deployed clean, main rolled back every time. The log said only 'Validation failed with 1 error(s)'. The cause was a name two branches were quietly fighting over.
Metro died with a transformFile error that named no file. The trail ended at a @babel/types from 2017, pulled into node_modules by an Amplify dependency. The hunt, and the one-block fix.
My Expo app needs three backends — sandbox, dev, prod. The obvious way to hand each build its config hit a 4 KiB wall. Here's the per-environment pattern that replaced it.