Technical Architecture

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.

Stack at a glance

LayerChoiceSwap path
APIPython 3.13 · FastAPI · Pydantic v2 · Alembic · asyncpg
FrontendNode 24 LTS · Vue 3 · Vite · Tailwind 4 · PWA (no App Store)
DatabasePostgreSQL 17 (→ 18 when GA) · RDS · db.t4g.microSupabase Postgres
AIAnthropic API direct (DPA + ZDR + SCCs) · Haiku 4.5 parsing · Sonnet 4.6 reviewBedrock-EU via AiEstimator
AuthSocial 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 JwtSessionCodecAuth0 / Supabase Auth (swap = new AuthBoundary integration)
PaymentsStripe (EUR + USD) · Stripe Tax (EU VAT 19%, US sales tax)
Compute Lambda ARM + Lambda Web Adapter + Function URL  ·  Prod ECS Express Mode · 0.25 vCPU / 512 MBFargate
Async jobsSQS + Lambda worker (Python 3.13) in beta; BackgroundTasks only as local fallback
CDNCloudFront (us-east-1 cert) + S3 private (OAC)
SecretsAWS Secrets Manager · KMS
Networking 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
IaCTerraform modular (network / ecs-express / rds / sqs / lambda / s3-cloudfront / observability)
CI/CDGitHub Actions · OIDC (keyless) · staging → prod manual gate
MonitoringCloudWatch Logs/Alarms · SNS email · Sentry (fe+be)
Regioneu-central-1 (Frankfurt) · all data EU-resident

The Boundaries (BCE)

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.

BoundaryHidesIntegration(s)
AiEstimatorAnthropic Claude — meal parsing + tip/suggestion generationAnthropicAiEstimator · FakeAiEstimator
MealStoreRead-side meal persistenceSqlAlchemyMealStore
MealLogStoreAtomic draft-meal + AI-job creation (the sync half of LogMeal)SqlAlchemyMealLogStore
MealSaveStoreAtomic draft→logged save + daily-summary recomputeSqlAlchemyMealSaveStore
MealReestimateStoreRe-estimating a caller-owned draft (max 1×)SqlAlchemyMealReestimateStore
MealRefineStoreReplacing a caller-owned logged meal's estimate + day-summary recomputeSqlAlchemyMealRefineStore
MealMemoryStoreConfirmed-dish reuse (meal_memory, 0-AI-call repeat logging)SqlAlchemyMealMemoryStore
AiJobStoreAsync estimation worker queue (ai_jobs)SqlAlchemyAiJobStore
ProfileStoreToday-nutrient visibility + profile preferencesSqlAlchemyProfileStore
DailyViewStoreRead seam assembling one local day's band inputs(integration lands with Epic 03)
DayContextStoreOptional per-day wellbeing context tagsSqlAlchemyDayContextStore
ActivityContextStoreOptional per-day movement/turnover contextSqlAlchemyActivityContextStore
AuthBoundarySocial identity provider OAuth 2.0/OIDC dance (Google now, Apple 7.1b)GoogleAuthBoundary · FakeAuthBoundary
AuthUserStoreClaim-or-create on (auth_provider, provider_subject)SqlAlchemyAuthUserStore
SessionCodecSigns/reads the session-cookie tokenJwtSessionCodec (HS256)
DemoSessionGatewayPre-signup demo sessions (rate-limited per IP)InMemoryDemoSessionGateway
PaymentServiceStripe — paid subscriptions only (trial is emealia-side via UsageMeter)StripePaymentService · FakePaymentService
EmailerTransactional email(integration pending)
ClockSystem time — testable via fake/fixed timeSystemClock
UsageMeterTrial entitlement (check_entitlement) + per-user/global AI rate caps (check_and_reserve)SqlAlchemyUsageMeter
ConsentStoreGDPR consent grant/revoke ledgerSqlAlchemyConsentStore
Trial enforcement flow (corrected — see strategy.html)

POST /mealsUsageMeter.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.

Key technical decisions

DecisionChoiceReason
Production computeECS Express ModeApp Runner closed to new customers 30.04.2026; beta stays Lambda to avoid the ALB/ECS fixed floor
AI routingDirect Anthropic APIBedrock-EU not available at project start; direct API + DPA + ZDR + SCCs = GDPR-safe
NAT GatewayAvoid in betaNAT Gateway fixed cost is not justified; beta uses a tiny NAT instance for non-AWS API egress
Compute (beta)Lambda ARM + Lambda Web AdapterALB ($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 + OACReuses 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 EBSCheaper than NAT Gateway; needed for Anthropic, Google OAuth, Stripe, and Sentry from VPC Lambdas
AuthSocial 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 modelCredit-based: 30 free AI estimates (admin-configurable), not a time window2026-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.6Higher quality for longer-form review tasks; one-off, not in critical path
Async API contract202 + pollingAI calls can take 3–8 s; 202 + GET /meals/{id} polling keeps frontend unblocked
Async impl — local fallbackBackgroundTasksUseful for local development only; beta uses SQS + Lambda worker
Async impl — betaSQS + Lambda workerRetry, DLQ, burst scaling; zero frontend contract change (same 202/poll)
PostgreSQL version17 (→ 18 when GA)Native JSON enhancements; pgvector for future meal memory
Python version3.13Free-threaded GIL option; latest asyncio improvements
Node version24 LTSLong-term support; security patches until 2027
Prompt architectureSnippet library, versioned dataComposable snippets (voice/tone, title+tag limits, user context: goals + cooking + knowledge) assembled per call — parse, daily suggestion, review all share snippets
AI response fieldsOne JSON (Tool Use): ranges, assumptions, title ≤60, tag ≤24Cosmetic 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 targetsPure BuildTargets, persisted in user_targetsNever computed on the fly: recalculated + saved on every profile change; missing body params substitute regional official averages (versioned fallback data)
Band labelsBackend maps percentages → text statesempty · getting_started · room_to_round_off · on_track · treat_today; mapping table versioned as data; recomputed into daily_summaries on every meal-log save
PricesGET /config/prices — frontend never hardcodes a priceOne prices object per market fills every placeholder (trial screen, paywall, checkout, cancel); one constant price per market
Suggestion contextPiggybacked server-side in the ai_jobs workerDay'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 reviewRemoved (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

Async API contract

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
Swap invariant

BackgroundTasks → SQS+Lambda remains a backend-only swap. The ai_jobs table, status enum, and polling endpoint stay identical. Frontend ships once.

AWS topology

Beta active

Region
eu-central-1 (Frankfurt) — all data EU-resident
VPC
Public + private subnets across 2 AZs · NAT instance t4g.nano in public subnet for third-party HTTPS egress
Frontend
CloudFront (us-east-1 ACM cert) → S3 private (OAC) · SPA fallback 403/404→/index.html
API
CloudFront /api/* (OAC SigV4) → Lambda Function URL (IAM-auth) → API Lambda ARM 512 MB · health: /health
Database
RDS PostgreSQL 17 · db.t4g.micro · private subnet · rds.force_ssl=1 · reserved_concurrency=10 caps DB connections
Async jobs
SQS (main + DLQ, maxReceiveCount=5) → Worker Lambda ARM (Python 3.13, 512 MB, 60 s) in VPC
Secrets
AWS Secrets Manager + KMS · injected as Lambda env vars at deploy time
Deploy
GitHub OIDC → ECR push → lambda update-function-code (API + worker) · S3 sync + CF invalidation
Migrations
emealia-beta-migrate Lambda invoke (same image, Alembic CMD override) · runs before API update
Observability
CloudWatch Logs (30 d retention) · Alarms: SQS DLQ>0, Lambda errors/throttles, RDS CPU/storage · SNS → email
Cost control
AWS Budgets (50/80/100% alerts) · Cost Anomaly Detection · Anthropic spend cap set before prod key active

Production target

VPC
Public + private subnets across 2 AZs · VPC endpoints for AWS APIs plus explicit egress plan for Anthropic, Google OAuth, Stripe, and Sentry
API
ALB (443/ACM eu-central-1) → ECS Express Mode (0.25 vCPU / 512 MB) · health: /health
Deploy
GitHub OIDC → ECR push → ecs update-serviceecs wait services-stable · S3 sync + CF invalidation
Migrations
One-off ECS task alembic upgrade head in deploy workflow before service update
All else
Same as Beta — same VPC, RDS, SQS, Worker Lambda, Secrets, Observability, Cost control

Real cost breakdown

eu-central-1, minimal tier. Add ~50% for staging parity.

ServiceWhat it doesBeta (Lambda)→ Production (ECS+ALB)
RDS db.t4g.micro, gp3, 20 GB, 1 AZPersistent PostgreSQL database~$15~$15
API computeCPU/RAM for HTTP requests$0 ¹~$10 (ECS)
Ingress / load balancerTLS termination + routing to API$0 ¹~$17 (ALB)
VPC Interface EndpointsPrivate tunnels to AWS services$0 (replaced)~$44 (6 × 1 AZ)
NAT instance t4g.nanoOutbound internet for VPC Lambdas → Anthropic/Google OAuth/Stripe/Sentry~$7-9production egress plan
Lambda worker, SQSAsync AI job processing + queue$0 ¹~$1
CloudFront + S3Frontend CDN + Vue PWA bundle~$2 ¹~$2
ECR, CloudWatch Logs, Secrets Manager, Route 53Registry, 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-evenBetaProd
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)
Cost watch

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.

Security layer

LayerMechanism
Auth tokensAccess token 15 min · Refresh 30-day rolling httpOnly Secure SameSite=Lax cookie
Demo sessionPre-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 keysSecrets Manager only · never in env vars committed to git · never logged
DBPrivate subnet only · force SSL · RDS encryption at rest
AI dataAnthropic DPA + Zero Data Retention + SCCs · no meal data retained on Anthropic side
GDPRConsentStore boundary · right to erasure · export endpoint · EU-only data residency
AuthSocial sign-in only — emealia stores no password, ever; Google OAuth 2.0/OIDC tokens are exchanged server-side, never touched by the frontend
TLSCloudFront (us-east-1 ACM) + Lambda Function URL (AWS-managed TLS) + forced HTTPS · HSTS header · Prod: adds ALB (eu-central-1 ACM)

Version pins

RuntimePinNote
Python3.13Lambda + ECS; free-threaded option available
Lambda Web Adapter0.9.0FastAPI-on-Lambda bridge (beta) — check GitHub releases for latest
Node24 LTSFrontend build only; security patches until 2027
PostgreSQL1718 when GANative JSON; pgvector ready for meal memory
Claude Haikuclaude-haiku-4-5-20251001Meal parsing
Claude Sonnetclaude-sonnet-4-6Review tasks, PR summaries

Repo structure

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.

Open gaps / next actions

GapPriority
Story 0.1: settings.py, main.py, wiring.py, /health, DB session factoryBlocker
Real Google OAuth client id/secret for beta (implementation already shipped, story 7.1a — GoogleAuthBoundary/FakeAuthBoundary); Apple Sign-In (7.1b) not yet builtBlocker
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
POST /meals/{id}/refine not wired to a router — RefineMeal feature + MealRefineStore boundary exist but no API endpoint calls them yet
Set Anthropic monthly spend cap in ConsoleBlocker
Sign Anthropic DPA before first EU user data sent to APIBlocker
Add ANTHROPIC_API_KEY to GitHub repo secretsBlocker
Dockerfile: add Lambda Web Adapter COPY + ENV AWS_LWA_PORT=8080 + uvicorn CMDBeta Blocker
CloudFront /api/* behavior + OAC/SigV4 to the API Lambda Function URLBeta Blocker
Terraform beta modules for Lambda API, migrate Lambda, worker Lambda, SQS, private RDS access, and NAT instanceDone
SQLAlchemy pool_size=1, max_overflow=1 + reserved_concurrency=10 on API LambdaBeta Blocker
EC2 Auto Recovery alarm on NAT instance (StatusCheckFailed → recover action)Beta Blocker
Add async SQS+Lambda pipeline as new stories in backlog
Add deploy workflow story (currently missing from CI epics)
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