Files
recipe/.planning/research/SUMMARY.md
2026-04-24 12:56:13 +02:00

160 lines
13 KiB
Markdown

# Project Research Summary
**Project:** Recipe (working title) — household meal planner + pantry + shopping list
**Domain:** Mobile (iOS-primary) + self-hosted backend, offline-first collaborative app for a 2-person household
**Researched:** 2026-04-24
**Confidence:** HIGH
## Executive Summary
This is a household-scoped meal-planning app built as a Kotlin Multiplatform client (iOS-primary) against a self-hosted Ktor server, with offline-first operation and last-write-wins sync over HTTP polling. The core value is "my week is planned" — the planner is the hero feature; pantry and shopping exist to reinforce it. User-base is ~5-10 authenticated users across a handful of households. Tech stack was locked in a direct discussion rather than derived from research, so the research scope was narrowed to two areas where novel value was expected: **architecture patterns within the locked stack** and **pitfalls specific to this library combination**.
The recommended approach centers on a **sync-engine-first** architecture: a single Koin-singleton `SyncEngine` owns the outbox, the pull cursor, and the push/pull cycles; repositories only write to SQLDelight + outbox, never to HTTP directly. Every feature in the app sits on top of this spine, which decouples UI/domain from transport concerns and makes offline-first a property of the system rather than a per-feature discipline. Household scope is enforced at **three layers** (client query filter, server `PrincipalResolver` deriving household from JWT `sub`, and a `household_id` column on every tenant-scoped table) — single-layer enforcement is consistently the source of cross-tenant data leaks in apps like this.
The **highest-risk area** is sync correctness under concurrent household edits. LWW with device-clock timestamps silently loses data when clocks drift; the mitigation is server-assigned `updated_at` for every write, UUIDs (never composite natural keys like `(date, slot)`) as row identity, and a `(updated_at, id)` lexicographic pull cursor with microsecond precision to survive same-millisecond edits. Secondary risks: Kotlin/Native memory/GC on iPhone 12-era devices, Ktor JWT validation leeway and JWKS caching interactions with Authentik, and Haze blur over scrolling content on older iPhones.
## Key Findings
### Recommended Stack
Locked via direct discussion (see PROJECT.md § Key Decisions). No research-driven changes required.
**Core technologies:**
- Compose Multiplatform + Jetpack Navigation CMP port — iOS-primary UI, official JetBrains-recommended router
- Koin + SQLDelight + Ktor Client + kotlinx.serialization/datetime + Kermit + Coil 3 + Haze — canonical KMP client stack in 2026
- Ktor Server + Exposed (DSL, not DAO) + Postgres + Flyway + ktor-server-auth-jwt — canonical Kotlin backend
- Authentik OIDC — user's existing homelab identity provider
### Expected Features
Not researched (user explicitly opted to start catalog fresh rather than survey the market). Active v1 requirements captured directly in PROJECT.md § Requirements — four feature pillars: recipe catalog browse, meal planner, pantry, shopping list; plus auth, household sharing, and offline sync foundations.
### Architecture Approach
See `.planning/research/ARCHITECTURE.md` for the full treatment.
**Major components (top to bottom):**
1. **Compose UI + Navigation** — screens observe ViewModel state; navigation via Jetpack Nav CMP with nested NavHosts per tab for independent back stacks
2. **ViewModel layer** — StateFlow of immutable `UiState`, method-per-action pattern, scoped to `NavBackStackEntry` via `koinViewModel()`
3. **Repository layer** — domain-shaped API; reads return SQLDelight Flows `.asFlow().mapToList(dispatcher)`; writes go to SQLDelight + outbox atomically
4. **SyncEngine (Koin singleton)** — drives outbox drain (push) and pull cursor (poll on foreground + pull-to-refresh + debounced-after-write); owns all HTTP sync traffic
5. **Local DataSources** — thin wrappers over SQLDelight generated queries; one driver per process, threaded correctly for iOS NativeSqliteDriver
6. **Remote DataSources** — Ktor Client with JSON negotiation; catalog fetches use HTTP caching; sync endpoints are separate from catalog
7. **Server Ktor routes** — auth-gated via `Authentication.jwt("authentik")`; every household-scoped handler routes through a `PrincipalResolver` that looks up membership once
8. **Server DB (Exposed + Postgres + Flyway)** — DSL-only, JSONB for meal-entry extras, `newSuspendedTransaction` for every coroutine-touching handler
### Critical Pitfalls
See `.planning/research/PITFALLS.md` for 14 critical pitfalls + anti-pattern tables. The five most load-bearing:
1. **Sync correctness under concurrent edits** — server-assigned `updated_at`, UUID row identity, lexicographic `(updated_at, id)` pull cursor. Any shortcut here causes silent data loss.
2. **Ktor JWT + Authentik integration** — audience, issuer, clock-skew leeway, JWKS cache TTL all configurable; default values fail silently when clocks drift or keys rotate. Explicit configuration mandatory.
3. **OIDC redirect URI + PKCE** — byte-exact match required; mobile clients are public so PKCE is mandatory. Common cause of 400-series auth loops that are opaque without server logs.
4. **Household tenancy derivation**`household_id` always derived from authenticated `sub`, never accepted from request body. Single source of cross-tenant leaks.
5. **iOS infra hygiene**`kotlin.native.binary.objcDisposeOnMain=false`, `gc=cms`, single `ComposeUIViewController` instance, Haze on chrome only (never over scrolling content). Cheap to bake in day 1; painful to retrofit when iPhone XR/11 users complain about jank.
## Implications for Roadmap
The architecture research suggests an explicit **foundation-first** build order. The pitfalls research reinforces this by surfacing that most high-cost mistakes live in the foundation (sync engine, auth validation, household scope), not the feature layer. Suggested phase skeleton:
### Phase 1: Project infrastructure + module wiring
**Rationale:** One-time setup that blocks everything. Convention plugins, version catalog, Koin bootstrap, shared DTOs module, iOS target config with binary flags (`objcDisposeOnMain`, `gc=cms`), server Gradle setup, Flyway plumbing.
**Delivers:** A running but empty composeApp (iOS + Android) + running but unrouted Ktor server.
**Avoids:** Retrofit cost on iOS memory flags and Gradle conventions mid-project.
### Phase 2: Authentication foundation
**Rationale:** Nothing else can be built without authenticated principals. Blocks sync, household data, all CRUD.
**Delivers:** Client OIDC flow (AppAuth on Android, ASWebAuthenticationSession on iOS) to Authentik → access token stored. Server ktor-server-auth-jwt validates token against Authentik JWKS. Protected `/api/v1/me` endpoint returns user.
**Avoids:** Pitfalls 7 + 8 (JWT validation misconfig, redirect URI/PKCE errors).
### Phase 3: Households + membership + server data foundation
**Rationale:** Every feature table needs `household_id`. Introducing this after feature tables exist is a migration nightmare.
**Delivers:** `users`, `households`, `memberships`, `invites` tables + Flyway migrations. `PrincipalResolver` that maps JWT `sub``household_id`. Endpoints for creating a household, generating invite codes, accepting invites. Client auth session now includes `household_id`.
**Avoids:** Tenant-scope leaks, cross-household data bugs.
### Phase 4: Sync engine skeleton
**Rationale:** Second-hardest piece after auth. Must exist before any feature-specific data can be synced. Built on a trivial first table (e.g., a `notes` sentinel table or the `households` metadata).
**Delivers:** SyncEngine interface + implementation. Outbox schema in SQLDelight with `id`, `table`, `row_id`, `op`, `payload`, `created_at`. `/api/v1/sync/push` and `/api/v1/sync/pull` endpoints. Polling scheduler + pull-to-refresh + debounced after-write trigger. Cursor persistence. Mock table round-trips.
**Avoids:** Pitfalls 9, 10, 11 (LWW timestamp sources, delete-recreate races, cursor edge cases).
### Phase 5: Recipe catalog (read path)
**Rationale:** Read-mostly, simpler sync (no writes from client), teaches Exposed + SQLDelight + Ktor end-to-end. Seeds the rest of the app with real data to develop against.
**Delivers:** `recipes`, `ingredients`, `products` tables on server (Flyway migrations). Seed data mechanism (SQL fixtures or admin CLI). Catalog Ktor routes. Client-side catalog cache in SQLDelight with pull-only sync. RecipeListScreen + RecipeDetailScreen reading from local cache.
**Avoids:** Sync-strategy-by-accident (catalog uses different cache rules than household data).
### Phase 6: Meal planner (hero write path)
**Rationale:** Core value. Exercises the full write path: optimistic local write + outbox + sync.
**Delivers:** `plan_entry` table (server + client) with `household_id` + `updated_at` + `deleted_at`. Planner calendar UI. Add/remove/edit meal flows. Nutrition totals computed client-side from catalog + plan.
**Avoids:** Pitfall 1 (sync correctness), by this point the foundation is already correct.
### Phase 7: Pantry
**Rationale:** Second household-scoped feature. Reuses all of the plumbing from Phase 6. Validates that sync foundation generalizes.
### Phase 8: Shopping list + session log
**Rationale:** Computed view over pantry + plan + a small session table. Ties the three data sources together.
### Phase 9: UI chrome with Haze liquid-glass approximation
**Rationale:** Intentionally late. Earlier phases use boring default chrome to avoid blocking on design. Now swap in Haze-based nav and tab bars, glassy cards, dark mode polish. Measurable real-device perf can be validated against real data from Phase 6-8.
**Avoids:** Pitfall 12 (Haze perf regressions — easier to measure once data volume is realistic).
### Phase 10: Polish, polish infra, iOS deployment
**Rationale:** Externalized strings with Polish copy, locale-aware date formatting (local display only — wire stays UTC), Bundle ID + privacy manifests, TestFlight distribution to partner.
### Phase 11 (optional, post-v1): Recipe authoring in-app
**Rationale:** Explicitly deferred in PROJECT.md. First v1 seeds catalog via server migrations.
### Phase Ordering Rationale
- **Auth → Households → SyncEngine → features**: each layer enables the next; skipping any accelerates for a week and costs a month (from ARCHITECTURE.md § Build Order).
- **Catalog (read) before planner (write)**: reads are simpler and catch sync-pull bugs in isolation before writes introduce push/outbox/conflict bugs.
- **UI chrome last**: Haze perf is measurable only with realistic data; design iteration shouldn't block data-layer correctness.
### Research Flags
Phases likely needing deeper phase-level research during planning:
- **Phase 2 (Auth):** Authentik-specific OIDC provider setup steps; AppAuth vs custom iOS wrapper tradeoffs; token refresh behavior
- **Phase 4 (SyncEngine):** Concrete cursor format, outbox schema ordering guarantees, retry/backoff policy
- **Phase 9 (UI chrome):** Current Haze perf benchmarks on CMP iOS; liquid-glass approximation design patterns
Phases with well-trodden paths (minimal research-phase needed):
- **Phase 1 (Infra):** Convention plugins + version catalog is well-documented
- **Phase 5 (Catalog read):** Basic CRUD + cache pattern
- **Phases 6-8 (features):** Once foundation is in place, these follow the architecture patterns directly
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | Locked in direct discussion with tradeoff analysis per library |
| Features | HIGH (scope-bounded) | User defined v1 directly in PROJECT.md; no market research done, intentionally |
| Architecture | HIGH | Research agent produced concrete patterns specific to the locked stack |
| Pitfalls | HIGH | 14 specific pitfalls with library-level detail; covers all major risk areas |
**Overall confidence:** HIGH for entering the roadmap phase.
### Gaps to Address
- **Authentik-specific OIDC flow details** — the research documented WHAT to get right (JWT validation, PKCE, redirect URIs) but not Authentik's specific UI/config steps. Resolve during Phase 2 planning.
- **Mobile OIDC library choice for iOS** — PROJECT.md notes "ASWebAuthenticationSession wrapper" with no specific KMP wrapper library recommended. Resolve during Phase 2 planning.
- **Haze current CMP-iOS perf characteristics on iPhone 12-era hardware** — needs real-device measurement, not research. Covered by Phase 9.
- **Seed-data mechanism** — "SQL migrations, JSON fixtures, or CLI tool" listed as options in PROJECT.md § Constraints. Resolve during Phase 5 planning.
## Sources
### Primary (HIGH confidence)
- `.planning/PROJECT.md` — authoritative product scope + locked stack
- `.planning/research/ARCHITECTURE.md` — agent-researched, ~1900 words, 7 sections
- `.planning/research/PITFALLS.md` — agent-researched, ~2800 words, 14 critical pitfalls + tables
### Secondary
- Direct discussion transcript (April 2026) — tech-stack tradeoff conversation that led to PROJECT.md decisions
- [Navigation in Compose Multiplatform](https://kotlinlang.org/docs/multiplatform/compose-navigation.html)
- [Kotlin/Native memory management](https://kotlinlang.org/docs/native-memory-manager.html)
- [Haze 1.0 — Chris Banes](https://chrisbanes.me/posts/haze-1.0/)
- [Exposed — Transactions](https://www.jetbrains.com/help/exposed/transactions.html)
- [Exposed — JSON/JSONB types](https://www.jetbrains.com/help/exposed/json-and-jsonb-types.html)
---
*Research completed: 2026-04-24*
*Ready for roadmap: yes*