This document separates:
PlatePilot is an adult-only general-wellness product. It is not a medical system.
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.
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 |
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.
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.
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:
operation envelope (idempotency key, baseVersion, 409 on conflict) and are recorded in client_operations with *_plan_item operation types; confirmation records the created meal as confirm_plan_item so replays return the same meal.generated, unconfirmed items in the requested days; manual items survive unless the adult asks to replace edits, and a week with edits is refused without that flag. The shopping list is rebuilt after every generation and manual change, keeping tick state by ingredient key.plan_weeks row, so repeated runs are idempotent; each generation is audited as system.GET /planning/week (other adult’s items when both share plans) and GET /household/shared-summary (other adult’s eight daily totals when they share them). Everything else filters by the caller’s own adultProfileId.503 when storage is unavailable, and leaves a deletion_receipts row carrying a one-way subject hash only.The repository implements the M1 asynchronous contract boundary with deterministic local adapters; real-provider and deployed infrastructure remain future work:
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.diary/ bucket. The object creation time determines the recorded 365-day deadline.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” 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.
ui_json; malformed metadata fails closed. Runtime
PRAGMA table_info migration adds ui_json to databases created before this field existed.platepilot-private-media before manipulation. Final sanitized and
cropped files are relocated there, and startup/sign-out sweep directory contents across process
restarts without deleting the external library original.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.
| 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.
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.
Product behavior for these areas may be specified in UX documentation, but specification must not be read as implementation.