BCE-based monorepo. FastAPI + Vue PWA. Beta: Lambda ARM + Lambda Web Adapter + Function URL + SQS worker + NAT instance (~$30-35/mo). Direct Anthropic API. No Bedrock, no App Runner.
| Layer | Choice | Swap path |
|---|---|---|
| API | Python 3.13 · FastAPI · Pydantic v2 · Alembic · asyncpg | — |
| Frontend | Node 24 LTS · Vue 3 · Vite · Tailwind 4 · PWA (no App Store) | — |
| Database | PostgreSQL 17 (→ 18 when GA) · RDS · db.t4g.micro | Supabase Postgres |
| AI | Anthropic API direct (DPA + ZDR + SCCs) · Haiku 4.5 parsing · Sonnet 4.6 review | Bedrock-EU via AiEstimator |
| Auth | Social sign-in only, no passwords · Google OAuth 2.0 Authorization Code via GoogleAuthBoundary (shipped, 7.1a) · Apple Sign-In planned (7.1b) · HS256 session cookie via JwtSessionCodec | Auth0 / Supabase Auth (swap = new AuthBoundary integration) |
| Payments | Stripe (EUR + USD) · Stripe Tax (EU VAT 19%, US sales tax) | — |
| Compute | Beta Lambda ARM + Lambda Web Adapter + Function URL · Prod ECS Express Mode · 0.25 vCPU / 512 MB | Fargate |
| Async jobs | SQS + Lambda worker (Python 3.13) in beta; BackgroundTasks only as local fallback | — |
| CDN | CloudFront (us-east-1 cert) + S3 private (OAC) | — |
| Secrets | AWS Secrets Manager · KMS | — |
| Networking | Beta VPC · NAT instance t4g.nano (~$7-9/mo incl. IPv4 + tiny EBS) · Prod VPC endpoints for AWS APIs plus explicit egress plan for third-party APIs | — |
| IaC | Terraform modular (network / ecs-express / rds / sqs / lambda / s3-cloudfront / observability) | — |
| CI/CD | GitHub Actions · OIDC (keyless) · staging → prod manual gate | — |
| Monitoring | CloudWatch Logs/Alarms · SNS email · Sentry (fe+be) | — |
| Region | eu-central-1 (Frankfurt) · all data EU-resident | — |
BCE = Boundary / Control / Entity. All external services and persistence are behind a boundary Protocol interface — swappable, mockable, testable. CLAUDE.md's "9 boundaries" is the founding shortlist from story 0.3; the shipped code has grown it to 21 as features landed (each still following the one-role-noun naming rule — see Architecture). Full detail + integration mapping: architecture.html · data-model.html.
| Boundary | Hides | Integration(s) |
|---|---|---|
| AiEstimator | Anthropic Claude — meal parsing + tip/suggestion generation | AnthropicAiEstimator · FakeAiEstimator |
| MealStore | Read-side meal persistence | SqlAlchemyMealStore |
| MealLogStore | Atomic draft-meal + AI-job creation (the sync half of LogMeal) | SqlAlchemyMealLogStore |
| MealSaveStore | Atomic draft→logged save + daily-summary recompute | SqlAlchemyMealSaveStore |
| MealReestimateStore | Re-estimating a caller-owned draft (max 1×) | SqlAlchemyMealReestimateStore |
| MealRefineStore | Replacing a caller-owned logged meal's estimate + day-summary recompute | SqlAlchemyMealRefineStore |
| MealMemoryStore | Confirmed-dish reuse (meal_memory, 0-AI-call repeat logging) | SqlAlchemyMealMemoryStore |
| AiJobStore | Async estimation worker queue (ai_jobs) | SqlAlchemyAiJobStore |
| ProfileStore | Today-nutrient visibility + profile preferences | SqlAlchemyProfileStore |
| DailyViewStore | Read seam assembling one local day's band inputs | (integration lands with Epic 03) |
| DayContextStore | Optional per-day wellbeing context tags | SqlAlchemyDayContextStore |
| ActivityContextStore | Optional per-day movement/turnover context | SqlAlchemyActivityContextStore |
| AuthBoundary | Social identity provider OAuth 2.0/OIDC dance (Google now, Apple 7.1b) | GoogleAuthBoundary · FakeAuthBoundary |
| AuthUserStore | Claim-or-create on (auth_provider, provider_subject) | SqlAlchemyAuthUserStore |
| SessionCodec | Signs/reads the session-cookie token | JwtSessionCodec (HS256) |
| DemoSessionGateway | Pre-signup demo sessions (rate-limited per IP) | InMemoryDemoSessionGateway |
| PaymentService | Stripe — paid subscriptions only (trial is emealia-side via UsageMeter) | StripePaymentService · FakePaymentService |
| Emailer | Transactional email | (integration pending) |
| Clock | System time — testable via fake/fixed time | SystemClock |
| UsageMeter | Trial entitlement (check_entitlement) + per-user/global AI rate caps (check_and_reserve) | SqlAlchemyUsageMeter |
| ConsentStore | GDPR consent grant/revoke ledger | SqlAlchemyConsentStore |
POST /meals → UsageMeter.check_and_reserve(user) / check_entitlement(user) → EntitlementError → HTTP 402. The trial is credit-based, not clock-based: usage_accounts.trial_credits_granted (default 30, admin-configurable via admin.toml [usage] trial_credits) is consumed one-per-AI-estimate; subscription_status starts at TRIALING and moves to Stripe-backed states only on upgrade. A per-user daily cap (usage_user_days, default 20/day) and a global daily cost cap (usage_global_days, default 13,333/day) apply on top. Read endpoints (GET) are never gated — only new AI estimation triggers the check. Conflict: the UsageMeter boundary's own docstring and CLAUDE.md still describe a superseded "168h from first log" clock model — the shipped SqlAlchemyUsageMeter integration only implements credits.
| Decision | Choice | Reason |
|---|---|---|
| Production compute | ECS Express Mode | App Runner closed to new customers 30.04.2026; beta stays Lambda to avoid the ALB/ECS fixed floor |
| AI routing | Direct Anthropic API | Bedrock-EU not available at project start; direct API + DPA + ZDR + SCCs = GDPR-safe |
| NAT Gateway | Avoid in beta | NAT Gateway fixed cost is not justified; beta uses a tiny NAT instance for non-AWS API egress |
| Compute (beta) | Lambda ARM + Lambda Web Adapter | ALB ($17) + ECS ($10) + 6 VPC Endpoints ($44) = $71/mo fixed before first request; Lambda free tier covers all beta volume |
| Ingress (beta) | CloudFront Function URL + OAC | Reuses existing CloudFront distribution; $0 fixed cost vs $17 ALB; OAC signs requests so Function URL rejects all non-CF callers |
| NAT (beta) | NAT instance t4g.nano ~$7-9/mo incl. IPv4 + tiny EBS | Cheaper than NAT Gateway; needed for Anthropic, Google OAuth, Stripe, and Sentry from VPC Lambdas |
| Auth | Social sign-in only, no passwords (Google now via GoogleAuthBoundary, Apple 7.1b) | emealia never stores a password; account keyed on (auth_provider, provider_subject), not email; swap path = a new AuthBoundary integration (Auth0, Supabase Auth, …) |
| Trial model | Credit-based: 30 free AI estimates (admin-configurable), not a time window | 2026-07-07 market analysis superseded the earlier 168h clock-based trial; UsageMeter checks trial_credits_used < trial_credits_granted |
| AI model (parsing) | Claude Haiku 4.5 | ~$1/$5 per M tokens (in/out); fast; sufficient for meal parsing |
| AI model (review) | Claude Sonnet 4.6 | Higher quality for longer-form review tasks; one-off, not in critical path |
| Async API contract | 202 + polling | AI calls can take 3–8 s; 202 + GET /meals/{id} polling keeps frontend unblocked |
| Async impl — local fallback | BackgroundTasks | Useful for local development only; beta uses SQS + Lambda worker |
| Async impl — beta | SQS + Lambda worker | Retry, DLQ, burst scaling; zero frontend contract change (same 202/poll) |
| PostgreSQL version | 17 (→ 18 when GA) | Native JSON enhancements; pgvector for future meal memory |
| Python version | 3.13 | Free-threaded GIL option; latest asyncio improvements |
| Node version | 24 LTS | Long-term support; security patches until 2027 |
| Prompt architecture | Snippet library, versioned data | Composable snippets (voice/tone, title+tag limits, user context: goals + cooking + knowledge) assembled per call — parse, daily suggestion, review all share snippets |
| AI response fields | One JSON (Tool Use): ranges, assumptions, title ≤60, tag ≤24 | Cosmetic fields violating a rule are dropped, never re-sent (a retry costs a full parse and buys a label); violations are logged + counted + alerted |
| Nutrient targets | Pure BuildTargets, persisted in user_targets | Never computed on the fly: recalculated + saved on every profile change; missing body params substitute regional official averages (versioned fallback data) |
| Band labels | Backend maps percentages → text states | empty · getting_started · room_to_round_off · on_track · treat_today; mapping table versioned as data; recomputed into daily_summaries on every meal-log save |
| Prices | GET /config/prices — frontend never hardcodes a price | One prices object per market fills every placeholder (trial screen, paywall, checkout, cancel); one constant price per market |
| Suggestion context | Piggybacked server-side in the ai_jobs worker | Day's meals/nutrition assembled from DB when a meal-log completes — never re-sent by the client; reviews send the last 3 days (≤15 meals) |
| AI code review | Removed (2026-07-04) | Per-PR AI reviewer workflows were not cost-efficient; review is human (CODEOWNERS) with the review skills runnable manually; no ANTHROPIC_API_KEY GitHub secret needed |
The API contract is stable: beta uses SQS+Lambda, while local development can use BackgroundTasks. The frontend never needs to change.
POST /meals → 202 { meal_id, status: "processing" }
GET /meals/{id} → 200 { ..., status: "pending" | "completed" | "failed" | "needs_user_confirmation" }
GET /meals/{id} → 200 { ..., nutrient_range: {...} } ← when completed
GET /meals → 200 [ ... ] ← always open, no entitlement check
POST /meals → 402 { detail: "trial_expired" } ← after all 30 trial credits are used and no active subscription
BackgroundTasks → SQS+Lambda remains a backend-only swap. The ai_jobs table, status enum, and polling endpoint stay identical. Frontend ships once.
/api/* (OAC SigV4) → Lambda Function URL (IAM-auth) → API Lambda ARM 512 MB · health: /healthrds.force_ssl=1 · reserved_concurrency=10 caps DB connectionslambda update-function-code (API + worker) · S3 sync + CF invalidationemealia-beta-migrate Lambda invoke (same image, Alembic CMD override) · runs before API updateecs update-service → ecs wait services-stable · S3 sync + CF invalidationalembic upgrade head in deploy workflow before service updateeu-central-1, minimal tier. Add ~50% for staging parity.
| Service | What it does | Beta (Lambda) | → Production (ECS+ALB) |
|---|---|---|---|
| RDS db.t4g.micro, gp3, 20 GB, 1 AZ | Persistent PostgreSQL database | ~$15 | ~$15 |
| API compute | CPU/RAM for HTTP requests | $0 ¹ | ~$10 (ECS) |
| Ingress / load balancer | TLS termination + routing to API | $0 ¹ | ~$17 (ALB) |
| VPC Interface Endpoints | Private tunnels to AWS services | $0 (replaced) | ~$44 (6 × 1 AZ) |
| NAT instance t4g.nano | Outbound internet for VPC Lambdas → Anthropic/Google OAuth/Stripe/Sentry | ~$7-9 | production egress plan |
| Lambda worker, SQS | Async AI job processing + queue | $0 ¹ | ~$1 |
| CloudFront + S3 | Frontend CDN + Vue PWA bundle | ~$2 ¹ | ~$2 |
| ECR, CloudWatch Logs, Secrets Manager, Route 53 | Registry, logs, secrets, DNS | ~$6 | ~$6 |
| Infra total | ~$30-35/mo | ~$95/mo |
¹ Lambda free tier is permanent (not 12-month limited): 1M requests/mo + 400K GB-seconds/mo. At beta volume (100 users × 50 calls/day × 30 days = 150k requests, 15k GB-s) API Lambda costs $0. CloudFront free tier (1 TB egress + 10M req/mo) also permanent — $0 at beta scale.
| AI cost (variable) | |
|---|---|
| Haiku 4.5 input | $1.00 / 1M tokens |
| Haiku 4.5 output | $5.00 / 1M tokens |
| Per meal (500 in + 200 out tokens) | ~$0.0015 |
| 100 users × 3 meals/day | ~$14/mo |
| 500 users × 3 meals/day | ~$68/mo |
| 1,000 users × 3 meals/day | ~$135/mo |
| Break-even | Beta | Prod |
|---|---|---|
| Infra only (no AI) | ~5 @ €5.99 | ~16 @ €5.99 |
| Infra + AI at 100 users | ~7 @ €5.99 | ~18 @ €5.99 |
| Revenue @ 100 paying | ~€599/mo | |
| Lambda → ECS break-even: ~10–15M API req/mo (~3–5k active users at 100 calls/day) | ||
The beta Lambda path avoids the fixed ALB/ECS and NAT Gateway floor while keeping the production scaling path ready. Switch to ECS Express when traffic exceeds ~10M API requests/month or cold-start latency becomes user-visible. Always set the Anthropic spend cap before the first production key goes live — an infinite loop can burn $1k+ overnight.
| Layer | Mechanism |
|---|---|
| Auth tokens | Access token 15 min · Refresh 30-day rolling httpOnly Secure SameSite=Lax cookie |
| Demo session | Pre-signup demo token/cookie · no real user data · disposable |
| IP rate limit | ≤3 demo session/registration starts per IP per 24 h → 429 + Retry-After |
| API keys | Secrets Manager only · never in env vars committed to git · never logged |
| DB | Private subnet only · force SSL · RDS encryption at rest |
| AI data | Anthropic DPA + Zero Data Retention + SCCs · no meal data retained on Anthropic side |
| GDPR | ConsentStore boundary · right to erasure · export endpoint · EU-only data residency |
| Auth | Social sign-in only — emealia stores no password, ever; Google OAuth 2.0/OIDC tokens are exchanged server-side, never touched by the frontend |
| TLS | CloudFront (us-east-1 ACM) + Lambda Function URL (AWS-managed TLS) + forced HTTPS · HSTS header · Prod: adds ALB (eu-central-1 ACM) |
| Runtime | Pin | Note |
|---|---|---|
| Python | 3.13 | Lambda + ECS; free-threaded option available |
| Lambda Web Adapter | 0.9.0 | FastAPI-on-Lambda bridge (beta) — check GitHub releases for latest |
| Node | 24 LTS | Frontend build only; security patches until 2027 |
| PostgreSQL | 17 → 18 when GA | Native JSON; pgvector ready for meal memory |
| Claude Haiku | claude-haiku-4-5-20251001 | Meal parsing |
| Claude Sonnet | claude-sonnet-4-6 | Review tasks, PR summaries |
The actual monorepo layout (verified against the shipped code — the previous version of this page described an aspirational apps/api/apps/web shape that was never built).
emealia-app/ ← implementation monorepo
├── backend/
│ └── app/
│ ├── core/ ← pure domain: meal, band_states, billing, nutrients, targets, ...
│ ├── features/ ← use cases: LogMeal, SaveMeal, BuildDailyView, SocialSignIn, ...
│ ├── boundaries/ ← Protocol interfaces — 21 files, one per seam
│ ├── integrations/ ← adapters: SQLAlchemy stores, Anthropic, Stripe, Google OAuth, ...
│ │ └── models.py ← every SQLAlchemy ORM row (the only place they live)
│ ├── api/ ← FastAPI routers (thin) — meals, daily, auth, billing, consents, ...
│ ├── prompts/ ← versioned AI prompt snippets (data, not code)
│ ├── wiring.py ← composition root — the one file that assembles everything
│ ├── settings.py ← typed env config, fail-fast in prod/staging
│ └── admin_config.py ← typed loader for backend/config/admin.toml (product knobs, not secrets)
│ ├── alembic/versions/ ← migrations, one file per schema change
│ └── tests/ ← unit / feature / api / integration / contract
├── frontend/
│ └── src/
│ ├── features/ ← screen-owned views + composables (today, log-meal, history, ...)
│ ├── components/ ← shared visual primitives (bands, sheets, meal rows, ...)
│ ├── api/ ← typed client (openapi-fetch) + generated/schema.d.ts
│ ├── stores/ ← Pinia + localStorage stopgaps (favourites, onboarding profile, ...)
│ ├── locales/ ← en.json / de.json
│ ├── router/ ← named routes
│ └── design/tokens.ts ← source of truth for assets/tokens.css (generated)
├── infra/aws/ ← Terraform modules (network, lambda-api, rds, sqs, ecs-express, ...)
├── implementation-plan/ ← one file per epic — the build backlog, source of truth
├── docs/ ← architecture diagrams, ADRs, standards, research notes
├── .claude/skills/ ← executable standards (BCE, testing, security, GDPR, ...)
└── .github/workflows/ ← backend.yml, frontend.yml, security.yml, sonar.yml
emealia-web/ ← marketing + docs site (NOT a git repo — deployed via FTPS)
└── public/
├── prototype/ ← 9-screen clickdummy
├── documentation/ ← this docs site
└── (landing page, etc.)
emealia-mgmt/ ← authoritative concept/product docs (strategy, technical, design, onboarding)
See Architecture for the full BCE dependency rule and boundary↔integration map, and Data Model for every table.
| Gap | Priority |
|---|---|
Story 0.1: settings.py, main.py, wiring.py, /health, DB session factory | Blocker |
Real Google OAuth client id/secret for beta (implementation already shipped, story 7.1a — GoogleAuthBoundary/FakeAuthBoundary); Apple Sign-In (7.1b) not yet built | Blocker |
GET /config/prices referenced in CLAUDE.md/strategy docs does not exist — prices are resolved server-side per market locale via BillingCatalogs/Stripe price ids in POST /billing/checkout, not fetched by the frontend as a standalone object | Doc gap |
POST /meals/{id}/refine not wired to a router — RefineMeal feature + MealRefineStore boundary exist but no API endpoint calls them yet | Doc gap |
| Set Anthropic monthly spend cap in Console | Blocker |
| Sign Anthropic DPA before first EU user data sent to API | Blocker |
Add ANTHROPIC_API_KEY to GitHub repo secrets | Blocker |
Dockerfile: add Lambda Web Adapter COPY + ENV AWS_LWA_PORT=8080 + uvicorn CMD | Beta Blocker |
CloudFront /api/* behavior + OAC/SigV4 to the API Lambda Function URL | Beta Blocker |
| Terraform beta modules for Lambda API, migrate Lambda, worker Lambda, SQS, private RDS access, and NAT instance | Done |
SQLAlchemy pool_size=1, max_overflow=1 + reserved_concurrency=10 on API Lambda | Beta Blocker |
EC2 Auto Recovery alarm on NAT instance (StatusCheckFailed → recover action) | Beta Blocker |
| Add async SQS+Lambda pipeline as new stories in backlog | Soon |
| Add deploy workflow story (currently missing from CI epics) | Soon |
| RDS Proxy — add when Lambda concurrency causes connection churn (>30 concurrent) | Later |
| WAF before CloudFront (production) | Later |
| Cross-region backup copy + documented restore drill (Epic 15.5) | Later |