Files
recipe/.planning/research/SUMMARY.md
2026-04-29 21:07:49 +02:00

13 KiB

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

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 derivationhousehold_id always derived from authenticated sub, never accepted from request body. Single source of cross-tenant leaks.
  5. iOS infra hygienekotlin.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 subhousehold_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


Research completed: 2026-04-24 Ready for roadmap: yes