# Roadmap: Recipe

**Core Value:** "My week is planned." I pick recipes, the calendar fills up, and I know what we're eating.

**Granularity:** Fine (11 phases + 1 inserted shell phase)
**Mode:** YOLO
**Source of truth:** Derived from `.planning/REQUIREMENTS.md` (73 v1 requirements) guided by `.planning/research/SUMMARY.md` (suggested skeleton) and `.planning/research/ARCHITECTURE.md` (build-order reasoning).

## Phases

- [x] **Phase 1: Project Infrastructure & Module Wiring** — Running-but-empty KMP client + Ktor server with all build infra baked in
- [x] **Phase 2: Authentication Foundation** — User signs in through Authentik (OIDC+PKCE) and the server validates tokens
- [ ] **Phase 2.1: App Shell, Navigation & Search Foundation** — Signed-in users can move between the four empty app areas through a Liquid-styled menu and open the search surface
- [ ] **Phase 3: Households, Membership & Server Data Foundation** — Users create/join households; server enforces household scope
- [ ] **Phase 4: Sync Engine Skeleton** — Offline-first read/write with outbox-backed LWW sync on a sentinel table
- [ ] **Phase 5: Recipe Catalog (Read Path)** — User browses, filters, and opens recipe details from a seeded catalog
- [ ] **Phase 6: Meal Planner — Core Write Path** — User picks recipes into the 5-slot calendar; first real outbox-backed aggregate
- [ ] **Phase 7: Meal Planner — Customization & Nutrition** — User tweaks servings/ingredients/products per meal entry and sees daily nutrition
- [ ] **Phase 8: Pantry** — User tracks what's on hand and sees shortfalls against the plan
- [ ] **Phase 9: Shopping List & Session Log** — User generates a grouped shopping list from the plan and shops with "bought" tracking
- [ ] **Phase 10: UI Chrome & Liquid-Glass Polish** — Real-device Liquid glass tuning, iOS-idiomatic chrome, calmer visual hierarchy
- [ ] **Phase 11: Localization & iOS Deployment** — Full Polish copy pass, i18n-ready resources, TestFlight to partner

## Phase Summary Table

| # | Name | Goal (one line) | Requirements | #SC |
|---|------|-----------------|--------------|-----|
| 1 | Project Infrastructure & Module Wiring | KMP client + Ktor server build cleanly with convention plugins, version catalog, iOS binary flags, and a shared DTO module | INFRA-01, INFRA-02, INFRA-03, INFRA-06 | 4 |
| 2 | Authentication Foundation | End-to-end OIDC+PKCE login to Authentik with JIT user provisioning and server-side JWT validation | AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06 | 5 |
| 2.1 | App Shell, Navigation & Search Foundation | Signed-in users land in the real 4-tab app shell with empty Planner / Recipes / Pantry / Shopping screens, Liquid-styled chrome, and an operational search affordance | UI-03, UI-04, UI-09, UI-10 | 5 |
| 3 | Households, Membership & Server Data Foundation | Create/join households via invites; every request carries a household-scoped principal derived from JWT | HSHD-01, HSHD-02, HSHD-03, HSHD-04, HSHD-05, HSHD-06, HSHD-07, INFRA-05 | 5 |
| 4 | Sync Engine Skeleton | Outbox-backed LWW sync works round-trip on a sentinel table with server-assigned timestamps and cursor pull | SYNC-01, SYNC-02, SYNC-03, SYNC-04, SYNC-05, SYNC-06, SYNC-07, SYNC-08, SYNC-09, SYNC-10 | 5 |
| 5 | Recipe Catalog (Read Path) | User browses a seeded recipe catalog, filters/searches, and opens a detail view — offline-capable | RCPE-01, RCPE-02, RCPE-03, RCPE-04, RCPE-05, RCPE-06, RCPE-07, RCPE-08, UI-05, UI-08 | 5 |
| 6 | Meal Planner — Core Write Path | User fills the 5-slot calendar with recipes; adds/removes/replaces/skips; writes survive offline and sync | PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, PLAN-12, PLAN-14 | 5 |
| 7 | Meal Planner — Customization & Nutrition | User customizes ingredients per meal entry and sees daily macro totals that respect customizations | PLAN-07, PLAN-08, PLAN-09, PLAN-10, PLAN-11, PLAN-13 | 4 |
| 8 | Pantry | User manages pantry inventory by category and sees shortfalls for a chosen horizon | PNTR-01, PNTR-02, PNTR-03, PNTR-04, PNTR-05 | 4 |
| 9 | Shopping List & Session Log | User generates a category-grouped shopping list and marks items bought during a session | SHOP-01, SHOP-02, SHOP-03, SHOP-04, SHOP-05, SHOP-06 | 4 |
| 10 | UI Chrome & Liquid-Glass Polish | Real-device tuning for Liquid glass chrome, iOS idioms, breathing-room visual hierarchy, and cross-screen polish after real data exists | UI-06, UI-07 | 5 |
| 11 | Localization & iOS Deployment | All strings externalized, Polish copy throughout, partner installs via TestFlight | UI-01, UI-02, INFRA-04, INFRA-07 | 4 |

## Phase Details

### Phase 1: Project Infrastructure & Module Wiring

**Goal:** Stand up a KMP + Ktor repo whose build is "boring correct" from day 1 — version catalog, convention plugins, iOS binary flags, and a pure-Kotlin `shared/` module — so every later phase slots into an already-configured system.
**Depends on:** Nothing (first phase)
**Requirements:** INFRA-01, INFRA-02, INFRA-03, INFRA-06
**Success Criteria** (what must be TRUE):
  1. `./gradlew build` succeeds across `composeApp`, `server`, `shared`, and produces an iOS framework and an Android APK from the bare template screens.
  2. All library versions are resolved through `gradle/libs.versions.toml`; no version literals exist inside any `build.gradle.kts`.
  3. iOS `gradle.properties` carry `kotlin.native.binary.objcDisposeOnMain=false` and `kotlin.native.binary.gc=cms`; a debug launch on simulator boots without warnings about legacy memory-management flags.
  4. `build-logic/` convention plugins apply the Kotlin/Compose/test configuration to every module — adding a new module requires only applying a convention plugin, not copying compiler args.
  5. `shared/commonMain` contains only domain models + serializable DTOs; no Ktor, Compose, or SQLDelight imports appear anywhere under `shared/`.
**Plans:** 7 plans

Plans:
- [ ] 01-01-PLAN.md — Version catalog extensions (Koin/Kermit/Spotless/Flyway/Postgres) + iOS K/N flags + verify-*.sh invariant scripts
- [ ] 01-02-PLAN.md — build-logic/ included build with 5 precompiled plugins (recipe.quality, recipe.kotlin.multiplatform, recipe.compose.multiplatform, recipe.android.application, recipe.jvm.server) + root settings.gradle.kts includeBuild wiring
- [ ] 01-03-PLAN.md — Module refactor: composeApp/shared/server build.gradle.kts apply recipe.* conventions; drop js target; enable explicitApi() on shared/
- [ ] 01-04-PLAN.md — Koin + Kermit bootstrap across all 4 platforms (commonMain Koin.kt/AppModule.kt/Logging.kt; iOS KoinIos.kt bridge; Android MainApplication.kt + manifest; JVM/Wasm main() rewrites; iOSApp.swift wiring)
- [ ] 01-05-PLAN.md — Server /health + Flyway bootstrap + HOCON config (application.conf, Database.kt with fail-loud contract, db/migration/.gitkeep, ApplicationTest.kt covers /health without Postgres)
- [ ] 01-06-PLAN.md — docker-compose.yml (postgres:16) + README.md Local development section (drops js docs)
- [ ] 01-07-PLAN.md — shared/ package scaffold + full green-build gate (spotlessApply, verify-*.sh, ./gradlew build, ./gradlew check)
**UI hint:** no
**Research flag:** no

### Phase 2: Authentication Foundation

**Goal:** Deliver a working end-to-end login: the app opens Authentik via OIDC (authorization code + PKCE), stores the tokens securely, and the Ktor server validates them on a protected `/api/v1/me` endpoint, JIT-provisioning users on first sign-in.
**Depends on:** Phase 1
**Requirements:** AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06
**Success Criteria** (what must be TRUE):
  1. From a fresh install on iOS, I can tap "Zaloguj się", complete Authentik's hosted login, and land back in the app as an authenticated user.
  2. I close and reopen the app an hour later; I am still signed in without re-entering credentials (refresh token flow runs transparently).
  3. I tap "Wyloguj się"; the app returns to the login screen and the stored tokens are gone from Keychain/EncryptedSharedPreferences.
  4. Calling `GET /api/v1/me` with a valid token returns my user record; the same call with a missing, expired, or wrong-audience token returns 401.
  5. My user row exists in the server DB after my first successful login, keyed by the OIDC `sub` claim (no manual user creation needed).
**Plans:** 7 plans

Plans:
- [x] 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit
- [x] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me`
- [x] 02-03-PLAN.md — Common OIDC/store contracts, JVM/Wasm actuals, and store contract test
- [x] 02-04-PLAN.md — Android OIDC actual, Android secure AuthState store, and manifest callback
- [x] 02-05-PLAN.md — iOS OIDC actual, iOS Keychain store, and URL scheme callback
- [x] 02-06-PLAN.md — AuthSession state machine, bearer HTTP client, refresh/logout behavior, and Koin wiring
- [x] 02-07-PLAN.md — Compose auth gate UI, Polish resource strings, and iOS Authentik UAT
**UI hint:** yes
**Research flag:** yes

### Phase 2.1: App Shell, Navigation & Search Foundation

**Goal:** Replace the post-login placeholder with the real app shell before household/domain data lands: four persistent top-level destinations (Przepisy, Planer, Spiżarnia, Zakupy), deliberate empty states for each, a working search affordance, and the first shared component layer based on Composables + Liquid instead of growing further around Material 3.
**Depends on:** Phase 2
**Requirements:** UI-03, UI-04, UI-09, UI-10
**Success Criteria** (what must be TRUE):
  1. After sign-in I land in the main app shell, not the Phase 2 welcome placeholder; I can switch between Przepisy, Planer, Spiżarnia, and Zakupy from the main menu without signing out.
  2. Each tab has its own navigation state boundary from day 1, so future detail screens can preserve back stacks independently; the initial screens are intentionally empty states, not throwaway placeholders.
  3. The shared UI foundation uses Composables' Compose Unstyled/renderless primitives for new controls where applicable, with local Recipe components providing the visual styling; Material 3 remains only as temporary legacy auth scaffold until migrated.
  4. Menu chrome and primary icon buttons use the Liquid library (`io.github.fletchmckee.liquid:liquid`) for the first Liquid-Glass-inspired treatment, constrained to chrome/buttons and backed by a simple fallback path if performance or platform support is not acceptable.
  5. The search button is functional: tapping it opens a search surface, query input updates state, close/clear actions work, and empty/no-data content is intentional until the recipe catalog read path wires real results in Phase 5.
**Plans:** 8 plans

Plans:
- [x] 02.1-01-PLAN.md — Dependency/library assumptions + Wave 0 validation stubs
- [x] 02.1-02-PLAN.md — Recipe theme tokens and legacy MaterialTheme wrapper
- [x] 02.1-03-PLAN.md — Glass backend, fallback, debug override, and shared backdrop source
- [x] 02.1-04-PLAN.md — Type-safe routes, tab destination metadata, RootNavHost placeholders, and shared shell/search strings
- [x] 02.1-05-PLAN.md — AppShell, DockBar, FloatingSearchButton, and active-tab search wiring
- [x] 02.1-06-PLAN.md — Search ViewModels, SearchPill, and search state tests
- [x] 02.1-07-PLAN.md — EmptyState component, four tab screens/ViewModels, and empty-state resources
- [x] 02.1-08-PLAN.md — Shell DI, app auth-gate integration, RootNavHost real screen wiring, and AppShell gate test
**UI hint:** yes
**Research flag:** yes

### Phase 3: Households, Membership & Server Data Foundation

**Goal:** Introduce the tenancy model before any feature tables land — `households`, `memberships`, `invites` with Flyway migrations; server's `PrincipalResolver` maps JWT `sub` to an active `householdId`; client finishes onboarding by creating or joining a household.
**Depends on:** Phase 2.1
**Requirements:** HSHD-01, HSHD-02, HSHD-03, HSHD-04, HSHD-05, HSHD-06, HSHD-07, INFRA-05
**Success Criteria** (what must be TRUE):
  1. On my first login, I see an onboarding screen asking me to create a new household or enter an invite code.
  2. I create a household, receive a short-lived single-use invite code, send it to my partner, and they redeem it to join the same household.
  3. Once both users are in the same household, any household-scoped API call returns identical data regardless of which member made it.
  4. A crafted API request that puts a different `household_id` in the body is ignored — the server always derives `household_id` from the authenticated principal, not the payload.
  5. The server starts up and Flyway automatically applies `V1__init.sql` (or equivalent) in the correct order; restarting the server twice in a row is idempotent.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 4: Sync Engine Skeleton

**Goal:** Build the offline-first spine — a Koin-singleton `SyncEngine` that owns the outbox and pull cursor, server endpoints `POST /sync/push` + `GET /sync/pull?since=`, and a sentinel table round-trips through it — so every later feature just adds a table, not a sync strategy.
**Depends on:** Phase 3
**Requirements:** SYNC-01, SYNC-02, SYNC-03, SYNC-04, SYNC-05, SYNC-06, SYNC-07, SYNC-08, SYNC-09, SYNC-10
**Success Criteria** (what must be TRUE):
  1. I write to the sentinel table while the app is offline (airplane mode); the write appears instantly in the UI, and when I reconnect it reaches the server within seconds without manual intervention.
  2. My partner edits the same sentinel row on their device; within the poll interval (20–30 s while foregrounded) I see their change, and if we both edited concurrently the server's later-assigned `updated_at` wins with no silent data loss.
  3. I delete a sentinel row on device A; after sync the row is gone on device B — and if I re-create "the same" row it comes back with a fresh UUID identity and does not resurrect old fields.
  4. Killing the app with pending writes in the outbox and relaunching later preserves those writes; they drain on the next sync cycle.
  5. Network failures and 5xx responses trigger exponential backoff retries without blocking the UI; no feature code issues HTTP sync writes directly — all go through the `SyncEngine`.
**Plans:** TBD
**UI hint:** no
**Research flag:** yes

### Phase 5: Recipe Catalog (Read Path)

**Goal:** Deliver the first real user-visible feature — a browseable recipe catalog — via a pull-only cache path that exercises Exposed + SQLDelight + Ktor + Coil end-to-end without write-path complexity, seeded server-side so the rest of the app has real data to develop against.
**Depends on:** Phase 4
**Requirements:** RCPE-01, RCPE-02, RCPE-03, RCPE-04, RCPE-05, RCPE-06, RCPE-07, RCPE-08, UI-05, UI-08
**Success Criteria** (what must be TRUE):
  1. I open "Przepisy" and see a grid of recipe cards with thumbnail, title, and cooking time — fully populated from server-seeded catalog data.
  2. I can filter the grid by meal slot, tag, and cooking-time range, and search by title/tag text; results update as I type.
  3. I tap a recipe and see a detail view with ingredients (amounts + units), steps, nutrition per serving, cooking time, and any defined substitutions.
  4. I put the device in airplane mode, relaunch the app, and the catalog still renders from the local SQLDelight cache.
  5. The app respects my system light/dark appearance setting, and recipe-list dates/times render in Polish locale format (day and month names in Polish).
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 6: Meal Planner — Core Write Path

**Goal:** Ship the hero feature's skeleton — the calendar with 5 slots per day — as the first real household-scoped write aggregate. Every add/remove/replace/skip/serving-change goes through the outbox, proving the sync spine on realistic load.
**Depends on:** Phase 5
**Requirements:** PLAN-01, PLAN-02, PLAN-03, PLAN-04, PLAN-05, PLAN-06, PLAN-12, PLAN-14
**Success Criteria** (what must be TRUE):
  1. I open "Planer", navigate between days/weeks/months, and see each day's 5 slots (śniadanie, drugie śniadanie, obiad, przekąska, kolacja) with whatever I've planned.
  2. I can tap a slot, pick a recipe from the catalog, and see it appear instantly — even while offline — then reappear on my partner's device after a sync cycle.
  3. I can remove a meal entry, replace it with a different recipe, adjust its servings (1–12), and mark a slot as "skipped" for a specific day.
  4. Every meal entry has a stable UUID identity; deleting and re-adding the same recipe on the same (day, slot) creates a distinct new entry rather than reviving the old one.
  5. Two household members concurrently editing the same slot converge deterministically on whichever edit the server stamped last, with no silent data loss.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 7: Meal Planner — Customization & Nutrition

**Goal:** Flesh out the hero feature with per-entry customization (substitutions, excludes, extras, amount overrides, product/pack choice) and the nutrition numbers that close the "am I eating right" loop — all while respecting customizations so the math is honest.
**Depends on:** Phase 6
**Requirements:** PLAN-07, PLAN-08, PLAN-09, PLAN-10, PLAN-11, PLAN-13
**Success Criteria** (what must be TRUE):
  1. On any meal entry I can substitute an ingredient with one of the catalog-defined alternatives and the change sticks after sync and restart.
  2. On any meal entry I can exclude an ingredient, add an extra ingredient from the catalog (amount + unit), and override an ingredient's amount — each of these reflects in shopping/pantry calculations later.
  3. On any meal entry I can select a specific product (pack size) for a given ingredient when multiple exist.
  4. Each day in the planner shows daily nutrition totals (kcal, protein, fat, carbs) aggregated across all planned meals for that day, recomputed when any customization changes.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 8: Pantry

**Goal:** Give the household a view of what's actually on hand and what's missing, so the plan connects to real life. Reuses the Phase 4 sync foundation on a second household-scoped aggregate.
**Depends on:** Phase 7
**Requirements:** PNTR-01, PNTR-02, PNTR-03, PNTR-04, PNTR-05
**Success Criteria** (what must be TRUE):
  1. I open "Spiżarnia" and see my pantry inventory grouped by category (pieczywo, nabiał, mięso i ryby, warzywa, owoce, suche, przyprawy, inne).
  2. I can manually add or update the quantity of any pantry ingredient using its pantry unit (g, ml, szt.), and the change syncs to my partner's device.
  3. I pick a planning horizon (e.g., "next 7 days") and see which ingredients fall short based on the plan minus current pantry.
  4. I can filter the pantry view by category and by shortfall status (needed / sufficient / not in plan).
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 9: Shopping List & Session Log

**Goal:** Close the loop from plan to store — generate a category-grouped shopping list from a chosen date range, mark items bought during an in-store session, and move bought items into the pantry automatically.
**Depends on:** Phase 8
**Requirements:** SHOP-01, SHOP-02, SHOP-03, SHOP-04, SHOP-05, SHOP-06
**Success Criteria** (what must be TRUE):
  1. I open "Zakupy", pick a date range from the plan, and see a shopping list aggregating ingredient needs minus current pantry, grouped by category for an efficient store trip.
  2. During a shopping session I can mark an item bought; it disappears from active needs and shows up in the pantry in its pantry unit.
  3. I can undo a recently marked-bought item within the same session; the item reappears in active needs.
  4. I close and reopen the app mid-shopping; my session's bought/unbought state is still there until I explicitly clear it.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

### Phase 10: UI Chrome & Liquid-Glass Polish

**Goal:** Polish and harden the app-wide visual system after real catalog/planner/pantry/shopping data exists — tune Liquid glass chrome on device, verify iOS idioms, remove remaining Material 3-looking surfaces, and run the calmer spacing/typography pass across every screen.
**Depends on:** Phase 9
**Requirements:** UI-06, UI-07
**Success Criteria** (what must be TRUE):
  1. The Phase 2.1 app shell still preserves each tab's back stack after real recipe detail, planner, pantry, and shopping flows exist.
  2. The tab bar, nav bar, and search/button chrome use the chosen Liquid-Glass approximation consistently in light and dark schemes, and scrolling a full recipe grid on iPhone 11 stays above ~55 fps.
  3. The app respects iOS safe areas, supports the swipe-back gesture where applicable, and keyboards never cover focused inputs.
  4. Typography and spacing feel noticeably calmer than the legacy PWA mockup — more whitespace between cards, larger hit targets, readable at arm's length.
  5. Any remaining Material 3-looking default components from earlier phases are replaced by Recipe-styled components built on the agreed component foundation.
**Plans:** TBD
**UI hint:** yes
**Research flag:** yes

### Phase 11: Localization & iOS Deployment

**Goal:** Externalize every string into Compose resources with complete Polish copy (correct plural forms), build and deploy the Ktor server image to the homelab alongside Authentik, and get the iOS build into my partner's hands via TestFlight.
**Depends on:** Phase 10
**Requirements:** UI-01, UI-02, INFRA-04, INFRA-07
**Success Criteria** (what must be TRUE):
  1. Every user-facing string across every screen is resolved through Compose resources — a grep for raw Polish literals inside composables returns only fixture/test data.
  2. The whole app reads as correctly-grammatical Polish, including plural forms (1 / 2 / 5 / 22 counts all render with the right form) and date/weekday names.
  3. The Ktor server builds into a Docker image and is running in the homelab reachable over HTTPS with a real (Let's-Encrypt-issued) cert, alongside Authentik.
  4. My partner installs the iOS app through TestFlight, signs in through Authentik, joins our household via invite, and can plan a meal that I see on my device.
**Plans:** TBD
**UI hint:** yes
**Research flag:** no

## Progress Table

| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Project Infrastructure & Module Wiring | 7/7 | Complete | 2026-04-24 |
| 2. Authentication Foundation | 7/7 | Complete | 2026-04-28 |
| 2.1 App Shell, Navigation & Search Foundation | 2/8 | In progress | - |
| 3. Households, Membership & Server Data Foundation | 0/0 | Not started | - |
| 4. Sync Engine Skeleton | 0/0 | Not started | - |
| 5. Recipe Catalog (Read Path) | 0/0 | Not started | - |
| 6. Meal Planner — Core Write Path | 0/0 | Not started | - |
| 7. Meal Planner — Customization & Nutrition | 0/0 | Not started | - |
| 8. Pantry | 0/0 | Not started | - |
| 9. Shopping List & Session Log | 0/0 | Not started | - |
| 10. UI Chrome & Liquid-Glass Polish | 0/0 | Not started | - |
| 11. Localization & iOS Deployment | 0/0 | Not started | - |

## Coverage Summary

- **v1 requirements total:** 73 (AUTH=6, HSHD=7, RCPE=8, PLAN=14, PNTR=5, SHOP=6, SYNC=10, UI=10, INFRA=7)
- **Mapped to phases:** 73
- **Unmapped:** 0
- **Coverage:** 100%

---
*Roadmap created: 2026-04-23*
*Granularity: fine (11 phases + inserted Phase 2.1) | Mode: yolo*
