Files
MealQuest/docs/ARCHITECTURE.md
T
dev_agentandClaude Opus 5.5 998898c549 Add AGENTS.md, CLAUDE.md, README and architecture docs
AGENTS.md is the authoritative agent guide: house rules, approved
dependencies, quality floor, repository layout, branch and merge
conventions, where context and work tracking live, GLOSSARY vocabulary
and the rule never to use tea. CLAUDE.md imports it. README gives the
purpose and a make dev quick start. docs/ARCHITECTURE.md mirrors the
CTXD CONTEXT.MD architecture section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:15:12 -04:00

9.3 KiB
Raw Blame History

MealQuest architecture

This mirrors the architecture section of CONTEXT.MD in the CTXD project mealquest, which is the source of record; if the two disagree, CTXD wins and this file should be updated. It describes the planned application. The repository today is the Feature 01 scaffold: cmd/server, internal/config, internal/server (/health), internal/frontend (embedded PWA with SPA fallback), the web/ skeleton, Makefile, Dockerfile and CI.

Decision numbers (D-NNN) refer to DECISIONS.MD in CTXD.

Topology

PWA (phone / tablet / desktop)
   │ HTTPS
   ▼
Traefik v3.6 — 10.60.1.33
   file route: mealquest-ccl.yml
   │
   ▼
docker compose project "mealquest" — 10.60.1.14
   ├── mealquest   Go service + embedded PWA, port 8090
   ├── searxng     sidecar for recipe search
   └── Litestream  SQLite replication → Synology NAS 10.60.1.50

One container serves the API, the auth flow, media and the PWA. SQLite lives on the host volume and is continuously replicated by Litestream to the NAS.

Repository layout

Path Purpose
cmd/server Entry point; wiring only.
internal/config Env-only MEALQUEST_* config; secrets from *_FILE.
internal/server HTTP server, router, middleware.
internal/frontend Embedded PWA, SPA fallback.
internal/auth Authentik OIDC, sessions, CSRF.
internal/db, internal/store SQLite connections, sqlc queries.
internal/secretbox AES-256-GCM encryption for _ct columns.
internal/household, internal/dinners, internal/recipes Domain.
internal/ai/llm, internal/ai/harness, internal/ai/skills AI provider adapters, skill harness, skills.
internal/suggest, internal/plan, internal/shopping, internal/ideas Feature services.
internal/gcal, internal/notify, internal/push, internal/chat Integrations.
internal/jobs, internal/scheduler Background jobs and schedules.
migrations/ SQL migrations (embedded; no goose/golang-migrate).
web/ React PWA.
deploy/ Compose, Traefik route, Litestream config.
docs/ Documentation.
design/ Requirements and UI handoff (read-only reference).

Backend stack and conventions

  • Go 1.25, CGO_ENABLED=0, a single static binary.
  • Router: go-chi/chi/v5. Logging: log/slog (structured).
  • SQLite via modernc.org/sqlite, with journal_mode=WAL, foreign_keys=ON and busy_timeout. One writer connection plus a read pool. Queries generated by sqlc.
  • IDs are ULIDs (oklog/ulid/v2).
  • Timestamps are stored in UTC; local calendar dates are YYYY-MM-DD strings in the household timezone.
  • Configuration is environment-only (MEALQUEST_*). Secrets come only from files named by *_FILE variables. Missing or invalid config stops startup.
  • Error responses always have the body {"error":{"code":"…","message":"…","retryable":false}}.

Authentication

  • Authentik OIDC; the backend is a confidential client (authorization code flow handled server-side with coreos/go-oidc/v3 and x/oauth2).
  • Session cookie __Host-mealquest_session (Secure, HttpOnly, path /).
  • Unsafe requests require the CSRF header X-MealQuest-CSRF.
  • Access requires membership of the Authentik group mealquest-admin. There is a single role: one shared household, no per-person profiles.

Data model

Columns ending in _ct hold AES-256-GCM ciphertext (via secretbox).

Table Holds
household_settings Household name, timezone, preferences.
recipes Dinners: name, method, minutes, difficulty, source, lifecycle status.
recipe_ingredients Ingredients with quantities per recipe.
recipe_steps Ordered steps per recipe.
photos Cached recipe photos.
dinners Dinner records and lifecycle state.
suggestion_batches One AI suggestion run.
suggestions Side quests with reason ("Why this?") and Try it / Pass status.
week_plans One plan per Monday-start week; draft or synced.
plan_days The dinner for each day of a week plan.
rotation_reviews Rotation check answers ("Keep it in rotation?").
shopping_lists One Supply run per week; frozen into the Supply archive after the week.
shopping_groups Items grouped by dinner.
shopping_items Items, quantity, needed / have-it.
ai_settings Provider, model, temperature, system prompt, API key (_ct).
recipe_search_settings Recipe search configuration (searxng).
integrations_google Google Calendar OAuth tokens (_ct) and calendar id.
chat_settings Google Chat webhook URL (_ct).
notification_prefs Per-event push / Chat toggles, reminder time.
push_subscriptions Web Push subscriptions.
notification_outbox Pending notifications for delivery.
jobs Durable background job queue.
schedule_runs Record of scheduled runs (idempotency).
ai_runs Every AI skill call: inputs, outputs, validation, timing.
sessions Server-side sessions.

Dinner lifecycle

suggested ──Try it──▶ quest_log ──cooked──▶ tried ──Rotation check──▶ regular
    │                                          └────────────────────▶ archived
    └──Pass──▶ declined (not suggested again)

Promotion to Regular is never automatic (D-021): it happens only when the household answers "Keep it in rotation?".

API surface

  • JSON API under /api/v1, using the error body above for every failure.
  • /auth/* — OIDC login, callback, logout.
  • /media/* — cached photos.
  • /health — liveness.
  • Everything else serves the embedded PWA with SPA fallback.

AI layer

  • Provider interface (ai/llm) with adapters for OpenAI-compatible, Anthropic and Ollama endpoints, called over plain HTTP — no vendor SDKs.
  • Skill harness (ai/harness): each skill (suggestions, week draft, ingredient ideas, quick ideas, archive post titles, …) layers its prompt — system prompt from ai_settings, skill instructions, household context — then validates the structured output against the skill's schema. An invalid response gets one repair retry; a second failure is an error, never a silent fallback. Every call is recorded in ai_runs.

Recipe grounding

  • Recipes are grounded in real pages found via the searxng sidecar.
  • Fetching is SSRF-safe (public addresses only, size and time limits).
  • Recipe data is parsed from schema.org Recipe JSON-LD.
  • Photos are downloaded, processed (golang.org/x/image) and cached locally; the client never hot-links external images.

Scheduler

Times are in the household timezone.

Schedule When Does
suggestions_weekly Sunday 08:00 New batch of Side quests.
plan_draft Friday 09:00 AI drafts next week's plan.
shopping_chat_autosend Saturday 08:00 Sends the Supply run to Google Chat.
day_of_reminder Daily 16:30 Tonight's dinner reminder.
rotation_check Daily 09:00 Asks "Keep it in rotation?" for tried dinners.
week_rollover Monday 00:05 Freezes last week's list into the Supply archive, rolls the plan.
ai_runs purge Periodic Deletes old ai_runs rows.

Runs are recorded in schedule_runs and executed through jobs.

Notifications

  • Events: plan ready, list ready, day-of reminder, new suggestions; each can go to Web Push and/or Google Chat per notification_prefs.
  • Delivery goes through notification_outbox.
  • Web Push uses VAPID (webpush-go). Google Chat uses the stored incoming webhook.

Google Calendar

  • One-way push: the app is the source of truth; edits in Google Calendar do not come back.
  • OAuth once in Settings; one event per planned dinner.
  • Deterministic event ids derived from the plan day, so re-pushing updates the same events instead of creating duplicates.

Frontend

  • React 19, TypeScript, Vite, React Router 7, TanStack Query 5, vite-plugin-pwa, lucide-react (stroke 2.75).
  • CSS Modules over tokens.css; no Tailwind, no Framer Motion, no hard-coded hex. Self-hosted Bagel Fat One and Figtree.
  • Tests: Vitest, Testing Library, jsdom.
  • Offline cache for the current plan, shopping list and recipes.
Route Screen
/ Home
/onboarding Add Regulars
/plan Weekly plan (Draft board)
/plan/shopping Supply run
/plan/shopping/archive Supply archive
/discover Swipe Side quests
/quick Quick dinners
/cook-from-pantry Cook from what I have
/dinner/:id Recipe detail / Kitchen reading mode
/settings Settings home
/settings/notifications Notifications and Google Chat
/settings/ai Admin · AI assistant

Boundaries and gotchas

  • Secrets never reach the client — API keys, OAuth tokens and the Chat webhook stay server-side, encrypted at rest.
  • The app is the source of truth for the plan; Google Calendar is a one-way mirror.
  • Never auto-promote to Regular (D-021).
  • Quick means cooked in a crock pot or ≤ 30 minutes prep to table.
  • Weeks run Monday–Sunday in the household timezone.
  • No silent fallbacks — fail fast and report.
  • iOS Web Push requires the PWA to be installed to the Home Screen; show an install hint.