You're in the kitchen, fridge open, phone in the other hand. You hit one button and say: "a couple of bananas, milk, a dozen eggs and some chicken breast."
Your shopping list now looks like this:
[
{ "name": "Banana", "quantity": 2, "category": "Produce" },
{ "name": "Milk", "quantity": 1, "category": "Dairy" },
{ "name": "Egg", "quantity": 12, "unit": "dozen", "category": "Dairy" },
{ "name": "Chicken Breast", "quantity": 1, "category": "Meat" }
]
That's real output from the parsing Lambda — "a couple" became 2, "a dozen" became 12, and everything landed in a sensible aisle. The app is called CannyCart, and this series is me building it in public: the stack, the features, and — mostly — the things that broke.
Why this app, and why in public
A grocery list is the most re-typed document in existence. You compose it while doing something else (cooking, staring into the fridge), which is exactly when typing is worst and talking is easiest. Voice → structured list felt like a real product, not a demo.
Why write it up? Because I've already burned evenings on bugs whose solutions live nowhere Google can find them. The fix for "my Metro bundler died with a 2017 babel package" deserves to be indexed somewhere. And honestly: writing the war story down is how I make the tuition worth it.
I'm building this solo, pair-programming with Claude. That shapes the workflow in ways I'll get into later in the series.
The stack, with one reason each
| Piece | Because |
|---|---|
| npm-workspaces monorepo | One repo, two apps (mobile + web), one shared backend definition. |
| AWS Amplify Gen 2 | The backend is TypeScript (CDK under the hood), the client is fully typed from the schema, and every git branch can get its own backend environment. |
| Expo SDK 57 + expo-router + NativeWind | File-based routing and Tailwind ergonomics in React Native; EAS handles builds without me touching Xcode. |
| Next.js 16 + MUI | The web/admin side. Boring on purpose — the mobile app is where the novelty budget goes. |
| Claude Haiku in a Lambda | Turning "some chicken breast and a couple bananas" into structured JSON is exactly the shaped-extraction task small LLMs are great and cheap at. |
| A skills library | Every repeatable setup (auth, soft deletes, async jobs…) lives as a written playbook in a repo I reuse across projects. The build follows the playbooks; the playbooks improve every build. |
What's built so far
A fast montage — each of these gets its own post:
- Auth + profile bootstrap — custom Cognito UI (no prebuilt Authenticator), and a post-confirmation Lambda that creates the user's profile row and group membership the instant they confirm.
- A real theme system — light/dark/system, chosen on an Appearance page, persisted, and applied through design tokens so the tab bar, headers, and screens all flip together.
- Branding — real icons, splash, and Nunito everywhere. (RN can't synthesize font weights; each weight is its own file. Ask me how I know.)
- The Shopping tab — the headline feature: mic button → live on-device transcript → Claude parse → a review sheet where you fix anything it misheard → a categorised checklist with swipe-to-delete.
The voice pipeline deserves one sentence of architecture: speech-to-text happens on-device (iOS SFSpeechRecognizer / Android SpeechRecognizer via expo-speech-recognition), so audio never leaves the phone — only the final text goes to the Lambda. Free, private, fast.
What already went wrong (a preview)
Three real errors from the first two weeks, each getting a full post:
1. Every EAS build profile needs its own backend config, so I put the config file in an EAS file env var. Then:
Files can't be larger than 4 KiB. Provided value is 410 bytes above the limit.
My config file is 4.5 KB — and the same file in a mature project of mine is 84 KB. The whole delivery mechanism was a dead end, and the redesign is post 2.
2. One evening the bundler simply stopped:
ERROR TypeError: Cannot read properties of undefined (reading 'transformFile')
The trail led through an icon library that reinvented itself between major versions, and ended at @babel/types@7.0.0-beta.4 — a package published in 2017 — silently hoisted to the top of my node_modules by a transitive Amplify dependency. Post 3.
3. My main branch refused to deploy while dev worked perfectly. CloudFormation said only:
CREATE_FAILED … Validation failed with 1 error(s).
ROLLBACK_COMPLETE
The cause was a name two branches were silently fighting over. Post 4 is the forensics.
The series map
- This post — the what and the why
- One backend, three environments — the 4 KiB limit and the per-env config pattern that replaced it
- Every icon vanished — a Metro debugging story featuring a babel package from 2017
- Main won't deploy — CloudFormation forensics on Amplify Gen 2
- "Milk, and a dozen eggs" — the voice → Claude → checklist pipeline in depth
- Light/dark/system in NativeWind, properly
Where you come in
If you've shipped on this stack, tell me where it bites later — I'd rather hear it now. And if you want the war stories as they land, follow the series.
Next up (now live): how three build profiles each get the right backend when the obvious mechanism caps at 4 KiB.




Top comments (0)