platepilot

Architecture

Scope and status

This document separates:

PlatePilot is an adult-only general-wellness product. It is not a medical system.

System context

flowchart LR
    Adult["Adult member (18+)"]
    Support["Authorized support operator"]
    PP["PlatePilot"]
    Providers["Future external providers<br/>analysis, nutrition, identity, billing, messaging"]

    Adult -->|"future mobile wellness workflows"| PP
    Support -->|"future constrained support workflow"| PP
    PP -. "M1+ provider adapters" .-> Providers

At M0 the adult-facing and internal applications are shells. The provider links are intentionally absent; local deterministic mocks exercise seams without contacting vendors.

Containers

M0 local development

flowchart TB
    Mobile["Expo mobile shell"]
    Admin["Next.js internal shell"]
    API["NestJS API<br/>health, OpenAPI, foundations"]
    Shared["Shared packages<br/>contracts, config, tokens, provider seams"]
    DB[("Local PostgreSQL")]
    S3["LocalStack S3"]
    Q["LocalStack SQS"]
    Mocks["Deterministic synthetic mocks"]

    Mobile --> Shared
    Admin --> Shared
    API --> Shared
    API --> DB
    API -. "asynchronous boundary scaffold" .-> Q
    API -. "object-storage boundary" .-> S3
    API --> Mocks

PostgreSQL and LocalStack are loopback-bound Docker services with persistent local volumes. They are development dependencies, not secure customer-data stores or a backup strategy.

Container/package Responsibility M0 status
apps/mobile Expo Router presentation shell; Paper, TanStack Query, Zustand, React Hook Form, and Zod foundations Implemented scaffold
apps/api NestJS REST boundary, startup configuration validation, typed HTTP errors, health, non-production OpenAPI, Prisma boundary, and meal-analysis job contracts Implemented scaffold
apps/admin Internal Next.js shell without customer-data access or consumer routes Implemented scaffold
packages/contracts Provider- and persistence-independent Zod/domain schemas Implemented
packages/provider-contracts Replaceable external-service interfaces and deterministic synthetic mocks Implemented foundation
packages/config Validated shared runtime configuration Implemented
packages/design-tokens Cross-application presentation primitives Implemented foundation
PostgreSQL Local relational persistence target Local only
LocalStack S3/SQS Local object and queue emulation Local only
infra Credential-free CDK synthesis target for us-east-1 Synth only

Future AWS beta

The accepted beta direction is one AWS region, us-east-1, using CDK and ECS. Every element below is planned, not deployed.

flowchart TB
    Client["Mobile client"]
    Ingress["Managed HTTPS ingress<br/>planned"]
    APIECS["ECS API service<br/>planned"]
    Queue["SQS analysis queue<br/>planned"]
    Worker["ECS analysis worker<br/>planned"]
    RDS[("RDS PostgreSQL<br/>planned")]
    Photos[("Versioned S3 uploads<br/>planned; 1-day fallback")]
    Retained[("Non-versioned S3 diary photos<br/>planned; 365-day fallback")]
    Identity["Cognito identity<br/>planned"]
    Vendors["Approved providers<br/>planned"]
    Observe["Logs, metrics, alarms, audit trail<br/>planned"]

    Client --> Ingress --> APIECS
    Client -. "authentication" .-> Identity
    APIECS --> RDS
    APIECS --> Photos
    APIECS --> Queue --> Worker
    Worker --> Photos
    Worker --> Retained
    Worker -. "minimum necessary data" .-> Vendors
    Worker --> RDS
    APIECS --> Observe
    Worker --> Observe

The synthesized S3 design separates raw quarantine, immutable sanitized processing, and optional retention. The source bucket is versioned and expires current and noncurrent quarantine/ and processing/ versions after 1 day. The API auto-orients and re-encodes uploads without EXIF, commits the sanitized checksum/version, and deletes the exact raw version. Providers and the encrypted, non-versioned 365-day diary/ bucket receive only sanitized bytes. The worker deletes processing and retained objects at their defined boundaries; lifecycle rules remain fallbacks.

Before any deployment, reviewed API and worker images, runtime construction of DATABASE_URL from a managed secret, certificates, an approved email identity, optional social-provider secrets, network controls, access logging, alarms, and tested backup/restore procedures are required. Resource names, accounts, and endpoints are deliberately unspecified.

Module boundaries and seams

Dependencies should point toward stable domain contracts:

UI / REST / queue handlers
          |
     use-case layer
          |
domain contracts and provider ports
     /                 \
persistence adapters   external-provider adapters
Seam Stable side Replaceable side Rule
REST OpenAPI plus shared request/response schemas Mobile, future clients Validate at ingress; return typed, non-sensitive errors
Domain packages/contracts HTTP, persistence, and provider representations Keep provider IDs and database details out of domain schemas
Persistence API-owned repository/Prisma boundary Local PostgreSQL; future RDS PostgreSQL UI and providers must not import persistence models
Meal analysis Provider interface and normalized provisional result Deterministic mock; future reviewed analysis vendor No vendor response becomes final without validation and adult review
Nutrition lookup Provider interface and normalized food data Future nutrition-data provider Preserve source/provenance; allow manual entry
Queue Analysis job contract with stable IDs, outbox publication, and fenced leased claims LocalStack SQS and M1 worker; future reviewed worker image Duplicate delivery is harmless; every processing mutation matches the active lease token, and expired work is durably reconciled
Object storage Photo reference plus retention policy LocalStack S3; future encrypted S3 Keep objects private; use a versioned source bucket and a separate non-versioned retention bucket with worker-enforced deadlines
Identity/authorization Verified adult subject plus per-adult ownership on every repository query Deterministic two-token mock; future Cognito adapter Authentication does not replace object-level authorization; cross-adult reads exist only for shared plans and shared day totals
Household sharing sharePlans / shareDaySummary switches per adult (ADR-0008) Household module and Prisma tables Off by default, mutual for plans, read-only, totals-only; nothing is copied between adults
Billing BillingProvider seam plus API-owned subscriptions state (ADR-0009) Deterministic mock; future store/vendor adapter Product access is based on verified server-side entitlement state; only photo analysis is gated, never manual entry
Notifications NotificationProvider seam; alerts derived on request from plans and preferences Deterministic mock returning receipts; future push adapter Reminders and problem notices only; no threshold or goal warnings
Meal planning MealPlanProvider seam (generate week, localize shopping items) plus API-owned preferences, plan items, and shopping list (ADR-0010) Deterministic synthetic recipe library; future model or recipe vendor 14 EU allergens are hard exclusions enforced again in the API; focus not targets; plans stay editable suggestions
UI and recovery state Contract-shaped views, local drafts, commands, photo-job handles, and preferences Query cache, SQLCipher, and per-adult SecureStore records Photo bytes stay outside databases; server records remain authoritative after acknowledgement

Only the contract/provider packages and local mocks exist for external systems. Real LogMeal, Spoonacular, RevenueCat, Cognito, notification, and health adapters are intentionally absent. Production must never silently fall back to mocks.

Planning, household, subscription, export, and alerts (M2/M3 API)

Each capability is one NestJS module under apps/api/src/<module>/ with the M1 shape: Zod contracts in packages/contracts, a repository port with a Prisma adapter, an audited service, a controller whose OpenAPI operation IDs name the generated SDK functions, and colocated tests.

planning   -> planning_preferences (allergens, dislikes, focus, pattern, servings, location, units, tz)
           -> MealPlanProvider.generateWeek -> plan_items(origin=generated) + plan_item_ingredients
           -> plan_weeks (generatedAt, editedAt) ; manual edits mark the week edited
           -> aggregate ingredients -> MealPlanProvider.localizeShoppingItems -> shopping_list_items
           -> DiaryService.createPlannedMeal (confirm) -> meal_entries(source=planned)
           -> PlanSharingReader (household) for the other adult's read-only items
worker     -> PlanGenerationScheduler (every minute): Sunday >= PLAN_AUTO_GENERATE_HOUR local
              -> generate NEXT Monday-to-Sunday week for opted-in adults with no plan_weeks row
household  -> household_invitations, household_sharing_settings, adult_profiles.householdId
subscription -> subscriptions + count(meal_analysis_jobs in UTC month) -> AnalysisAllowanceGuard
photo      -> AnalysisAllowanceGuard before authorizing a NEW upload (403 analysis_allowance_exhausted)
adult-data -> export (own rows only) / delete (retention bucket first, then one cascading transaction, receipt)
alerts     -> plan_items (unconfirmed, same day) + alert_preferences -> NotificationProvider.send -> receipts

Data-flow rules:

Asynchronous meal-analysis flow

The repository implements the M1 asynchronous contract boundary with deterministic local adapters; real-provider and deployed infrastructure remain future work:

  1. An adult captures or chooses a meal photo and sees the retention choice.
  2. The client removes unnecessary metadata where supported and requests an authorized upload.
  3. The API creates an adult-scoped provisional meal record and a non-public object reference.
  4. The client uploads the image; the API verifies completion and enqueues a job.
  5. A worker claims the job idempotently, checks authorization/retention metadata, submits once, and persists the accepted provider job ID under the active lease before polling.
  6. Later attempts poll the stored provider job ID. Queued and processing states remain retryable; terminal success is accepted only when the response correlates to that exact ID. The M1 mock encodes a recognizable signed deterministic ID so a fresh worker process can reproduce its result without resubmission; future real providers retain their provider-issued correlation IDs.
  7. The worker stores a provisional estimate with provenance and completion state.
  8. The API deletes the exact raw quarantine/ version after committing the sanitized source. The worker deletes the exact processing/ version after analysis. One-day lifecycle rules cover current and noncurrent versions under both prefixes.
  9. The client refreshes or receives a future notification, then presents review and edit controls.
  10. The adult confirms, changes, or replaces the estimate with manual entry.
  11. Opted-in photos are copied from the exact source version to a separate non-versioned diary/ bucket. The object creation time determines the recorded 365-day deadline.
  12. The worker deletes due retained objects and records cleanup idempotently. The bucket lifecycle is a fallback rather than the primary enforcement mechanism.

Failures remain visible and recoverable. The worker reconciles expired processing leases and stale queued jobs with no pending outbox record into a fresh idempotent transactional-outbox message. Processing claims and successful queue publications have separate configured attempt limits. At either limit the job enters a retryable failed state (processing_attempts_exhausted or queue_publication_attempts_exhausted) while preserving the immutable source version for the public retry route. Retained-photo cleanup is also restart-safe: an object delete can be replayed until its database cleanup timestamp is recorded. Queue delivery and provider latency do not block the rest of a diary.

Local-first and sync boundaries

“Local-first” means the repository can be developed and validated without cloud credentials or vendor calls, and the M1 mobile client durably records supported diary mutations before transport.

Native SQLCipher/SecureStore and Expo media-directory behavior still require development-build validation on supported devices. Background execution while the app is suspended remains deferred; M1 recovery runs at startup, route entry, foreground, connectivity restoration, and retry deadlines.

Data classification

Class Examples Handling
Public Published documentation and approved product copy No confidentiality requirement; integrity still matters
Internal Synthetic test output, build logs, operational metrics without personal data Repository/team access; avoid user-entered values
Confidential Adult account details, household membership, diary records, nutrition estimates, entitlements Least privilege, encryption in transit/at rest, scoped logs, audited support access
Restricted Meal photos, authentication secrets/tokens, export archives, deletion evidence, vendor credentials Strongest access controls, short retention, no routine logs, no public URLs, explicit deletion path

Wellness data may be sensitive even when it is not represented as regulated medical data. Legal classification and regional obligations require review before beta.

Quality, SLO, and recovery goals

These are beta targets, not measured M0 guarantees:

Goal Beta target / status
Capacity assumption 10,000 registered adults and 1,000 daily active adults
Core service availability 99.9% monthly target; final SLI definition pending
Cached dashboard Under 1 second
Normal API latency p95 under 500 ms
Analysis acknowledgement Return an accepted/pending state without waiting for provider completion; budget pending
Meal-analysis completion p90 under 15 seconds; measured separately from core availability
Recovery point objective 15 minutes
Recovery time objective 4 hours
Database backup retention 35 days
Application log retention 30 days, with no photo or sensitive payload content
Security audit retention 90 days, subject to legal/security review

The performance targets are confirmed but not measured M0 guarantees. Restore tests, load tests, queue saturation tests, and provider-failure exercises are required before the targets can be treated as credible. See runbooks.

Explicitly deferred

Product behavior for these areas may be specified in UX documentation, but specification must not be read as implementation.