A warm, hand-painted recipe app led by Otto the otter chef.
750+ honest recipes, a place for yours, and nobody shouting five stars at you.
Discover & cook
- Browse and search 750+ recipes (seeded from TheMealDB, served from Otto's own DB) with painted category tiles and an "Otto's pick" hero
- Recipe detail: parallax photo hero, serif title on cream, source attribution, computed meta (servings Β· ingredients Β· steps), live ingredient scaling with US/Metric conversion, inline video, semantic-ink method steps, nutrition estimate card, related-recipes exit
- Cook mode: mise-en-place β big-type steps with hand-painted Otto action art (chop/mix/sautΓ©/simmer/bake/wait/season/pour/serve), tappable durations that start named timers, a multi-timer hub, alarm sound + vibration, keep-awake, swipe navigation, exit protection, and a Proud-Otto finish with "Snap your plate" journal capture
Your cookbook
- Save any recipe with the paw mark (Otto's signature)
- Import from any recipe site β paste a URL, Otto reads the ingredients and steps (deterministic schema.org parsing, no AI), you review, it's on the shelf with the source credited forever
- Ask Otto β chat with Otto to generate a recipe, or snap a photo of one; write your own in the same editor; edit and delete freely. The editor's Ask-Otto row (new + edit modes) hands whatever's typed to the chat so Otto continues from your half-finished recipe
- Cookbook tab with All Β· Saved Β· My recipes segments and a Cooked filter
Plan & shop
- Otto's week: a loose 7-day planner (no meal-slot guilt), add from any recipe or from the plan, cooked check-offs, "What's cooking tonight?" surfaced on Discover
- Shopping list built from the week on demand: one row per ingredient with summed quantities, aisle sections, provenance ("for World's Best Lasagna"), check-offs that never reorder mid-store, your own extras
- Shared kitchen: start a household or join one with an invite code β everyone adds to and checks off the same list in real time
- Share a recipe or a shopping list as a link (capability tokens) or a rendered share card
The rest
- 3-screen painted onboarding + splash (still image + animated lid-lift, reduced-motion aware)
- "You" tab: earned stats (cooked/saved/yours β each a door), a private cooking journal, inline US/Metric, editable name + avatar, quiet sign-out, visible account deletion
- Otto Club membership β $34.99/yr Β· $4.99/mo Β· 5-day trial, honest pricing math; RevenueCat wired end-to-end (purchase/restore/entitlement + Supabase membership webhook), running on the RC Test Store until the App Store products go live
Expo SDK 54 Β· React Native 0.81 Β· React 19 Β· TypeScript Β· expo-router v6 Β· TanStack Query v5 Β· zod v4 Β· Supabase (Auth + Postgres + Edge Functions + Realtime) Β· Lora (serif). No custom server β server-side logic runs as Supabase Edge Functions. RLS is the security boundary.
Recipe-App/
βββ app/ expo-router routes (file-based; a file = a screen)
β βββ _layout.tsx root: GestureHandler β ErrorBoundary β SafeArea β
β β QueryClientProvider β AuthProvider β <Stack> (+ ToastHost, NotifSync)
β βββ index.tsx launch gate β onboarding / sign-in / tabs (resolveRoute)
β βββ (auth)/ sign-in Β· sign-up Β· forgot-password (+ redirect guard)
β βββ (tabs)/ Discover (index) Β· Cookbook Β· raised οΌ (create/chat) Β· Plan Β· Account
β βββ recipe/[id].tsx recipe detail
β βββ recipe/cook/[id].tsx Cook mode
β βββ recipe/edit.tsx recipe editor (write/edit your own)
β βββ add.tsx Add sheet (URL / paste / photo import)
β βββ shopping.tsx shopping list
β βββ household.tsx shared kitchen (create/join by code)
β βββ journal.tsx cooking journal
β βββ otto-club.tsx membership / paywall surface
β βββ chats.tsx recent Ask-Otto chats
β βββ onboarding Β· notifications Β· preferences Β· faq Β· auth/callback Β· *-password
β
βββ src/
β βββ features/ feature-first modules β each owns its screens, components,
β β *.queries.ts (TanStack Query), pure logic (*.ts) + tests (*.test.mjs)
β β βββ auth/ session (AuthProvider), sign in/up, password reset,
β β β OAuth (Apple/Google/Facebook), social + username helpers
β β βββ recipes/ Discover, recipe detail, cards, filters, video embed,
β β β TheMealDBβrecipe transform, ingredient scaling
β β βββ cookbook/ saved + "my recipes" with segments (All Β· Saved Β· Mine)
β β βββ cook/ Cook mode + engines: session (step split/match),
β β β stepEnrich (timers/temps), stepAction (which Otto art)
β β βββ nutrition/ the on-device nutrition engine (see below) +
β β β NutritionCard / CalorieRing + category estimates
β β βββ planner/ Otto's week + shopping list; week + shoppingList engines
β β βββ household/ shared kitchen: queries + realtime hooks (useHousehold)
β β βββ import/ Add sheet (URL/paste/photo) + recipe editor + draft hand-off
β β βββ chat/ "Ask Otto" chat β recipe generation
β β βββ share/ share cards + capability share links (recipe / list) + tokens
β β βββ journal/ private "snap your plate" cooking journal
β β βββ notifications/ reminder sync (NotifSync) + notification preferences
β β βββ profile/ Account, Preferences, Household screen, Otto Club, FAQ
β β βββ onboarding/ carousel + splash + the onboarding/route gate
β β
β βββ shared/ cross-feature primitives
β β βββ theme/ design tokens + semantic-ink (terracotta = computed/interactive)
β β βββ ui/ shared components (Screen, buttons, mascot states, toastβ¦)
β β βββ supabase/ the Supabase JS client
β β βββ lib/ fdaCalories (21 CFR 101.9 rounding), format helpers
β β βββ haptics.ts haptic feedback wrapper
β β βββ storage.ts AsyncStorage key-value store
β β βββ imagePicker.ts camera / photo picker
β β βββ bus.ts Otto event bus (mascot reacts to app events)
β β βββ assets.ts asset registry (mascot/action/food art)
β β βββ motion.ts reanimated helpers (no layout anims on web)
β β
β βββ types/database.ts generated Supabase types (regenerate after each migration)
β
βββ supabase/
β βββ functions/ Edge Functions = the "backend" (Deno, deployed to Supabase)
β β βββ content/ TheMealDB proxy (key server-side; the app never sees it)
β β βββ import-recipe/ URL β recipe via schema.org JSON-LD (deterministic, SSRF-guarded)
β β βββ generate-recipe/ Ask Otto β Claude (opus) recipe gen: prompt Β· chat Β· photo modes
β β βββ resolve-nutrition/ Claude (haiku) picks a real USDA record for unseen ingredients
β β βββ revenuecat-webhook/ RevenueCat events β public.memberships (shared-secret auth)
β β βββ canonicalize/ otto_recipes curation: original recipe β canonical structured record
β β βββ delete-account/ completes account deletion (auth user + storage)
β βββ migrations/ Postgres schema + RLS as timestamped SQL (source of truth)
β
βββ assets/ mascot art, Otto action paintings, food-category icons, app icons, sounds
βββ docs/ reference/ (living docs) Β· history/ (build record + captures) Β· legal/
β see docs/README.md for the index
βββ e2e/ Playwright journeys (auth Β· cook Β· plan Β· share Β· β¦)
βββ test/ node --test unit suites (co-located *.test.mjs run here too)
βββ eas.json EAS Build profiles (production = native, no OTA channel)
βββ app.json Expo config (bundle id com.otto.recipes, plugins, scheme otto://)
βββ references/ non-app material: the Altavida brand project + reusable app/website scaffolds
Your app (Expo / RN)
β Supabase JS client + TanStack Query
βΌ
Supabase
ββ Postgres ββ your data behind Row-Level Security (RLS is the security boundary)
ββ Auth ββ email/password + Apple/Google/Facebook OAuth
ββ Realtime ββ the shared shopping list syncs via postgres_changes
ββ Edge Functions ββ the few jobs that must run server-side:
content β TheMealDB import-recipe β schema.org delete-account
generate-recipe β Claude (opus) resolve-nutrition β Claude (haiku) + USDA
revenuecat-webhook β memberships canonicalize β otto_recipes curation
- No custom server. The app queries Postgres directly through the Supabase client; RLS decides what each user can see. The Edge Functions hold the only secrets (TheMealDB / Anthropic / USDA keys, the RevenueCat webhook secret, the service-role key) so they never touch the app bundle.
- Memberships: RevenueCat is the purchase layer (entitlement
club); its webhook writespublic.memberships(read-own-row RLS, service-role writes) so the backend always knows who's in the Club. - Two content sources, one job each β TheMealDB supplies recipes + ingredient lines (it carries no nutrition, by design); USDA FoodData Central (CC0) supplies the per-ingredient nutrition those lines are costed against.
- Semantic ink β terracotta = computed/interactive, ink = human-authored. Honesty law β never fabricate data;
nullbeats a guess.
Runs on-device with zero network calls β usdaTable.json ships in the app. An ingredient line is parsed to grams (parse.ts), looked up against the bundled USDA table (lookup.ts), and costed Γ per-100g Γ· servings with guards for raw/cooked, coverage, and kcal plausibility (compute.ts + guards.ts). Only an unseen ingredient falls back to the resolve-nutrition Edge Function (Claude picks a real USDA record), then caches forever. The cook/scale/shopping engines (cook/session, stepEnrich, planner/shoppingList, recipes/recipe.scale) are likewise pure, deterministic, and unit-tested β no LLM in the render path.
Light-only, token-pure. Terracotta #C4562E accent, cream paper surfaces, Lora serif display, warm ink. Otto appears at emotional beats; the paw is the save mark everywhere.
docs/reference/DESIGN_SYSTEM.md(authoritative) Β·docs/reference/MASCOT.mdΒ·docs/reference/SCREEN_MAP.md- Tokens:
src/shared/theme/; mascot/action/food art underassets/viasrc/shared/assets.ts - Figma DS file:
X1eGT54CTwtowHNve30vvE; master boardmM0uWkHod9rL1Ff1VJ64Au
# app (web + Expo Go)
npm install && npx expo start # :8081
# native iOS dev build (needs the native modules β expo-audio/keep-awake/image-picker)
npx expo run:ios --device "iPhone 17 Pro Max"Server-side logic (Edge Functions) and schema (migrations) live under supabase/ and deploy to the
Supabase project β there is no separate server to run.
CocoaPods on a stock Mac (no Homebrew, system Ruby 2.6):
gem install --user-install activesupport -v 6.1.7.10 --no-document && gem install --user-install cocoapods --no-document, then put$HOME/.gem/ruby/2.6.0/binon PATH.ios/andandroid/are generated (expo prebuild) and gitignored. Splash/plugin changes needexpo prebuild -p ios --clean.
# .env (root) β publishable client config, safe to expose; RLS is the security boundary
EXPO_PUBLIC_SUPABASE_URL=...
EXPO_PUBLIC_SUPABASE_ANON_KEY=...
EXPO_PUBLIC_USE_OTTO_RECIPES=true # serve recipe display from Otto's DB (not TheMealDB)EXPO_PUBLIC_* vars are inlined at build time β restart Expo after changing them, and keep the same
set registered in EAS env (eas env:list production) or TestFlight builds silently ship without them. Server-side secrets
(ANTHROPIC_API_KEY, THEMEALDB_KEY, USDA_API_KEY, the service-role key for account deletion) live
in Supabase Edge Function config, never in the app bundle.
npm run typecheck # tsc --noEmit
npm run lint # eslint app src test
npm test # node --test (unit + engine + edge-function logic)CI (.github/workflows/ci.yml) runs all three on every push to main and PRs into it.
Native builds only β no OTA channel (native modules change too often). Credentials are local
(credentials/ios/, gitignored: distribution cert .p12 + provisioning profile + the App Store
Connect API key), so no Apple ID login is ever needed. The loop:
# 1. bump ios.buildNumber (and version if it's a new release) in app.json, commit
# 2. build (~20 min; env vars come from EAS `production` environment, NOT .env)
EXPO_NO_CAPABILITY_SYNC=1 npx eas-cli build -p ios --profile production --non-interactive
# 3. submit β temporarily add ascApiKeyPath/ascApiKeyId/ascApiKeyIssuerId to
# eas.json submit.production.ios (the CLI ignores the env-var route), then revert
npx eas-cli submit -p ios --profile production --latest --non-interactive
git checkout eas.json
# 4. attach to the internal group β REQUIRED, the group doesn't auto-distribute;
# a submitted-but-unattached build reaches nobody
node ~/.claude/skills/eas-ios-testflight/scripts/tf-attach.mjs "Otto Insiders"Internal testing needs no Beta App Review β testers install the moment the build is attached.
ITSAppUsesNonExemptEncryption: false in app.json skips the export-compliance prompt.
| Doc | What it holds |
|---|---|
docs/reference/ARCHITECTURE.md |
Full v2 codebase map (grounded in the source) |
docs/reference/API_ARCHITECTURE.md |
The data-access model β RLS queries, Edge Functions, RPCs, Realtime |
docs/reference/DESIGN_SYSTEM.md |
The authoritative design system |
docs/reference/CONTEXT_ENGINEERING.md |
"How to navigate the tree" + taskβfile map |
docs/reference/contracts/ |
Feature-module, database, engine, UI, persistence, testing, data-ownership contracts |
docs/tickets/ |
Active work: nutrition accuracy + the otto-recipes data-ownership plan |
docs/history/REDESIGN_NOTES.md |
The decision log β every call with rationale |
docs/history/ Β· docs/README.md |
The full build record (tickets, rebuild, research, captures) |
On TestFlight β v1.0.15 (build 31), internal "Otto Insiders" group. The feature-first v2 rebuild is
live on main (the old RN-JS app + Express backend are archived on the v1-legacy branch). Backend runs
entirely on Supabase β all Edge Functions active, Claude generation + nutrition resolution working,
security advisors cleaned up. Otto Club paywall shows launch pricing against the RevenueCat Test Store.
Next: a device pass on the OS-level paths (Apple/Google/Facebook login, timer audio, camera, push);
real App Store subscription products (Paid Apps agreement β ASC products β swap the appl_ RevenueCat
key) and feature gating on the club entitlement; then App Store submission.
- Domain: ottosapp.com β marketing site lives in its own repo (
Otto_Website); Terms + Privacy hosted at/termsand/privacy(App Review requirement, linked from the paywall) - Auth: email/password + Apple, Google, and Facebook OAuth (Supabase Auth); transactional email over Google Workspace SMTP
- Deep-link scheme:
otto://(OAuth callbacks + share links); bundle idcom.otto.recipes, Apple teamA6J6HGNWZK - Active data work (
docs/tickets/): nutrition accuracy (usdaTable is per-100g β the weight layer owns accuracy) and OWN_RECIPE_DB β migrating the 750 seed recipes into curatedotto_recipesrows via thecanonicalizefunction
Recipe content by TheMealDB. Nutrition from USDA FoodData Central (public domain, CC0). Built with free-tier tools throughout β two sources, one job each: TheMealDB supplies recipes and ingredient lines; USDA supplies the per-ingredient nutrition those lines are costed against.