application
Ration — System Design
Product: AI-powered kitchen management (pantry → recipes → meal plan → shopping → restock)
Platform: Cloudflare Workers (edge SSR) · Domain:ration.mayutic.com
AI surfaces: Copilotcopilot.ration.mayutic.com· MCPmcp.ration.mayutic.com
Clients: Web (React Router v7) · Native iOS (SwiftUI)
This wiki is the system design homepage for project members. Deep runbooks, workflow diagrams, and operator checklists live in the private repository (README.md, docs/, ios/README.md).
1. Mission & product loop
Ration is an orbital supply-chain pantry system: zero-latency inventory, AI-assisted meal logistics, and automated waste reduction.
flowchart LR
Cargo["Cargo<br/>Inventory"] --> Galley["Galley<br/>Recipes"]
Galley --> Manifest["Manifest<br/>Meal plan"]
Manifest --> Supply["Supply<br/>Shopping list"]
Supply --> Dock["Dock<br/>Restock cargo"]
Dock --> Cargo| Domain | Responsibility |
|---|---|
| Cargo | Org-scoped inventory (qty, unit, domain, expiry, tags) |
| Galley | Recipes & provisions; cook deducts cargo; active selection feeds Supply |
| Manifest | Calendar meal plan (breakfast / lunch / dinner / snack) |
| Supply | Shopping list rebuilt from Galley + Manifest horizon; dock → Cargo |
| Hub | Customizable dashboard widgets (shared layout web |
| Ask Ration | First-party Copilot (WebSocket + Durable Object) |
| MCP | External agents (Claude, Cursor, ChatGPT, …) operate the same kitchen data |
2. High-level architecture
Three Workers share D1 / KV / R2 / AI / Vectorize where appropriate. There are no always-on VMs — capacity scales with Cloudflare’s edge.
flowchart LR
subgraph Clients["Clients"]
Browser["Browser / PWA"]
iOS["iOS SwiftUI"]
Agent["MCP client"]
end
subgraph DNS["Cloudflare DNS"]
App["ration.mayutic.com"]
McpHost["mcp.ration.mayutic.com"]
CopilotHost["copilot.ration.mayutic.com"]
end
subgraph Workers["Workers"]
Main["ration<br/>SSR + APIs + queues"]
Mcp["ration-mcp<br/>MCP tools"]
Copilot["ration-copilot<br/>Ask + Think DO"]
end
Browser --> App --> Main
iOS -->|Bearer /api/mobile/v1| App
iOS -->|WSS| CopilotHost
Browser -->|WSS| CopilotHost
Agent -->|OAuth / API key| McpHost --> Mcp
CopilotHost --> Copilot2.1 Worker roles
| Worker | Entry | Role |
|---|---|---|
| ration | workers/app.ts |
React Router SSR, Better Auth, Stripe/RevenueCat webhooks, AI queue producers/consumers, REST + mobile APIs |
| ration-mcp | workers/mcp.ts |
Model Context Protocol tool server; OAuth 2.1 + rtn_live_* API keys |
| ration-copilot | workers/copilot.ts |
Ask Ration WebSocket; ProjectThinkAgent Durable Object; shared MCP tool runtime + docs search |
2.2 Cloudflare bindings
| Binding | Service | Purpose |
|---|---|---|
DB |
D1 | Users, orgs, cargo, meals, plans, ledger, API keys |
RATION_KV |
KV | Rate limits, embedding cache, tier cache, webhook idempotency, Copilot session state |
STORAGE |
R2 | Scan images, avatars, exports |
AI |
Workers AI | Embeddings (embeddinggemma-300m); Copilot inference (gpt-oss-120b) |
VECTORIZE |
Vectorize (ration-cargo) |
Semantic ingredient match — namespaced per org |
AI Gateway |
External | Gemini (gemini-3.5-flash) for scan / generate / plan-week / import-url |
| Queues | scan, meal-generate, plan-week, import-url | Long AI jobs off the request path |
PROJECT_THINK |
Durable Object | One Think isolate per {org}:{user}:{tier}:{conversationId} |
FLAGS |
Flagship | Gradual rollout / kill switches (server-enforced) |
Smart Placement: Isolates relocate near the D1 primary so DB-heavy work stays ~5ms to D1 instead of cross-ocean round-trips.
3. Request & tenancy model
3.1 Multi-tenant isolation
All kitchen data is owned by an organization (household / group), not a lone user row.
- Session carries
activeOrganizationId. - Every D1 query is scoped with
organization_idfrom verified session membership (requireActiveGroup()/ mobile JWT org claim) — never from unchecked client input. - Vectorize uses
namespace = organizationIdfor the same isolation at the embedding layer. - Credits and capacity limits follow the org owner’s tier.
3.2 Auth surfaces
| Surface | Mechanism |
|---|---|
| Web hub | Better Auth session cookies — magic link (POST continue), Google OAuth, optional Apple (Flagship-gated) |
| iOS | PKCE magic link + Sign in with Apple / Google → mobile access JWT + refresh |
| REST v1 | Org API keys rtn_live_* (SHA-256 hashed at rest) |
| MCP | OAuth 2.1 (mcp:* scopes) or API key |
| Copilot | Web: short-lived handshake token; iOS: mobile Bearer on WSS upgrade |
3.3 Defence in depth (summary)
- Edge / TLS — CDN, HSTS, CSP, frame deny
- Authentication — Session / JWT / API key / OAuth
- Authorization — Org membership, roles, tier gates, Vectorize namespace
- Rate limiting — KV sliding windows; AI spend buckets fail closed
- Integrity — Zod at API boundary,
db.batch()atomic writes, credit overdraft SQL guard, Stripe/RC webhook idempotency
4. Web application structure
Stack: React Router v7 (Framework Mode) · TypeScript · Tailwind · Drizzle ORM · Bun · Biome · Vitest / Playwright
application/
├── workers/ # Worker entries: app.ts, mcp.ts, copilot.ts
├── app/
│ ├── routes/ # File-based routes (routes.ts)
│ │ ├── hub/ # Authenticated product UI
│ │ └── api/ # Webhooks, scan, checkout, mobile, v1, …
│ ├── components/ # Feature UI (cargo, galley, manifest, supply, hub, support, …)
│ ├── lib/ # Server utilities (*.server.ts), schemas, MCP, Copilot, mobile
│ ├── db/ # Drizzle schema
│ └── test/ # Fixtures & mock Cloudflare bindings
├── drizzle/ # Generated migrations (never hand-write)
├── docs/ # Fin knowledge + developer notes
├── e2e/ # Playwright journeys
├── content/ # Blog / help markdown
└── public/ # PWA manifest + shell service worker4.1 Route map (conceptual)
| Area | Paths |
|---|---|
| Marketing / legal | /, /about, /blog, /help, /legal/*, /pricing (hub) |
| Auth | /auth/*, Better Auth under /api/auth/* |
| Product hub | /hub, /hub/cargo, /hub/galley, /hub/manifest, /hub/supply, /hub/settings |
| Mobile API | /api/mobile/v1/* |
| Public REST | /api/v1/{inventory,galley,supply}/… |
| Agent discovery | /.well-known/*, /auth.md, OpenAPI JSON |
4.2 Server conventions
*.server.ts— Cloudflare / D1 / secrets only on the server- Zod in
app/lib/schemas/— validate at the API boundary - D1 bind limit — max 100 parameters per statement; chunk with
query-utils.server.tsconstants - Writes — prefer
db.batch([...])over sequential awaits - AI jobs — enqueue → poll
queue_job; credits deducted at gate, refunded on consumer failure - Feature flags —
isFeatureEnabled()server-side; UI flags are insufficient for auth/AI kill switches
4.3 Async AI pipeline
flowchart LR
Client -->|"withCreditGate"| Producer["Worker producer"]
Producer --> Queue["Cloudflare Queue"]
Producer --> Status["queue_job pending"]
Queue --> Consumer["Queue consumer"]
Consumer --> Gateway["AI Gateway → Gemini"]
Consumer --> D1["D1 + Vectorize"]
Client -->|"poll status"| Status| Job | Typical credit cost | Notes |
|---|---|---|
| Receipt scan | 2 | Vision; R2 temp image; PDF or image |
| Meal generate | 2 | Vectorize verification against pantry |
| Plan week | 3 | Fills Manifest |
| Import URL | 1 | Optional Browser Rendering for JS-heavy pages |
5. iOS application structure
Stack: SwiftUI · iOS 18+ · XcodeGen (project.yml) · RevenueCat · Textual (markdown) · XCTest
Native client speaks only to /api/mobile/v1/* (Bearer JWT). Entitlements come from RevenueCat; the app reads user.tier / credits from the server.
ios/
├── project.yml # Source of truth for Xcode project
├── ci_scripts/ # Xcode Cloud post-clone (XcodeGen + SPM pin)
├── swiftpm/Package.resolved # Pinned SPM graph for CI
├── Ration/
│ ├── App/ # @main, DI, auth-gated root, tab shell
│ ├── Core/
│ │ ├── Auth/ # AuthManager, Keychain, PKCE
│ │ ├── Networking/ # APIClient, RationAPI, AIJobPoller
│ │ ├── Session/ # Org context, credits, AI consent
│ │ ├── Consent/ # Shared gate for scan / generate / import / plan-week
│ │ ├── Persistence/ # Org-scoped offline SnapshotStore
│ │ ├── Billing/ # RevenueCat boundary
│ │ ├── Design/ # Orbital Luxury theme, Space Mono, list chrome
│ │ └── Models/ # Codable ↔ mobile API
│ ├── Features/
│ │ ├── Dashboard/Hub/ # Widget grid (shared hubLayout with web)
│ │ ├── Cargo/ Scan/ Galley/ Manifest/ Supply/
│ │ ├── Ask/ # Copilot WebSocket UI
│ │ ├── Settings/ # Account + group + tags
│ │ ├── Billing/ # Paywall
│ │ └── Auth/ Onboarding/
│ └── Resources/ # Assets, fonts
└── RationTests/ # XCTest5.1 Tabs & chrome
- Tabs: Hub · Cargo · Galley · Manifest · Supply
- Leading: Org switcher (avatar, credits, Crew pill) → Group Settings
- Trailing: Page filters + profile → Account Settings
- FAB: Context menus per surface; Ask Ration dock
5.2 Client architecture notes
| Concern | Approach |
|---|---|
| Auth | PKCE magic link + Apple/Google social; Universal Links primary, ration:// fallback |
| Org switch | New org-scoped token pair; wipe/reload via orgGeneration |
| Offline | Org-scoped snapshots; forced logout runs full wipe (snapshots, billing, images, session) |
| AI consent | Single SessionStore flag + AIConsentCoordinator for all four AI entry points |
| Async AI | Same queue job IDs as web; AIJobPoller |
| Billing | RevenueCat SDK purchases; server materializes tier/credits from webhooks when fulfillment is enabled |
Versioning (iOS): MARKETING_VERSION + CURRENT_PROJECT_VERSION in project.yml (independent of web package.json, same patch/minor cadence). After project.yml edits: bun run ios:generate.
CI: Pushes to main → Xcode Cloud (not .gitlab-ci.yml) → Archive → TestFlight Internal. Ration.xcodeproj is generated, not committed.
6. Data model (conceptual)
erDiagram
user ||--o{ member : joins
organization ||--o{ member : has
organization ||--o{ cargo : owns
organization ||--o{ meal : owns
organization ||--o{ supply_list : owns
organization ||--o{ meal_plan : owns
organization ||--o{ ledger : credits
meal ||--o{ meal_ingredient : contains
meal_plan ||--o{ meal_plan_entry : schedules
supply_list ||--o{ supply_item : lists
tag ||--o{ cargo_tag : labels
tag ||--o{ meal_tag : labelsPrinciples:
- Organization is the tenancy root (shared pantry).
- Tags are an org-wide registry (
tag+ join tables). - Quantity 0 is a restock reminder; delete is permanent jettison.
- Migrations: edit
app/db/schema.ts→bun run db:generate→ review → migrate. Never hand-writedrizzle/*.sql.
7. AI & matching
| Layer | Implementation |
|---|---|
| Embeddings | Workers AI @cf/google/embeddinggemma-300m (768-dim); KV cache ~7 days |
| Semantic index | Vectorize ration-cargo, cosine, org namespace |
| Match thresholds | ~0.78 product paths; ~0.60 MCP search recall |
| Meal match | Strict (100% cookable) vs delta (partial); unit conversion + density |
| LLM (ops) | AI Gateway → Gemini for vision/text jobs |
| Copilot | Workers AI gpt-oss-120b + Think DO; tools wrap MCP runtime |
Credits are org-pooled. Deduction uses SQL WHERE credits >= cost RETURNING id. Failed queue consumers refund via failAiJobWithRefund.
8. Billing & tiers
| Channel | Role |
|---|---|
| Stripe | Web Embedded Checkout + webhooks (legacy / still active until RC fulfillment cutover) |
| RevenueCat | Cross-platform catalog; iOS StoreKit; Stripe app strp_ for external purchase sync; webhook → Ration |
Free vs Crew (capacity): Inventory, meals, supply lists, owned groups, invites, and public share links are tier-gated. Crew includes 1 free Ask Ration conversation / group / day. Capacity is evaluated from the org owner’s effective tier (KV-cached ~60s).
9. External interfaces
9.1 MCP (ration-mcp)
- Transport: streamable HTTP at
/mcp - Auth: OAuth 2.1 (preferred) or API key
- Tools: inventory, galley, manifest, supply, preferences, match, imports — uniform
{ ok, tool, data | error }envelope - Most tools are credit-free (rate-limited); credit-aware exceptions for plan-week / generate after host approval
9.2 Public REST v1
Scoped API keys for inventory / galley / supply export & import. OpenAPI at /api/openapi.json.
9.3 Mobile REST v1
Full product surface for iOS. OpenAPI at /api/openapi/mobile-v1.json.
9.4 Agent discovery
RFC-style /.well-known/ catalog, OAuth metadata, MCP server card, Apple AASA, DNS-AID HTTPS records under mayutic.com.
10. Scalability & reliability notes
| Concern | Design choice |
|---|---|
| Request latency | Edge SSR + Smart Placement; heavy AI on Queues |
| D1 writer | Single-region primary; compound indexes; org-scoped queries |
| KV | Eventually consistent; rate-limit TTLs self-clean |
| AI cost abuse | Credits + fail-closed rate limits + Flagship kill switches + Gateway spend limits |
| Idempotency | Stripe/RC event IDs in KV; queue runIdempotentAiJob claim |
| Observability | Analytics Engine datasets (ration_ops, ration_copilot) — no PII |
| GDPR delete | Purge D1 + Vectorize + R2 for the user |
11. Development & quality gates
| Command | Purpose |
|---|---|
bun run dev:local / dev:remote |
Local Miniflare vs remote bindings |
bun run test:unit |
Vitest |
bun run typecheck / lint |
Types + Biome |
bun run test:e2e |
Playwright (dev server for Option A) |
bun run ios:generate / ios:check |
XcodeGen + build/test |
bun run flag:check |
Flagship registry sanity |
bun run db:generate / db:migrate:dev |
Schema migrations |
Definition of done: unit tests + typecheck + lint pass; version bump in package.json + app/lib/version.ts (and iOS project.yml when ios/ ships); README updated when behaviour changes.
Versioning: Web 1.X.1…1.X.49 then 1.(X+1).0. iOS marketing version follows the same rule independently.
12. Where to go next (in-repo)
| Doc | Contents |
|---|---|
README.md |
Full architecture, sequence diagrams, rate-limit matrix, MCP tool table, Copilot lifecycle |
ios/README.md |
XcodeGen, signing, Xcode Cloud, auth, UI conventions |
docs/dev/feature-flags.md |
Flagship registry & AI kill switches |
docs/fin/ |
Support / Copilot knowledge corpus |
docs/mcp/ |
MCP operator notes |
.cursor/rules/ |
Engineering protocols (security, D1 patterns, testing, version bumps) |
Maintained for project members. Prefer updating this page when the system topology changes (new Worker, tenancy model, or client architecture); keep workflow-level detail in README.md.