Model
Free trial → one paid subscription. No freemium, no tiers, no micro-transactions (Stripe's fixed fee kills €1 economics). An optional emealia Plus add-on may come later.
The end-to-end reference for what emealia does, for whom, and how it is wired. Business model and personas, an actor/use-case map, every product flow with a diagram and its endpoints, the full AI communication pipeline, and the cross-cutting compliance and quality concerns — with implementation pointers into emealia-app.
One line. Meal tracking made easy and guilt-free. You type what you ate in plain language; emealia estimates nutrition as honest ranges, shows the day as calm colored bands (no numbers, no red), and offers one small forward-looking tip. Intuition over arithmetic.
emealia is an AI nutrition companion sold as a mobile-first PWA (no App Store) in Germany (primary) and the USA (simultaneous), all copy in EN + DE, GDPR-first. The base product is one version, one price per market — a free trial converting to a single paid subscription. There is no freemium tier and the core loop is never gated.
Free trial → one paid subscription. No freemium, no tiers, no micro-transactions (Stripe's fixed fee kills €1 economics). An optional emealia Plus add-on may come later.
DE (primary launch) + US (simultaneous). EN + DE copy everywhere. Locale drives units, reference values (DGE/USDA), currency, and AI output language.
≈ 13–15 paying users cover the ~$56–63/mo fixed infra + AI at 100 users. Beta infra runs ~$30–35/mo.
BillingCatalogs — never hardcoded in the frontend; no GET /config/prices endpoint exists, contrary to earlier drafts of this doc)| Market | Trial | Monthly | Annual | Save |
|---|---|---|---|---|
| Germany | 30 free AI estimates · all features · no card | €5.99 | €39.99 | −44% |
| USA | 30 free AI estimates · all features · no card | $6.99 | $44.99 | −46% |
Annual is the checkout default with a secondary monthly toggle. Price is constant (founder decision 2026-07-03): one price per market, no retention/win-back discount, no lower price on the cancel screen. Trial-CTA subline is always "All features, one plan, forever."
The trial is credit-based, not time-based: usage_accounts.trial_credits_granted defaults to 30 (admin-configurable, admin.toml [usage] trial_credits), decremented one-per-AI-estimate in trial_credits_used. Enforcement lives in the LogMeal use case via UsageMeter.check_entitlement()/check_and_reserve() before any AI call → EntitlementError → HTTP 402 once credits are exhausted and no subscription is active. Read access (history, stats, profile) stays open after exhaustion; only new AI estimation is blocked. A per-user daily cap (20/day) and a global daily cost cap (13,333/day) apply on top, both admin-configurable. Note: the boundary docstring (UsageMeter) and the CLAUDE.md root context still describe the earlier 168-hour clock-based trial (trial_started_at + 168h) — that model is superseded and not what the shipped SqlAlchemyUsageMeter implements; treat the credit model above as authoritative.
D7 retention ≥25% (min) / ≥30% / ≥38% (great). Trial→paid ≥10% / ≥12% / ≥15%. Premium-candidate usage is instrumented invisibly from day 1 via UsageMeter.
D7 <20% after 200 users → stop acquisition, redesign the core loop. AI cost >€0.08/user/month → audit every AI call; something is leaking. Paid ads only once D30 ≥ 15%.
emealia Plus (post-launch, gated on D7 ≥ 30%). A recurring add-on layer — recipe suggestions, photo logging, deep micros, advanced stats + export, "level up a meal". It may only ever add new surfaces and must never gate the core loop or nutrient data. Nothing about Plus is built before that retention gate clears.
Every use case, flow and PR is checked against these. They are the product, not preferences.
| # | Rule | How it shows up in the flows below |
|---|---|---|
| 1 | No red in nutrition feedback. Ever. | Band color = category, never judgment. Failed estimates become a calm needs-confirmation state, not an error. |
| 2 | No numbers on the main daily view. | Today shows bands + qualitative labels only; ranges/numbers live one tap away in All-details and on the meal detail. |
| 3 | Free trial, not freemium. | The whole loop is in the base product; Plus can only add surfaces later. |
| 4 | AI free-text logging is the only primary input. | One sentence in; no barcode, no weighing, no database hunting. |
| 5 | Value before registration. | Pre-signup demo = hardcoded data, zero AI calls, zero cost. |
| 6 | No nutrition state may look like a failure. | Technical status can be shown transparently ("still estimating"); a light-eating day is never framed as bad. |
Curious, unregistered. Sees the demo day to understand the product before committing. Zero AI cost, zero abuse surface. Real usage begins only after registration.
Registered, has picked goals and (optionally) body data. Logs real meals; the credit-based trial and AI cost meter run. Wants to see whether daily bands become a habit.
Converted. Full loop: log, review, history, statistics, coaching. The retention target audience — D7/D30 measured here.
The async worker draining AI jobs, the timing engine deciding when a tip or review may fire, and retention/cost instrumentation. Acts on behalf of the loop, never spams the user (silence is default).
A warm companion in du-form (German). Never judges, always forward-looking; honest about estimates ("roughly 550–650, close enough to place it", not "612 kcal"). The companion voice is set in Newsreader italic; ordinary UI labels stay in Hanken Grotesk.
Who acts, what emealia does for them, and the external systems reached only through boundaries. The SDK/driver for each system lives solely inside its integration module — swapping a provider is a one-line change in wiring.py.
The canonical list. Each row links a business goal to its primary actor, trigger, and the endpoints that serve it. Detailed flows follow in §06–§15.
| ID | Use case | Actor | Trigger | Primary endpoints |
|---|---|---|---|---|
| UC-01 | Understand the product before signup | Visitor | Opens demo | ?demo=1 (frontend fixtures, no API) |
| UC-02 | Register & set goals + optional body data | Visitor→Trial | Taps "start" | GET /auth/google/login → GET /auth/google/callback, POST /consents/body-health-data |
| UC-03 | Log a meal from free text | Trial/Sub | Types a sentence | POST /meals → poll → /save |
| UC-04 | Review the glass-box estimate | Trial/Sub | Draft ready | GET /meals/{id}/draft |
| UC-05 | Refine / re-estimate a draft once | Trial/Sub | Wants to adjust | POST /meals/{id}/reestimate |
| UC-06 | Reuse a confirmed dish (0 AI) | Trial/Sub | Repeats a meal | POST /meals (memory hit), GET /meals/suggestions |
| UC-07 | Read the day as bands | Trial/Sub | Opens Today | GET /daily |
| UC-08 | See numbers & transparency | Trial/Sub | Taps into details | GET /daily/{day}/details |
| UC-09 | Add day / activity context | Trial/Sub | Logs how the day felt | PUT /daily/{day}/context, /activity-context |
| UC-10 | Browse history & favourites | Trial/Sub | Opens History | GET /meals?date= |
| UC-11 | Receive a gentle tip / review | System→User | Timing gate fires | coaching (UsageMeter-capped AI) |
| UC-12 | See trends over time | Trial/Sub | Opens Statistics | period summaries |
| UC-13 | Start trial & subscribe | Trial→Sub | Trial expiry / upgrade | Stripe · UsageMeter · HTTP 402 |
| UC-14 | Install & use offline | Trial/Sub | Adds to home screen | PWA manifest · SW · offline queue |
| UC-15 | Export / delete my data | Trial/Sub | Privacy request | GDPR erasure / export (Epic 15) |
The signature interaction and the only primary input. It is async from day 1: the estimator never blocks the HTTP response. The contract is stable across the phase-1 in-process worker and the phase-2 SQS + Lambda worker — the frontend is never touched by that swap.
POST /meals → 202 draft + job → worker → poll → POST /meals/{id}/save.meal_type (never AI-derived; defaults to other). POST /meals {text, meal_type, locale, log_date?}.LogMeal verifies body-health consent, validates the backdate window (≤7 days, no future), and calls UsageMeter.check_entitlement() — the trial/subscription gate.{status:"completed", reused:true} — the client skips polling.check_and_reserve() meters one AI call, a draft meal + an ai_jobs row are written, and the API returns 202 {meal_id, status:"processing"} immediately.AiEstimator boundary. See §07 for the full AI pipeline. Provider latency/failure becomes retry state, never user-facing red.GET /meals/{id}/draft until completed, failed, or needs_user_confirmation. The pre-save "meal log detail" screen shows the estimate with an explicit "nothing saved yet".POST /meals/{id}/save {meal_type} flips draft → logged, rebuilds daily_summaries, and records the confirmed dish for reuse. Only now does the meal count toward the day and appear in History.logged on reconnect (§15).validate_log_date.reestimate_count is returned so the button disables (§09).EntitlementError → HTTP 402; reads stay open.Implementation. backend/app/features/log_meal.py, save_meal.py, reestimate_draft.py; router backend/app/api/meals.py; core core/log_meal.py, core/save_meal.py, core/backdate.py. Frontend feature frontend/src/features/log-meal/ and meal-detail/.
The product promise depends on free text staying natural and estimates staying honest. The AI estimates; the application validates, maps, formats and interprets. The meal text is always data, never instructions. Prompts are versioned data (a snippet library), not hardcoded strings — changing a prompt means editing a data file and bumping its version.
The system prompt is composed per call from a snippet library, with constants (allowed units, portion descriptors, title/tag caps) injected at build time:
build_parse_meal_prompt(locale) → parse_meal.md
+ voice_tone.{en|de}.md
+ title_tag.{en|de}.md
+ user_context.md
prompt_version = "meal-estimate-v1"
The user message carries prompt_version, locale, and the HTML-stripped, length-capped meal text inside an <meal_input>…</meal_input> wrapper the model is instructed to treat as data — the injection-defense seam.
Estimation is a single Anthropic Tool Use call through the AiEstimator boundary: temperature=0, max_tokens=1200, tool_choice pinned to the meal_estimate tool, and the SDK's own retries disabled (max_retries=0) because retries are our concern, not the transport's. The tool schema constrains the JSON:
| Field | Meaning |
|---|---|
portion_size | A structured Amount for the whole meal — {number, attribute|null, unit} against an enum-constrained unit vocabulary (g, ml, cup, bowl, portion, handful, …) plus an optional size attribute (small/large/…). Never a free-text string; a size word is used only if the user gave one. |
ingredients[] | The single merged ingredient list the estimate depends on — always ≥1, unlimited. {text_en, text_de, amount: Amount|null, assumed, dominating}; assumed=false only for what the user stated explicitly (name and amount). ≤5 marked dominating. Replaces the earlier separate products[]/assumptions[] split (redesign 18.11) — ingredients and assumptions were always one concept. |
ranges | A required object keyed by every canonical nutrient (core/nutrients.py, all 32) — {min, max} per key, both nullable. Making the full key set mandatory (rather than a variable-length list) closes the "sugar appears then vanishes between calls" drift (redesign increment A, PR #208). |
title / tag | Cosmetic labels, capped at 60 / 24 chars. |
confidence_level | AI-supplied high · medium · low; the domain adds a fourth state, pending, before a job resolves — never returned by the tool call itself. Drives whether the meal contributes to the day's ranges. |
In ai_estimate_mapping.py, two field classes get two policies:
title, tag): wrong type or over cap → set to None, record a PII-free Violation, keep the meal. A retry would buy only a nicer label, so it is dropped, never re-sent. Violations are logged and counted for alerting.Amount objects at the schema level, so the model can't hand back a free-text unit; an amount the schema can't fit downgrades confidence_level to low rather than guessing.MealEstimateStructureError → the model is re-asked once, fed a compact reason derived from its own output (never the user's text).The parsed estimate passes through validate_meal_plausibility and enforce_assumption_language — assumptions must read as natural German for de* and natural English for en*, regardless of the meal's input language. A language failure also triggers the single retry.
Ranges + ingredients written to the draft. On the day, midpoints sum linearly and uncertainties combine by root-sum-square (§08). This quadratic aggregation is the honesty promise, not a bug.
After two validation failures, soft_fallback_after_validation_failures returns needs_user_confirmation at low confidence with a calm, editable message — "This one is best checked once. You can confirm it or adjust the details." Never a red error.
Cost & safety. Meal parsing and bounded coaching are the only routine paid calls. Every call is schema-validated, idempotent, metered and capped per user/month via UsageMeter. Cosmetic-rule violations are counted and alerted. Memory reuse (≥3 confirmations) removes the AI cost entirely for repeat dishes. The Anthropic monthly spend cap must be set in the Console before any prod key is used.
Implementation. integrations/anthropic_estimator.py (tool use + retry), integrations/fake_ai_estimator.py (default/tests), integrations/ai_estimate_mapping.py + ai_language.py (validation), core/meal_estimation.py (status + soft fallback), core/meal.py (Ingredient, MealEstimate), core/amount.py (Amount, unit/attribute enums), prompts/ (snippet library + builder). Boundary: boundaries/ai_estimator.py. Skill: ai-prompt-engineering.
In-flight. The structured-amounts redesign (Epic 18) landed the AI-seam and DB-native shapes above (18.10–18.12, merged) behind a write/read adapter that keeps the POST/GET /meals… wire contract and OpenAPI byte-identical for now. Story 18.13 (open PR) removes that adapter so portion_size/ingredients flow to the frontend natively — the endpoint table in §17 and the wire shapes implied by §09 still describe the pre-18.13 contract until it merges.
The signature surface. A band gives a feel for the day, not a sum. Color says which nutrient, never good/bad; status is carried by the text label only. No numbers on Today (non-negotiable #2); numbers live one tap away in All-details.
daily_summaries → band facts → versioned state mapping → localized label.aggregate_daily_meals sums logged meals for the day. Drafts are excluded; pending / low-confidence meals contribute zero to the ranges and instead increment a calm "still estimating" count (non-negotiable #6).σ = √Σ(halfwidth²) (root-sum-square) — not naive addition.daily_summaries on every save and compared to the persisted user_targets ideal zone [min, max] (DGE/USDA reference; §10).BandFacts reduces the band to objective booleans: no_data, end_below_zone, start_above_line, after_pivot (before/after 15:00).classify_band_state runs a versioned, ordered rule table (first match wins) over those facts and returns an opaque state key. The evaluator knows the rule shape, never what a key means.Default 5 bands (energy, protein, carbs, fat, fiber); the user can add up to 10 from the full nutrient/vitamin/mineral set — this is how non-weight goals (e.g. "watch iron") become visible, without new schema. The range boundary marker is drawn above the fill (z-index) so the ideal zone is never occluded when an estimate overshoots.
A pure function of the day's energy-band position, used only for History calendar dots and statistics distribution — never as a fill-color judgment.
Implementation. core/daily_summary.py (RSS), core/band_states.py (facts + rule table), core/day_level.py, features/build_daily_view.py, integrations/sqlalchemy_daily_summary.py. Frontend features/today/ + all-details/. Research: docs/research/band-label-mapping.md. Skill: design-system-tokens.
The glass box. A logged or draft meal shows exactly what the estimate depends on so the user can trust or adjust it. Bands describe a day; numbers describe a meal — the meal detail may show honest ranges, but never a day-budget band.
POST /meals/{id}/reestimate) replaces a draft's estimate once, spending one metered AI call and passing an expected_version for optimistic concurrency. reestimate_count disables the button after the single beta re-estimate.GET /meals/{id} (logged) and GET /meals/{id}/draft (pre-save). The frontend ?demo=1 route uses fixture data for band-comprehension testing without any backend.Implementation. features/reestimate_draft.py, refine_meal.py; core/reestimate_meal.py, refine_meal.py, meal_plausibility.py. Frontend features/meal-detail/. Research: docs/research/meal-log-detail-bands.md.
Value before registration, then a low-friction path to goals, targets, a 30-estimate credit-based trial, and one paid plan.
The profile carries goals: list[Goal] (+ optional free text). The canonical set: understand eating habits · improve nutrient intake · healthier lifestyle · more energy · build muscle · lose weight · gain weight · watch specific nutrients · medically motivated. No "maintain weight" (implicit when neither lose nor gain is chosen); lose × gain is the only mutual exclusion. The internal goal_focus / goal_energy_direction flags were explicitly rejected — no unnecessary backend complexity.
BuildTargets (pure) runs on every profile change: Mifflin-St Jeor TDEE, a selected-goals energy band (lose→deficit, gain→surplus, else maintenance), missing body params substituted from versioned regional averages, and activity from a movement×sport PAL grid (two onboarding radios). It upserts user_targets; the frontend renders ideal-zone bars only from the stored numbers.
Hardcoded data, animated logo loader, pre-computed bands + one hardcoded tip. Zero AI call, zero cost, zero abuse surface. Demo data is a signed local session, purged after 30 days of inactivity, and is not migrated into a real account.
Implementation. Frontend features/onboarding/, features/demo/demoData.ts; stores onboardingProfile.ts, todayNutrients.ts. Backend features/body_health_consent.py. Targets spec: docs/nutrition/build-targets-spec.md. Skills: gdpr-compliance, i18n-localization.
GET /meals?date= lists a day's logged meals oldest-first; History calendar dots use the derived day level. Day/activity context (PUT /daily/{day}/context, /activity-context) adds optional calm context — mood, hunger, energy, rough activity — never judgment.
Currently a client-side localStorage stopgap (emealia.favourite-meals) until a backend favourite field/endpoint lands. Named as a stopgap in code.
Every confirmed dish is stored in the user's own words (meal_memory). A keyword match surfaces "From history" chips on the add-meal screen (GET /meals/suggestions). At ≥3 confirmations the dish re-logs instantly with zero AI cost — the draft lands complete and the client skips polling. Bounded recent history may inform coaching, but the product never promises the AI "learns over time".
Implementation. features/log_meal.py (memory shortcut), boundaries/meal_memory_store.py, integrations/sqlalchemy_meal_memory_store.py. Frontend features/history/; stores mealConfirmations.ts, favourites.ts.
Bounded, consented, forward-looking. Silence is the default — emealia never sends "you haven't logged in X days" messages. Coaching sends only selected goals, cooking level, nutrition-knowledge level, preferences/free-text context, visible/starred band states, and bounded logged-meal history. Calls are schema-validated, idempotent, metered and capped.
| Trigger | Timing rule | Guards |
|---|---|---|
| Daily tip | Fires when cumulative kcal today > 50% of the 7-day rolling median (floor 300 kcal) | Not before 10:00 local · hard cutoff 23:00 · 1/day · min. 3 logging days all-time |
| Every-other-day review | After every second active logging day (≤1 calendar-day gap) | Min. 2 calendar days between reviews |
Delivery piggybacks on natural app opens. The voice is the Newsreader-italic companion: observe, don't judge; always one small step that builds on what the user already does.
Implementation. Epic 07 (implementation-plan/07-coaching-suggestions.md). Research: docs/research/suggestion-timing-and-context.md. AI cost model shares the AiEstimator/UsageMeter seams as §07/§13.
UsageMeter against usage_accounts.trial_credits_granted/used (credit-based, default 30, admin-configurable) — not the clock-based trial_started_at + 168h model described elsewhere in older docs.check_entitlement() runs in LogMeal before any AI call. Expiry without a subscription → EntitlementError → HTTP 402. A separate per-user AI rate cap (check_and_reserve) protects cost.SubscriptionStatus): trialing · active · past_due · canceled. Payments own the transitions; the meter only interprets whether the next estimate is covered.GET /config/prices. Soft paywall: reads stay open, only new logging is gated.Implementation. boundaries/usage_meter.py, payment_service.py; core/usage.py; integrations/sqlalchemy_usage_meter.py. Tables usage_accounts, usage_user_days, usage_global_days. Epic 09. Skill: security-hardening.
Period summaries and a range-capable statistics chart — the honest ranges carry through to trends, so a period never collapses to a single fake-precise number. Distribution uses the derived day level, never red. The frontend statistics screen is currently a placeholder over recent backend groundwork.
Implementation. Epic 10 (implementation-plan/10-reviews-statistics.md). Frontend features/statistics/. Aggregation reuses the RSS math from §08.
Mobile-first PWA, no App Store. Installable via a hardened web app manifest (story 12.1), a service worker, an offline meal queue, and a performance budget. A meal logged offline queues locally and auto-commits to logged on reconnect — no error wall, ever.
Implementation. frontend/src/pwa/manifest.ts, service worker + Vite PWA config. Epic 11 (implementation-plan/11-pwa-offline.md). Skill: frontend-vue-pwa.
DPA with Anthropic, consent flows (append/revoke ledger in consents, one active per type), EU data residency (RDS Frankfurt), export, erasure (cascades on user), retention. Free-text meals carry an Art. 9 posture; body/health data is erased when consent is revoked. Impressum §5 DDG required for DE.
Headers/CSP, CORS, CSRF, rate limits (api/rate_limit.py, security_headers.py), tenancy isolation (every read scoped to the resolved user), input validation, secrets hygiene (OIDC, no long-lived keys), SAST, pen test. Anything touching identity, money, quota, AI spend, consent or protected data lives on the backend — frontend storage is never a trust boundary.
Structured PII-safe logs, Sentry (scrubbed), 5 product metrics, consent-gated PostHog analytics (no PII without consent), backups, accessibility (WCAG AA), runbooks, launch sign-off. AI cosmetic-rule violations are counted and alerted.
Full string externalization (locales/en.json, de.json); locale drives units, reference values (DGE/USDA, salt), currency and AI output language. Market behavior is admin-configured (admin.toml), not embedded in a screen. DE voice QA gate.
Launch blockers. Epics 15 (GDPR), 16 (Security), 17 (Observability/QA) run in parallel from week 1 and must all be green before shipping. Two founder-only human gates: G1 band comprehension (≥3/5 testers unaided) before Epic 03 UI is finalized; G2 AI calibration (30 real meals DE+US, correction rate <40%) before scaling acquisition.
The current API surface (backend app/api/). Meal logging is async from day 1; the frontend consumes a typed client generated from the backend OpenAPI export.
| Method & path | Use case | Notes |
|---|---|---|
GET /health | Liveness | Infra probe. |
POST /meals | Log a meal (UC-03) | 202 {meal_id, status:"processing"}; reused:true on memory hit. Runs consent + backdate + entitlement. |
GET /meals?date= | History (UC-10) | Logged meals for one local day, oldest first. Read, always open, tenancy-scoped. |
GET /meals/{id} | Meal detail (UC-04) | One logged meal (glass-box read). |
GET /meals/{id}/draft | Poll draft (UC-04) | Pre-save estimate; completed | pending | needs_user_confirmation | failed. |
GET /meals/suggestions | Reuse (UC-06) | ≤5 "from history" chips for a meal_type, de-duplicated. |
POST /meals/{id}/save | Save (UC-03) | draft → logged; rebuilds daily summary; records reuse. |
POST /meals/{id}/reestimate | Refine (UC-05) | One metered re-estimate; optimistic expected_version. |
GET /daily | Today bands (UC-07) | Currently a mock route under MOCK_MODE; the real builder replaces it. |
GET /daily/{day}/details | All-details (UC-08) | Numeric transparency for one day. |
GET · PUT /daily/{day}/context | Day context (UC-09) | Mood/hunger/energy tags. |
GET · PUT /daily/{day}/activity-context | Activity context (UC-09) | Rough activity turnover ranges. |
GET /statistics/nutrition | Statistics (UC-12) | Min/max range chart per nutrient over today · week · month; percent-of-target with gap-filling, has_reference:false instead of a fabricated 0% when a nutrient has no target. |
GET · DELETE /body-health-data | Profile / erasure (UC-02, UC-15) | Body/health data + consent-linked deletion. |
POST /demo-sessions | Demo (UC-01) | Signed local demo session; no AI, no cost. |
Macros and micros are stored as individual columns (not JSONB) with CHECK constraints, so statistics can aggregate any nutrient. One storage tier, columns only. daily_summaries is pre-computed. Meals follow a draft → logged lifecycle.
| Table | Owns |
|---|---|
users | Identity anchor: id, timezone, timestamps. Day boundary via Clock.local_date. |
consents | GDPR consent ledger — append/revoke, one active per user/type; cascades with erasure. |
meals | Raw text, AI/display metadata, status/lifecycle/version, structured portion_size (JSONB Amount) and a merged ingredients list (JSONB, replacing the old separate products/assumptions columns as of 18.12), macro/micro/detail nutrient ranges with plausibility checks. |
meal_memory | User-scoped canonical text + confirmed nutrient/product/assumption shape for 0-AI reuse. |
ai_jobs | Async estimation queue: pending/running/succeeded/failed, attempts, next-attempt time, error metadata. |
daily_summaries | Per-user/day aggregate ranges + pending count. Rebuilt on every save/change. |
user_targets | Persisted BuildTargets output — ideal zones the bands render against. |
usage_accounts · usage_user_days · usage_global_days | Trial credits, subscription status, per-user + global daily AI reservations. |
daily_context_tags · activity_context | Optional day context tags and rough activity turnover ranges. |
Implementation. ORM rows in integrations/models.py; migrations in backend/alembic/versions/. Never put ORM in core/. Nutrient DDL + display rules: docs/nutrition/nutrient-reference.md. Skill: database-migrations.
Current implementation state (July 2026). See the developer onboarding for the live mock/real map and the app repo's implementation-plan/ for story-level acceptance criteria.
| Feature / use case | Status | Note |
|---|---|---|
| Meal logging (UC-03) & save | Real async contract | Draft → poll → save is real DB-backed; estimator is the fake unless ALLOW_REAL_AI is set. |
| AI communication pipeline (§07) | Built | Tool use, two-layer validation, retry, soft fallback, violation counting all present. All-32 required ranges object + merged ingredients/structured Amount shipped in the AI seam and DB (18.10–18.12); wire-contract cutover (18.13) still an open PR. |
| Memory reuse (UC-06) | Built | 0-AI re-log path in LogMeal; suggestions endpoint live. |
| Meal detail / re-estimate (UC-04/05) | Real read + refine | Demo fixtures via ?demo=1 for comprehension tests. |
| Today bands (UC-07) | Mock-backed | GET /daily exists only under MOCK_MODE; real builder pending. Band-state classifier is real (core/band_states.py). |
| All-details (UC-08) | Mock-backed | Numeric transparency from the same mock day variant. |
| Day / activity context (UC-09) | Backend-backed | Real stores, wired outside mock-only daily summary. |
| History / favourites (UC-10) | Hybrid | Reads real meals; favourites is a localStorage stopgap. |
| Coaching (UC-11) | Planned | Epic 07 — timing rules specified; shares AI/meter seams. |
| Statistics (UC-12) | Built | Real GET /statistics/nutrition + a min/max range chart (today/week/month) on the Statistics tab (story 10.1). |
| Trial & payments (UC-13) | Boundaries first | UsageMeter/entitlement seams present; Stripe rollout env-gated. |
| PWA / offline (UC-14) | In progress | Manifest hardened (story 12.1); SW + offline queue underway. |
| GDPR / Security / Observability | Launch blockers | Epics 15/16/17 — parallel, must be green before shipping. |
Sources of truth. On any conflict: emealia-mgmt/ wins for concept/product; implementation-plan/ wins for what gets built. This page mirrors both for humans; story state and acceptance criteria live in the app repo.