22 KiB
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
- Phase 1: Project Infrastructure & Module Wiring — Running-but-empty KMP client + Ktor server with all build infra baked in
- 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):
./gradlew buildsucceeds acrosscomposeApp,server,shared, and produces an iOS framework and an Android APK from the bare template screens.- All library versions are resolved through
gradle/libs.versions.toml; no version literals exist inside anybuild.gradle.kts. - iOS
gradle.propertiescarrykotlin.native.binary.objcDisposeOnMain=falseandkotlin.native.binary.gc=cms; a debug launch on simulator boots without warnings about legacy memory-management flags. 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.shared/commonMaincontains only domain models + serializable DTOs; no Ktor, Compose, or SQLDelight imports appear anywhere undershared/. 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):
- 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.
- I close and reopen the app an hour later; I am still signed in without re-entering credentials (refresh token flow runs transparently).
- I tap "Wyloguj się"; the app returns to the login screen and the stored tokens are gone from Keychain/EncryptedSharedPreferences.
- Calling
GET /api/v1/mewith a valid token returns my user record; the same call with a missing, expired, or wrong-audience token returns 401. - My user row exists in the server DB after my first successful login, keyed by the OIDC
subclaim (no manual user creation needed). Plans: 7 plans
Plans:
- 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit
- 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and
/api/v1/me - 02-03-PLAN.md — Common OIDC/store contracts, JVM/Wasm actuals, and store contract test
- 02-04-PLAN.md — Android OIDC actual, Android secure AuthState store, and manifest callback
- 02-05-PLAN.md — iOS OIDC actual, iOS Keychain store, and URL scheme callback
- 02-06-PLAN.md — AuthSession state machine, bearer HTTP client, refresh/logout behavior, and Koin wiring
- 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):
- 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.
- 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.
- 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.
- 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. - 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: TBD 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):
- On my first login, I see an onboarding screen asking me to create a new household or enter an invite code.
- 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.
- Once both users are in the same household, any household-scoped API call returns identical data regardless of which member made it.
- A crafted API request that puts a different
household_idin the body is ignored — the server always deriveshousehold_idfrom the authenticated principal, not the payload. - 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):
- 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.
- 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_atwins with no silent data loss. - 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.
- Killing the app with pending writes in the outbox and relaunching later preserves those writes; they drain on the next sync cycle.
- 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):
- I open "Przepisy" and see a grid of recipe cards with thumbnail, title, and cooking time — fully populated from server-seeded catalog data.
- I can filter the grid by meal slot, tag, and cooking-time range, and search by title/tag text; results update as I type.
- I tap a recipe and see a detail view with ingredients (amounts + units), steps, nutrition per serving, cooking time, and any defined substitutions.
- I put the device in airplane mode, relaunch the app, and the catalog still renders from the local SQLDelight cache.
- 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):
- 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.
- 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.
- 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.
- 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.
- 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):
- On any meal entry I can substitute an ingredient with one of the catalog-defined alternatives and the change sticks after sync and restart.
- 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.
- On any meal entry I can select a specific product (pack size) for a given ingredient when multiple exist.
- 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):
- I open "Spiżarnia" and see my pantry inventory grouped by category (pieczywo, nabiał, mięso i ryby, warzywa, owoce, suche, przyprawy, inne).
- 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.
- I pick a planning horizon (e.g., "next 7 days") and see which ingredients fall short based on the plan minus current pantry.
- 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):
- 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.
- 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.
- I can undo a recently marked-bought item within the same session; the item reappears in active needs.
- 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):
- The Phase 2.1 app shell still preserves each tab's back stack after real recipe detail, planner, pantry, and shopping flows exist.
- 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.
- The app respects iOS safe areas, supports the swipe-back gesture where applicable, and keyboards never cover focused inputs.
- Typography and spacing feel noticeably calmer than the legacy PWA mockup — more whitespace between cards, larger hit targets, readable at arm's length.
- 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):
- 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.
- 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.
- 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.
- 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 | 0/0 | Not started | - |
| 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