Home About Contact LinkedIn Work with me
All work

One designer. One AI. One full-stack app.

Shipped Sous Pantry solo on iOS (Swift) using light spec-driven development and rapid prototyping across several AI coding tools. Research to App Store in six months, with zero rework between prototype and shipping. The Android client is in build on the same backend.

Live on the App Store iOS shipped · Android in build Built with Claude Code Solo, end to end

Serves

iOS, live

Swift on the App Store. Android (React Native) in build on the same backend

Prep time

4 months

Research, ideation, competitor analysis & learning the tools

Cook time

6 weeks

Focused iOS implementation + 2 weeks QA, launched June 2025

Ready in

6 months

Concept to App Store, solo. Android shipping TBC

The problem

Meal planning apps solve the wrong friction.

Most meal planning apps either require tedious manual data entry, suggest meals with ingredients you don't have, or don't connect to your actual shopping workflow. Users want to reduce food waste and simplify meal decisions, but existing solutions create more friction, not less.

Manual entry, high abandonment

Tracking a pantry by hand is tedious enough that most people give up within a week.

Suggestions that ignore reality

Meal ideas built from a generic recipe database, not what's actually in your kitchen.

Disconnected from shopping

Meal planning and grocery shopping live in separate apps, so nothing carries over.

Research & discovery · Jan to April 2025

Four months before a line of production code.

User research. Interviewed potential users on their actual pain points:

  • "I buy groceries then forget what I have. Things go bad." (food waste)
  • "Deciding what to cook takes 20+ minutes daily." (meal fatigue)
  • "I have to manually make lists or remember what I need." (shopping friction)

Competitor analysis. Studied Yummly, BigOven, Mealie, and AnyList:

  • Most require manual pantry entry, the exact friction users complained about
  • AI features were superficial, bolted on rather than core to the experience
  • None connected pantry → meals → shopping into one seamless loop

The rest of discovery went into learning what this build would require: modern mobile development in Swift and React Native, LLM integration with the Anthropic API, and rapid AI-assisted prototyping workflows with Claude Code.

Core hypothesis · validated before writing a single screen

Photo-based pantry scanning + AI reasoning removes the #1 friction point, manual entry. Get this right, and the rest follows.

The method

Light spec-driven development,
not vibe-coding from scratch.

Rather than jumping straight into code, I ran a structured seven-phase process before implementation began, treating architecture decisions with the same rigor as UX decisions.

  1. Product clarity

    Vision, pain points, and a single success metric: plan a week of meals in under 15 minutes. Key assumption to validate: photo-based scanning removes enough friction to make the whole idea work.

  2. Architecture-first design

    Mapped the data model (users → pantry inventory → meals → shopping lists), put a single LLM reasoning layer behind inventory matching, meal suggestions and shopping automation, and settled on one backend API designed to serve both iOS and a later Android client.

  3. Research & validation

    Settled the question the whole product rests on: should the AI read the photo directly, or should the photo first become a structured list of items that everything else works from? I chose the second. Extracting the items first means the user can see and correct what was recognised, and every later feature (meals, shopping lists) reads from that same clean inventory instead of re-interpreting an image. I tested both with real API calls before committing.

  4. Capability audit

    Identified the specialised skills the build needed: Swift for uncompromising native iOS performance, React Native for Android with shared business logic, Anthropic API prompt design, native camera APIs, and a Node.js backend.

  5. Options & recommendation

    Weighed three options: a web-only MVP, one platform first, or both in parallel. Solo, parallel was never realistic, so the real question was which platform. I checked mobile market share in the two markets I was building for. In early 2025 iOS held roughly 63.7% in Australia against Android's 36.3%, and 57.5% to 59% across North America. iOS first put the app in front of the larger share of users in both markets from day one, with Android to follow on the same backend once the core product was validated. (StatCounter, early 2025: Australia, North America.)

  6. Executable roadmap

    Phase 1: iOS MVP, six weeks of implementation plus two weeks of QA covering pantry scanning, AI meal suggestions, shopping lists and preferences. Phase 2: Android in React Native with native patterns, not a direct port, reusing the backend and business logic. That phase is still running.

  7. Ship with quality

    Interactive prototypes built with Claude Code and run in the iOS Simulator validated UX before production code. Iterated with real early testers, then worked across several AI coding environments (Cursor, Claude Code, OpenClaw and my own Hermes Agent), matching the tool to the task across design, components, API integration and testing.

Technical architecture

No server to babysit, and no keys in the app.

iOS · Swift + Xcode

Native SwiftUI for photo capture and pantry management, with SwiftData for on-device persistence and an offline-first experience, syncing with Supabase when back online. Responsive across phone and tablet.

Android · React Native (in build)

In build. Shared component architecture with iOS design patterns, native modules for the camera and performance-critical paths, and the same Worker and Supabase contracts, designed as a re-platform against what is already deployed rather than a direct port.

Data

PostgreSQL on Supabase for user data, preferences and meal history, paired with on-device SwiftData for offline access, syncing when connectivity returns.

AI · proxied at the edge

Claude is the reasoning engine today, with purpose-built prompts for inventory extraction, meal reasoning and shopping lists. Model calls go through a Cloudflare Worker that holds the API key as a wrangler secret and accepts only requests carrying a bearer token, so no model credential ships in the app. Each prompt returns a strict JSON contract the UI renders directly.

Routing every model call through one place is what keeps swapping or comparing models a contained change. I'm currently evaluating ILMU, a Malaysian-built LLM, for non-English input, testing how pantry items and recipes hold up in other languages and regional dialects, where product names and cooking terms rarely translate literally.

iOS client // holds no model credentials ├──> Cloudflare Worker souspantry-images Authorization: Bearer <APP_SECRET> │ POST /anthropic proxies model calls │ POST /generate recipe imagery via fal │ secrets: ANTHROPIC_API_KEY, IMAGE_API_KEY │ storage: R2 bucket, souspantry-recipe-images └──> Supabase Postgres + auth households, pantry, shopping, meal history row-level security per household

Under the hood

Four decisions, in the shipped code.

These are unedited excerpts from the repositories behind the app on the App Store. In each one, the design decision and the engineering decision were the same decision.

01 · The design system is code, not a swatch sheet

Sous Pantry/Theme.swift
// Earthy green + dark navy design system. enum AppTheme { static let navy = Color(red: 0.086, green: 0.141, blue: 0.216) // #162437 static let green = Color(red: 0.176, green: 0.353, blue: 0.239) // #2D5A3D static let cream = Color(red: 0.969, green: 0.953, blue: 0.925) // #F7F3EC static let gold = Color(red: 0.769, green: 0.588, blue: 0.353) // #C4965A } extension View { // one card treatment, applied everywhere func soCard(padding: CGFloat = 16) -> some View { self.padding(padding) .background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 14)) .shadow(color: AppTheme.navy.opacity(0.07), radius: 8, x: 0, y: 2) } }

The core palette and the shared card treatment live in one file, and the app leans on it hard: 1,168 AppTheme references across the iOS codebase. That's a design system enforced by the compiler rather than one that only exists in Figma, so changing a brand colour is a one-line diff, not a hunt. Auditing it for this write-up also surfaced 278 one-off colours that had drifted into feature views. That's the honest state of most design systems, and the first thing I'd reconcile.

02 · Product decisions, encoded in the prompt

backend/src/routes/shopping.js
// POST /api/shopping/generate — restock list from pantry const prompt = `${learnedContext}${dietaryContext} You are Sous Pantry's AI chef for an Australian household. Based on the pantry below, suggest a practical weekly shopping list of items that are low, missing, or commonly needed alongside what's in stock. Aim for 10-15 items. Prioritise items that unlock the most meals. Return a JSON array only — no markdown: [{"name":"…", "category":"…", "quantity":"…", "priority":"Essential|Nice to Have", "reason":"one short reason"}]`;

Three product decisions live in this string, not in a spec doc. "Prioritise items that unlock the most meals" is a ranking rule: the list is sorted by cooking potential, not alphabetically. priority gives the UI something to sort and badge. reason forces the model to justify every row, so the interface can always tell you why an item is there. Designing the prompt was designing the interaction.

03 · The cold-start problem, solved separately

backend/src/routes/shopping.js
// POST /api/shopping/staples — staples list for empty pantry You are Sous Pantry's AI chef. This user has an empty pantry. Generate a practical Australian household starter pack of 15-20 pantry staples — everyday essentials like eggs, flour, rice, pasta, canned tomatoes, olive oil, butter, onions, garlic, and common spices.

A recommendation engine with nothing to recommend from is the classic empty-state failure. Rather than a blank list or generic filler, an empty pantry routes to a different endpoint entirely and returns a real starter pack. The locale in that prompt is a setting, not a limit. The same structure serves any kitchen, which is exactly what the ILMU language testing is for. A UX decision that had to be built as an API decision.

04 · Reading a real Australian supermarket receipt

backend/src/routes/receipt.js
// POST /api/receipt/image — photographed receipt → OCR → line items // POST /api/receipt/text — eReceipt web sync → line items The following text was extracted from an Australian grocery receipt. Identify every food and grocery item purchased. Rules: - EXCLUDE: store name, date, prices, subtotals, GST, loyalty/rewards points, payment method, bag fees. - Fix OCR artefacts (e.g. "Wh0le M1lk" → "Whole Milk"). - Expand abbreviations (e.g. "ORG FF MILK 2L" → "Organic Full Fat Milk"). - Capture weight or volume if shown ("500g", "2L", "6pk"). - Use Australian supermarket product naming conventions.

These rules come from reading real receipts. Supermarket dockets print ORG FF MILK 2L, not "Organic Full Fat Milk", so the parser expands them. They print loyalty points and bag fees that aren't groceries, so those are excluded by name. Photographed receipts arrive with OCR damage, so character repair is part of the contract. There are two ingestion paths: a photographed receipt, and an eReceipt web sync that skips OCR entirely when the text is already digital. The market sits in the prompt rather than in the parser, so the same rules re-tune for another country's receipts without touching the pipeline.

The product

Pantry → meals → shopping, in your pocket.

Screenshots from the shipped iOS app, running against a real pantry. Not mockups. Each step of the loop is also a decision about how much to tell the user.

Sous Pantry pantry inventory listing 49 items with categories, quantities and expiry reminders

A pantry built from receipts, not typing

Forty-nine items, none of them typed in. The list was scanned straight from the grocery receipt and expanded into full product names. Each item shows its printed expiry date where it has one, alongside what the app has learned about how long that kind of item usually keeps: "Usually fresh ~2 days · reminder set". The printed date is what it goes by, the learned window fills the gap for items that carry no date, and the reminder is set either way.

Sous Pantry home screen showing a Pantry Audit Due card with one expired item and one expiring tomorrow, and a prompt showing six meals cookable right now

Shows what is expiring, and what to cook with it

The home screen opens with what is about to be wasted. "Expired" is red, "Expires tomorrow" is amber, and each has a matching dot, so you see how urgent an item is before you read it. Directly under that sits "6 meals you could cook right now." A warning on its own only makes people feel bad. Putting a meal next to it gives them something to do tonight. Price trends sits below, because waste and spend are the same problem.

Sous Pantry recipe suggestion showing a 75 percent pantry match badge in amber with soy and sesame allergen chips

Guardrails on every AI suggestion

Every suggestion carries a pantry-match percentage and its allergens before you open it. The disclaimer sits at the top of the screen, not buried in settings: "Sous is AI-powered and can make mistakes. Always verify recipes before cooking." Those are the guardrails I built around the AI. A 75% match shows in amber because it is a good option, not a certain one.

Sous Pantry showing a recipe pulled in from an external cooking site, scored at 45 percent pantry match with the missing ingredients listed and a To buy action

It reads recipes from anywhere, not just its own

This recipe did not come from Sous. It came from an external cooking site, and the app read the page, scored it against the pantry at ~45%, and listed what was missing: garlic, olives, parmesan. "To buy" sends those straight to the shopping list, "Open link" goes back to the original page. The tildes are deliberate. When the numbers are read off someone else's page rather than generated here, the estimate is approximate and the interface says so: ~10 mins, not 10.

Sous Pantry recipe detail showing 9 of 12 ingredients in pantry with an add 3 missing action

From recipe to basket in one tap

"9 of 12 ingredients", each row marked In pantry or Missing, with one "Add 3 missing →" action. Changing the serving count rescales every quantity. That closes the loop. The pantry becomes a recipe, and what is missing becomes the shopping list.

Key features

Four features, one connected loop.

Pantry scanning

Photograph the shelf and an LLM extracts the items, with manual entry as a fallback. Which model does that is not fixed. Extraction runs through the same server-side wrapper as every other call, so swapping or comparing models never touches the app. Items are tracked with quantity and expiry, syncing instantly across devices.

AI-powered meal suggestions

The model reads dietary preferences and current inventory, then suggests 3 meals you can cook right now and lists exactly which pantry items each one uses.

Shopping list automation

Select meals for the week and the app works out exactly what's missing, organised by category: produce, proteins, pantry staples.

One system, many clients

Two services sit behind the app and nothing else: a Worker for model calls and imagery, and Supabase for data. A second client builds against the same two contracts rather than reimplementing them, and both clients share the same design tokens.

Development process

Built across several AI coding tools.

Interactive prototypes built with Claude Code and run in the iOS Simulator through Xcode validated UX decisions before a line of production code was written. From there the work moved between tools rather than living in one: Claude Code stayed on component and API integration loops and mid-build diagnosis, Cursor for fast in-editor iteration, OpenClaw, and Hermes Agent, one I built myself, for the repetitive scaffolding. Using several on real work is how you learn what each is actually good at. The iOS prototype went to production in six weeks with no rework; the Android client is being built on the backend and business logic already shipped.

Jan – Apr 2025 · 4 months

Research & planning

User research, competitor analysis, ideation, and learning the tools this build would require.

Apr – mid-May 2025 · 6 weeks

iOS implementation

Focused development against the roadmap from the spec-driven planning phase.

Mid-May – mid-June 2025 · 2 weeks

iOS testing & launch

QA, refinement, and launch. Sous Pantry went live on the App Store in June 2025.

From end of May 2025 · in progress

Android build · ongoing

Started alongside iOS testing and still in build, reusing the backend and business logic already shipped. Ship date TBC.

Results & impact

Production quality, zero rework.

Zero Rework between prototype and shipped production code
6 months Concept to the App Store, solo
1,168 Design-token references. The system enforced in code, not just Figma

The closed beta ran across 15 real households, deliberately split between paid-subscription and free-tier accounts so the paywall and its limits got exercised alongside the core loop. It ran long enough that the same themes kept recurring rather than new ones appearing. Three came up again and again: photo-based pantry scanning feels close to magic next to manual entry in competitor apps, the meal suggestions read as genuinely useful rather than generic, and the shopping lists match what's actually needed, which cuts waste.

  • Production-ready AI integration: Anthropic API with deliberate, transparent UX
  • Shipped to the App Store from a solo contributor: design, client, backend and AI
  • Zero external service dependencies: every infrastructure layer owned end-to-end
  • Backend architected so a second client reuses the same API, and the Android build is proving it now

Why it matters

The same problems, from the kitchen side.

Sous Pantry is built for any kitchen. Pantry, meals and shopping work the same wherever you cook, and only a thin local layer changes per market: receipt formats, product naming, and language. Australia was the first market to be tuned, North America was scoped alongside it, and other languages are in testing now. Retailers see the basket. This app starts one step earlier, in the cupboard, which turns out to be the same set of problems approached from the other end.

List building is cart building

Generating a weekly shopping list from what a household already has is the front half of an online grocery order. The ranking rule, prioritise what unlocks the most meals, is a basket-composition problem, not a search problem.

Onboarding an empty basket

A new customer with no history is the hardest recommendation case in retail. Here it's an empty pantry, and it routes to a purpose-built starter pack rather than a blank screen or generic filler. It's a cold-start pattern that transfers directly.

Waste is a design outcome

Suggesting meals against what's already in stock, and tracking expiry, means buying less of what would spoil. Reducing household food waste is a sustainability outcome reached through interface decisions, not messaging.

AI that has to explain itself

Every recommendation carries a reason field, enforced at the API contract level, so the interface can always say why an item is on the list. For grocery AI, where trust is the constraint, explainability is a build requirement rather than a nice-to-have.

What worked well

Five decisions that paid off.

  1. Architecture-first

    One week on the database schema and system design prevented rework across eight-plus weeks of implementation.

  2. Photo-based input

    The single biggest UX lever. Manual entry ran 3–5 minutes per session. A camera snap or photo scan brings it down to 10–15 seconds per item.

  3. Transparent AI reasoning

    Users trust a suggestion when they can see why. "You have chicken, rice, and soy sauce → fried rice" beats an unexplained recommendation.

  4. Designed for a second client from day one

    Android reuses the API, business logic and database schema, so the second build starts from a proven contract instead of a rewrite.

  5. Prototype in the Simulator first

    Prototypes built with Claude Code and run in the iOS Simulator caught UX issues before implementation, while they were still cheap to change.

What I'd do differently

Four things for next time.

  1. Assistive support, designed in, not retrofitted

    Voice input shipped as a convenience feature, but the assistive layer behind it (VoiceOver labels, Dynamic Type, contrast auditing) was left until after launch. On a product whose core interaction is a camera and a long list, that belongs in the planning phase, not the backlog.

  2. Validate receipt import sooner

    Receipt OCR and parsing was deferred as a "future feature" and turned out to be far harder, and far more central, than expected. It's built and running now, but it should have been proven during the iOS build, because it's the fastest path into a populated pantry and the whole product is weaker without it.

  3. Longitudinal testing, not just sessions

    Discovery interviews and a 15-household beta validated the core loop, but the pantry friction that surfaced after launch was the kind short sessions miss: a pantry that's been accumulating for weeks behaves nothing like a freshly seeded one. Next time I'd run a smaller group over a longer stretch rather than more people once.

  4. A clearer offline-first strategy

    Local-first sync works, but the approach evolved mid-build, and deciding upfront would have simplified things.

What I took away

What this taught me as a design technologist.

Spec-driven development is still relevant

Fast-shipping teams still need architecture thinking. It isn't "spec vs. vibe-code". It's a smart spec paired with rapid iteration.

AI integration needs design thinking

LLMs are powerful, but thoughtful UX is what turns that power into delight. Transparent reasoning plus user control drives adoption.

Multi-platform is one system, not three

Clients and backend aren't separate projects. They're one system with different views onto it. Architecture thinking from day one prevents rework.

Shipping removes assumptions

Feedback on a shipped product beats research on wireframes every time. I learned more in the first weeks on the App Store than in the months of planning before it.

Build it as if a team will inherit it

A design system, one shared backend, reusable business logic. I was the only person on this, but I built the artefacts a team would need. That is what let a second client start from a proven contract instead of a rewrite, and it is the part that transfers to any team.

Prototype before you commit

A prototype running on a real device answers questions a mockup cannot. Building them with Claude Code made them cheap enough to throw away, so decisions got made on something I could use rather than something I could only look at.

Rapid iteration and production quality are not opposites

With the right tooling and a spec worth following, moving quickly did not cost me quality. Zero rework between prototype and shipped code is the evidence.

What's next

Where Sous Pantry goes from here.

iOS

Iterating on user feedback: sharpening receipt import, meal-history learning, richer dietary preferences, and the assistive layer.

Android

Finishing the React Native client against the shipped backend, then store submission. Ship date TBC.

Web version

A React.js companion for recipe discovery and collaborative meal planning.

Integrations

Grocery delivery APIs, calendar sync, and nutrition tracking.

Technical links

See it, or look under the hood.

Code like an engineer, think like a designer, use AI to amplify both.

Let's build great things together

The codebase is private.

Happy to walk you through the Sous Pantry source: backend, iOS, and the AI integration. Send a quick request with who you are and I'll open it up.

Request access by email Use the contact form

Requests are usually answered within a day.