Architecture (BCE)

Boundary–Control–Entity, verified against the shipped backend (backend/app/ on origin/main, July 2026) — not the founding plan. This page + Data Model + API are the deepest layer of this spec: if you only had these three pages and the code's directory names, you could rebuild the backend's shape.

The one rule

Dependencies point inward

api → features → core, and features → boundaries. integrations implements boundaries and is the only place an external SDK (anthropic, stripe, google-auth, sqlalchemy's engine, …) may be imported. wiring.py is the single composition root — the only file allowed to import both a use case and a concrete integration and wire them together. core/ imports no framework and performs no I/O — no datetime.now(), no DB session, no HTTP client; time comes in only via the injected Clock. A CI lint (scripts/check_core_purity.py) fails the build if this is violated.

Layer map

LayerFolderHoldsMay import
Entitycore/Pure domain: value objects, policies, pure functions (meal estimation, band-state classification, RSS aggregation, target-building, billing catalogs, nutrients, locale/market rules, calibration, backdate rules)stdlib + other core/ modules only
Controlfeatures/Use cases — one class per file, orchestrates core + boundariescore/, boundaries/ (never another features/ class, never integrations/ directly)
Boundaryboundaries/Protocol interfaces — the seam to every external systemcore/ types only (for method signatures)
Integrationintegrations/Concrete adapters implementing a boundary; the only place an SDK/driver/ORM engine is imported; also owns the SQLAlchemy models.py (ORM rows are infrastructure, never in core/)the SDK it wraps, boundaries/, core/ (to map rows ↔ domain types)
Boundary (HTTP)api/Thin FastAPI routers — parse request, call one features/ class, map the result/exception to a responsefeatures/, Pydantic wire models (defined alongside the router)
Composition rootwiring.pyReads settings.py, chooses which integrations/ class implements each boundary, constructs every features/ use case with its dependencies, registers routerseverything
Backend BCE flow from HTTP request to API router, feature use case, core domain, boundary protocols, integrations, and wiring.
Request flow and the inward dependency direction. Source: docs/architecture/diagrams/02-component-bce.md (emealia-app repo).

Naming conventions (codified in .claude/skills/python-coding-standards/SKILL.md, "Naming by BCE layer")

~40 shipped classes already follow one shape per layer. New code matches it — grep the layer and copy the neighbours rather than inventing a scheme.

LayerPatternExamples
Boundary (boundaries/, a Protocol)<Domain><RoleNoun> — domain first, role last. Role noun from a fixed vocabulary: Store (persistence), Estimator (AI→estimate), Service (external transactional system), Gateway (external session/entry), Meter, Codec, Emailer, Clock. Fall back to bare Boundary only when no crisp noun fits.MealStore, AiEstimator, PaymentService, SessionCodec, UsageMeter, AuthBoundary
Integration (integrations/, an adapter)<AdapterTech><ExactBoundaryName> — provider/tech first, boundary name verbatim after. Never feature-first-then-provider.AnthropicAiEstimator + FakeAiEstimator (both implement AiEstimator), SqlAlchemyMealStore, StripePaymentService, GoogleAuthBoundary, JwtSessionCodec, SystemClock, InMemoryDemoSessionGateway
Use case (features/, the control)An imperative verb phrase — not a noun, not a …Generation/…Activity/…BA suffix. A verb phrase names the process; a role noun (boundary) names the thing that does it.LogMeal, SaveMeal, BuildDailyView, RefineMeal, SearchMealHistory, SocialSignIn, StartCheckout, GenerateDailySuggestion
Domain (core/)Plain nouns for value objects; configurable rules → <Domain>Policy; SQLAlchemy ORM rows (in integrations/models.py, never core/) → <Entity>RowMealEstimate, SuggestionContext, DailyTipPolicy, MealRow, UserRow

Avoid: vague role suffixes (Manager, Handler, Processor, Facade, bare Service outside the external-boundary sense) and operation nouns as a class role (…Generation, …Building, …Logging) — the operation is the verb-phrase use case; the object is the role-noun boundary. Prefer extending an existing boundary (e.g. add a method to AiEstimator for both meal parsing and tip/suggestion generation) over minting a new provider-named …Generator boundary.

Boundary to integration to external system map: 21 boundary Protocols grouped by AI, meal persistence, profile and context, auth and session, billing and trial, and cross-cutting concerns, each pointing to its concrete adapter and, where applicable, the external system or PostgreSQL table it reaches.
Boundary → integration → external system map, generated from assets/diagrams/boundary-integration-map.mmd — verified against backend/app/boundaries/ + backend/app/integrations/ (July 2026). Dashed nodes are not yet implemented.

Boundaries → integrations (the full seam map — 21 shipped)

Every row is a Protocol in backend/app/boundaries/ with at least one concrete adapter in backend/app/integrations/. A Fake*/InMemory* adapter exists wherever a story needs to run in CI without a real credential.

BoundaryHidesShipped integration(s)Used by (features)
AiEstimatorAnthropic Claude — meal parsing + tip/suggestion generation. Only module allowed to import the anthropic SDK.AnthropicAiEstimator, FakeAiEstimatorLogMeal, RunAiJob, ReestimateDraft, RefineMeal, GenerateDailySuggestion, RunCalibration
MealStoreRead-side meal persistence (owns only the meals table's queries)SqlAlchemyMealStoreSearchMealHistory and meal read endpoints
MealLogStoreAtomic draft-meal + AI-job creation — the one transaction that must not splitSqlAlchemyMealLogStoreLogMeal
MealSaveStoreAtomic draft→logged save + daily_summaries recomputeSqlAlchemyMealSaveStoreSaveMeal
MealReestimateStoreRe-estimating a caller-owned draft (capped 1×)SqlAlchemyMealReestimateStoreReestimateDraft
MealRefineStoreReplacing a caller-owned logged meal's estimate + day recomputeSqlAlchemyMealRefineStoreRefineMeal (feature exists; no API route wired yet — gap)
MealMemoryStoreConfirmed-dish reuse (meal_memory) — 0-AI-call repeat logging ≥3 confirmationsSqlAlchemyMealMemoryStoreLogMeal, SaveMeal
AiJobStoreAsync estimation worker queue (ai_jobs); patches the estimate onto the owning meal atomicallySqlAlchemyAiJobStoreRunAiJob
ProfileStoreUser profile + preferences (currently: Today-nutrient visibility)SqlAlchemyProfileStoreprofile endpoints
DailyViewStoreRead seam assembling one local day's band inputs (daily_summaries + user_targets + Today nutrients)not yet implementedMOCK_MODE serves GET /daily from fixtures instead (Epic 03)BuildDailyView
DayContextStoreOptional per-day wellbeing context tags (daily_context_tags)SqlAlchemyDayContextStoreday-context endpoints
ActivityContextStoreOptional per-day movement/turnover context (activity_context) — informational only, never feeds bands/budgets/streaksSqlAlchemyActivityContextStoreactivity-context endpoints
AuthBoundaryThe social identity provider's OAuth 2.0/OIDC Authorization Code dance — build the redirect, exchange the code for identity. emealia stores no passwords.GoogleAuthBoundary, FakeAuthBoundarySocialSignIn
AuthUserStoreClaim-or-create on (auth_provider, provider_subject) — the identity claim, nothing else (not profile, not meals, not usage)SqlAlchemyAuthUserStoreSocialSignIn
SessionCodecSigns/reads the session-cookie token; signing key/algorithm live only in the integrationJwtSessionCodec (HS256)/auth/* endpoints
DemoSessionGatewayPre-signup demo sessions, rate-limited per IPInMemoryDemoSessionGateway/demo-sessions endpoint
PaymentServiceStripe — paid subscriptions only. The free trial is never enforced here (non-negotiable #3) — that's UsageMeter's job.StripePaymentService, FakePaymentServiceStartCheckout, StartCustomerPortal
EmailerTransactional emailnot yet implemented(not yet called by a shipped feature)
ClockSystem time — testable via fake/fixed time; local_date defines "today" per the user's IANA timezoneSystemClocknearly every feature (backdate windows, trial reservation, daily_summaries.local_date)
UsageMeterTrial entitlement (check_entitlement) + per-user/global AI rate caps (check_and_reserve) — two distinct checks, two distinct failure modesSqlAlchemyUsageMeterLogMeal, ReestimateDraft, RefineMeal
ConsentStoreGDPR consent grant/revoke ledger, versionedSqlAlchemyConsentStoreBodyHealthConsent

Use cases (features/ — 13 shipped)

ClassDoes
LogMealConsent + backdate + entitlement checks, creates a draft meal + ai_jobs row atomically (via MealLogStore), returns 202
SaveMealFlips a draft to logged, recomputes daily_summaries (RSS aggregation) atomically
ReestimateDraftRe-runs the AI estimate on a caller-owned draft, capped at 1 re-estimate
RefineMealReplaces a caller-owned logged meal's estimate, recomputing that day's summary (not yet exposed via an API route)
RunAiJobThe async worker step: calls AiEstimator, maps + validates the response, writes it onto the job's meal
RunCalibrationGate G2 tooling — runs a batch of real meals through the estimator and reports the correction rate
BuildDailyViewAssembles the no-numbers band view model for Today (reads DailyViewStore)
GenerateDailySuggestionCoaching: builds a bounded SuggestionContext, calls AiEstimator.suggest_daily, gated by the pure DailyTipPolicy
SearchMealHistoryKeyword search over a user's logged meals + meal_memory
SocialSignInExchanges the provider's auth code (AuthBoundary) for an identity, claims-or-creates the user row (AuthUserStore), issues a session (SessionCodec)
StartCheckoutResolves the market's Stripe price id (BillingCatalogs) and opens a Stripe Checkout session
StartCustomerPortalOpens a Stripe customer-portal session for self-service plan management
BodyHealthConsentGrant/revoke the Art. 9 body-health-data consent
Rule already enforced

No use case imports another use case — orchestration composes at the api/ or wiring.py level, not feature-to-feature. New feature modules stay ≤250 lines of logic.

Roadmap — planned, not yet built (do not implement without a story)

These items are named in CLAUDE.md, the concept docs, or research notes, but have no code behind them yet. An AI asked to "rebuild emealia as it is" should treat everything above this section as the spec, and everything below as future scope only.

ItemStatus
Access+refresh session split, a sessions table + SessionStore boundary, rotation/revocation, POST /auth/logout-all
AppleAuthBoundary (Apple Sign-In)
Age gate
Account settings, GET/PATCH /account, full_name/username columns on users
daily_suggestions table + SuggestionStore boundary (persisting coaching output)
Every-other-day coaching review suggestions (4.6a–c)
Breakfast-adaptive daily-tip threshold (4.4d)
Epic 18 — activity logging as first-class context beyond the current calm turnover-range noteDRAFT — explicitly "do not implement" per the implementation plan
Emailer integration (SES or equivalent)
DailyViewStore real integration (Today is currently MOCK_MODE-only)
emealia Plus add-on layer (recipes, photo logging, advanced stats/export)Post-launch, gated on D7 ≥ 30%

Coaching timing — corrected

The shipped core/daily_tip.py + DailyTipPolicy (story 4.3, GenerateDailySuggestion) gate the one daily tip on: cumulative kcal today > 35% of the rolling baseline (floor 300 kcal), time gates at 10:00 / 20:00, hard cutoff 23:00, minimum 3 logging days all-time, 1/day. CLAUDE.md's root context states a 50%-of-7-day-median threshold with only a 10:00/23:00 gate — that wording is stale; the values above (35%/300kcal, 10:00/20:00/23:00) are what the shipped policy implements. Every-other-day review suggestions are deferred for beta, not currently built.

Feature isolation

See also