199 lines
17 KiB
Markdown
199 lines
17 KiB
Markdown
# Recipe (working title)
|
||
|
||
## What This Is
|
||
|
||
A mobile-first meal planning app for a small household — pick recipes for the week, fill a calendar across five meal slots per day, and watch pantry gaps + shopping lists emerge from the plan. Kotlin Multiplatform targeting iOS primarily, with Android as the secondary app target and a JVM Ktor server. Built for me + my partner (shared household plan) with a handful of family/friends as authorized users on the same self-hosted backend.
|
||
|
||
## Core Value
|
||
|
||
**"My week is planned."** I pick recipes, the calendar fills up, and I know what we're eating. Everything else — pantry tracking, shopping list, nutrition numbers — exists to reinforce that one moment.
|
||
|
||
## Requirements
|
||
|
||
### Validated
|
||
|
||
(None yet — ship to validate)
|
||
|
||
### Active
|
||
|
||
**Authentication & identity**
|
||
- [ ] Users sign in via the user's self-hosted Authentik instance (OIDC)
|
||
- [ ] Sessions persist across app launches; offline access works with cached credentials
|
||
|
||
**Household sharing**
|
||
- [ ] Users can join a household: one plan, one pantry, one shopping list
|
||
- [ ] Changes by either user converge on all devices when online
|
||
|
||
**Recipes (browse & detail)**
|
||
- [ ] User can browse a curated recipe catalog with a grid view
|
||
- [ ] User can filter recipes by meal slot, tags, cooking time, and ingredients
|
||
- [ ] User can search recipes by title/tag
|
||
- [ ] User can open a recipe detail view with ingredients, steps, and nutrition per serving
|
||
|
||
**Meal planner (hero feature)**
|
||
- [ ] User can view a calendar of days and see planned meals per day
|
||
- [ ] User can add recipes to any of 5 slots/day (śniadanie, drugie śniadanie, obiad, przekąska, kolacja)
|
||
- [ ] User can remove or replace a meal entry
|
||
- [ ] User can adjust servings on a meal entry
|
||
- [ ] User can customize a meal entry: swap ingredients (substitutions), exclude ingredients, add extras, override amounts
|
||
- [ ] User can select a specific product (pack size) for an ingredient in a meal entry
|
||
- [ ] User can mark a meal slot as "skipped" for a day
|
||
- [ ] User sees daily nutrition totals (kcal, protein, fat, carbs) computed from the plan
|
||
|
||
**Pantry**
|
||
- [ ] User can view current pantry inventory grouped by category
|
||
- [ ] User can add/update quantities manually in the pantry
|
||
- [ ] User sees which ingredients fall short over a chosen planning horizon
|
||
- [ ] User can filter pantry by category and by shortfall status
|
||
|
||
**Shopping list**
|
||
- [ ] User can select days from the plan to generate a shopping list
|
||
- [ ] Shopping list aggregates ingredient needs across selected days minus pantry
|
||
- [ ] Shopping list groups items by category for an efficient store trip
|
||
- [ ] User can mark items as bought during a shopping session; marked items are removed from active needs and added to pantry
|
||
|
||
**Offline + sync**
|
||
- [ ] App is fully usable offline: read and write plans, pantry, shopping list
|
||
- [ ] Local changes sync to the backend when connectivity returns, without data loss
|
||
- [ ] Conflicts between two household members' concurrent edits resolve deterministically (last-write-wins for MVP; revisit if it hurts)
|
||
|
||
**Polish UI foundation**
|
||
- [ ] All user-facing strings are externalized into resource files (i18n-ready), even though v1 ships Polish only
|
||
- [ ] UI uses a Liquid-Glass-inspired visual language (translucent surfaces, blur, soft depth) implemented in Compose Multiplatform
|
||
- [ ] Signed-in users have a real app shell early: main menu/tab chrome, empty Planner / Recipes / Pantry / Shopping views, and a working search affordance before domain data arrives
|
||
- [ ] Visual hierarchy is less cramped than the mockup (more breathing room, calmer typography)
|
||
- [ ] iOS app feels iOS-idiomatic within Compose's constraints (tab bar placement, navigation patterns, safe areas, dark mode)
|
||
|
||
### Out of Scope
|
||
|
||
**For v1 (deferred to later phases / milestones):**
|
||
- In-app recipe authoring — *v1 seeds the DB manually; authoring in-app comes next phase*
|
||
- Recipe sharing between users/households — *future feature; households are isolated in v1*
|
||
- Nutrition goal tracking (targets, streaks, deficits) — *v1 shows numbers informationally only*
|
||
- English and other language copy — *code is i18n-ready but v1 ships Polish only*
|
||
- True native iOS 26 Liquid Glass via SwiftUI interop — *Compose approximation for v1; revisit only if real-device chrome feels clearly inadequate*
|
||
- Desktop and Wasm app targets — *removed from the v1 target matrix to keep the build focused on iOS, Android, and the JVM server*
|
||
- Sign in with Apple as a first-class button — *user's Authentik handles auth; Apple can be federated upstream in Authentik if needed later*
|
||
- Barcode scanning / receipt OCR for pantry updates — *manual entry is fine for a 2-person household*
|
||
- AI-generated recipes — *curated catalog is the value*
|
||
|
||
**Permanently out of scope (explicit exclusions):**
|
||
- Social features: comments, ratings, recipe feeds, public profiles — *this is a private household app, not a community product*
|
||
- Meal-plan marketplaces / paid plans — *personal-use product*
|
||
- Grocery delivery integrations (Instacart, etc.) — *Polish market + small scope; not worth the integration cost*
|
||
|
||
**Deliberately not carried forward from the mockup:**
|
||
- The mockup's seed data (~80 ingredients, ~30 recipes) — *user chose to start the catalog fresh*
|
||
- The mockup's visual design — *full visual rebuild; mockup is functional reference only*
|
||
- The mockup's localStorage data model — *server-backed with local cache replaces it*
|
||
|
||
## Context
|
||
|
||
**Codebase state.** The `~/dev/repo/recipe` directory has four modules: `composeApp` (Android + iOS shared UI), `iosApp` (iOS bootstrap), `server` (Ktor), and `shared` (common domain/DTO code). `shared` still has a JVM target because the server consumes it; `composeApp` does not ship Desktop or Wasm targets in v1.
|
||
|
||
**Reference implementation.** The user built a working PWA at `~/dev/repo/recipe-mockup/` (vanilla JS + Tailwind CDN + nginx/Docker). It implements the same four views (Recipe List, Meal Planner, Pantry, Shopping List) and has mature logic worth mining as a *functional* spec — particularly planner entry customization (substitutions, amount overrides, product selection), shortfall computation over a horizon, and shopping-list aggregation with "bought" session tracking. The mockup's UI design is **not** being carried forward; the user is redesigning visuals around a Liquid-Glass-inspired language.
|
||
|
||
**Users.** Authorized users only, behind the user's Authentik. Primary user is the author; secondary is their partner (household sharing from day 1); a handful of family/friends may use their own household accounts. Not an App Store public launch — personal / close-circle use.
|
||
|
||
**Infra.** User runs a homelab. Authentik is already installed. The Ktor backend will run on the same server (containerized). No managed cloud dependencies planned.
|
||
|
||
**Language & platform.** Polish-only UI for v1 (strings externalized for future i18n). iOS is the primary daily driver; Android deployed later for friends. Desktop and Wasm app targets are deferred out of v1.
|
||
|
||
**Liquid Glass decision.** True iOS 26 Liquid Glass (refractive material, specular highlights, morphing chrome) is a SwiftUI-native feature that Compose on iOS cannot reproduce exactly (Compose uses Skia, not Metal-native glass material). The v1 plan is: Compose-only approximation using the Liquid library for menu/search/button chrome first, with blur/translucency fallbacks where needed; measure real-device performance and visual quality; and **only** selectively add SwiftUI interop for chrome if the approximation feels insufficient. This avoids upfront interop complexity for 90%+ of the UI.
|
||
|
||
## Constraints
|
||
|
||
- **Tech stack**: Kotlin Multiplatform + Compose Multiplatform for UI, Ktor for server, Authentik OIDC for auth — Locked; aligns with user's skills + self-hosted infra
|
||
- **Primary platform**: iOS — Must feel good here first; other platforms are secondary
|
||
- **Hosting**: Self-hosted on user's existing homelab (alongside Authentik) — No managed cloud; implies containerized deploy, self-managed DB, reverse proxy
|
||
- **Offline**: Full offline read/write is required — User will use the shopping list in-store where signal is unreliable; online-only is unacceptable
|
||
- **Audience size**: ~5–10 authenticated users total — Don't over-engineer multi-tenancy, rate limiting, or horizontal scaling
|
||
- **Language**: Polish UI for v1, i18n-ready code — All strings must be externalized from day 1 to avoid costly retrofit later
|
||
- **Data seeding**: Catalog starts empty; user will author recipes directly in DB for MVP — Need admin-friendly seeding path (SQL migrations, JSON fixtures, or CLI tool)
|
||
- **Visual direction**: Liquid-Glass-inspired (Compose approximation) — Bright mockup palette is being replaced; design needs to be reworked as part of the rebuild
|
||
|
||
## Key Decisions
|
||
|
||
### Product & scope
|
||
|
||
| Decision | Rationale | Outcome |
|
||
|----------|-----------|---------|
|
||
| KMP with Compose Multiplatform for UI | iOS-primary + Android secondary; Desktop/Wasm app targets removed from v1 to keep the build focused | — Pending |
|
||
| Household-sharing from day 1 (me + partner share one plan) | Core use case is cooking together; per-user + later-sharing would force data-model rewrite | — Pending |
|
||
| Authentik OIDC as sole auth provider for MVP | User already runs Authentik; self-hosted == aligned; Apple Sign-in likely not required for App Store since Authentik is user's own IdP, not a third-party social login | — Pending |
|
||
| Server lives on user's homelab alongside Authentik | Existing infra, zero managed-cloud cost, same ops surface | — Pending |
|
||
| Offline-first with last-write-wins sync | Grocery-store usage demands offline; conflict resolution overkill for a 2-person household | — Pending |
|
||
| Compose-only Liquid Glass approximation for v1 | Real iOS 26 Liquid Glass requires SwiftUI interop; Liquid gives Compose chrome/buttons a closer approximation while keeping a single codebase; revisit only if chrome feels inadequate on real device | — Pending |
|
||
| Real app shell before household/domain work | The authenticated app should stop feeling like a placeholder before Phase 3; menu navigation, empty states, and search can be built without household data and will reduce UI churn in later phases | — Pending |
|
||
| Polish-only strings, i18n-ready infrastructure | Single-language content for v1 speed; externalized strings prevent future rewrite | — Pending |
|
||
| Start catalog fresh (don't port mockup seed data) | Mockup data is a reference, not production content; user wants to re-curate | — Pending |
|
||
| Nutrition is informational only in v1 | Keep scope tight; tracking/goals are a natural v2 if usage patterns justify | — Pending |
|
||
| Mockup is functional spec only, not visual spec | Visual direction is changing (Liquid Glass); logic is mature and worth mining | — Pending |
|
||
|
||
### Client tech stack (composeApp)
|
||
|
||
| Decision | Rationale | Outcome |
|
||
|----------|-----------|---------|
|
||
| Navigation: Jetpack Navigation Compose (CMP port) — `org.jetbrains.androidx.navigation:navigation-compose:2.9.x` | JetBrains-official recommendation on kotlinlang.org; type-safe routes via `@Serializable`; works across Android/iOS; skill transferable to Android | — Pending |
|
||
| Component foundation: Composables / Compose Unstyled | Use renderless, accessible primitives from `composables.com` for new shared controls so Recipe owns the visual language instead of inheriting Material 3's Android look. Composables One is optional only if the project has/chooses the paid kit. | — Pending |
|
||
| Architecture: ViewModel + StateFlow + method-per-action | Standard modern pattern; matches JetBrains/Google samples; lowest ceremony; upgrade individual screens to sealed-event onEvent only when they grow complex | — Pending |
|
||
| DI: Koin — `koin-core`, `koin-compose`, `koin-compose-viewmodel` | De facto KMP standard; smoothest `koinViewModel()` integration with Jetpack Nav back-stack scoping; no codegen; small surface to learn | — Pending |
|
||
| Local DB: SQLDelight 2.x | Most mature KMP DB; Wasm-ready (hedge for future Compose-for-Web target); raw SQL is a transferable skill; clear migration story via .sq files | — Pending |
|
||
| HTTP client: Ktor Client | Same team as server; first-class KMP; shared serialization config | — Pending |
|
||
| Serialization: kotlinx.serialization (JSON) | Standard; works everywhere; pairs with Ktor and SQLDelight | — Pending |
|
||
| Date/time: kotlinx.datetime | Standard; SQLDelight adapters available | — Pending |
|
||
| Logging: Kermit (Touchlab) | KMP-native logger; simple API; optional Crashlytics/Sentry bridges | — Pending |
|
||
| Image loading: Coil 3 (`io.coil-kt.coil3:coil-compose`) | First-class Compose Multiplatform support; modern API | — Pending |
|
||
| Key-value settings: `com.russhwolf:multiplatform-settings` | Small prefs (last tab, theme toggles) that don't belong in SQLDelight | — Pending |
|
||
| Glass effects: Liquid (`io.github.fletchmckee.liquid:liquid`) first for chrome/buttons; Haze only as a fallback/simple blur tool if needed | Liquid lets modifier nodes sample/manipulate pixels behind controls for a closer Liquid-Glass-style effect in Compose Multiplatform; keep effects constrained to chrome/buttons and verify on real iOS hardware | — Pending |
|
||
| Mobile OIDC: Lokksmith on Android/iOS, exposed via the KMP `OidcClient` interface | Keeps OIDC browser flow, PKCE, state/nonce, refresh, and end-session in Kotlin Multiplatform; removes the Swift AppAuth bridge and SwiftPM package from the iOS shell while still using ASWebAuthenticationSession underneath on iOS | — Pending |
|
||
|
||
### Server tech stack
|
||
|
||
| Decision | Rationale | Outcome |
|
||
|----------|-----------|---------|
|
||
| Server framework: Ktor Server 3.x | Same team as client HTTP; Kotlin-native; fits homelab deployment; coroutines throughout | — Pending |
|
||
| Database: Postgres | Homelab-friendly (Docker); JSONB for meal-entry extras; room to grow; standard skill | — Pending |
|
||
| SQL: Exposed DSL | Kotlin-backend standard; type-safe SQL builders; JSONB first-class; strong tutorial trail with Ktor. Avoid Exposed's DAO (active record) API | — Pending |
|
||
| Migrations: Flyway | Industry-standard numbered `V__.sql` files; auto-apply on startup; works with any JDBC-backed stack | — Pending |
|
||
| Token validation: `io.ktor:ktor-server-auth-jwt` | Built-in JWKS support with caching + rotation; direct integration with Authentik's OIDC endpoint | — Pending |
|
||
|
||
### Sync
|
||
|
||
| Decision | Rationale | Outcome |
|
||
|----------|-----------|---------|
|
||
| Strategy: last-write-wins with server-assigned `updated_at` per row | Household of 2 has negligible concurrent-edit risk; simplest to implement and debug; upgradeable per-table to op-log later if hurt | — Pending |
|
||
| Transport: HTTP polling (20–30s when foreground) + pull-to-refresh + debounced push after local writes | Sufficient freshness for a household; SSE is a v2 enhancement if polling feels laggy | — Pending |
|
||
| API shape: REST, versioned `/api/v1`, two sync endpoints (`POST /sync/push`, `GET /sync/pull?since=...`) plus catalog + households/invites CRUD | Versioning leaves room to evolve; minimal surface area; read-mostly catalog is heavily cached on client | — Pending |
|
||
| Server-side data model: `users`, `households`, `memberships`, `invites` + household-scoped tables carrying `household_id`, `updated_at`, `deleted_at` | Supports household sharing + invites, JIT user provisioning from OIDC `sub`, soft deletes for sync | — Pending |
|
||
|
||
### Build & module structure
|
||
|
||
| Decision | Rationale | Outcome |
|
||
|----------|-----------|---------|
|
||
| Gradle version catalog (`gradle/libs.versions.toml`) | Single source of truth for versions; standard KMP practice | — Pending |
|
||
| Convention plugins (`build-logic/` module) from day 1 | Centralizes Kotlin/Compose/test config; educational payoff; small upfront cost | — Pending |
|
||
| Keep template modules: `composeApp/`, `iosApp/`, `server/`, `shared/` — no feature modules in v1 | Feature modules don't pay off until ~10 features or multiple devs; flat is clearer at this scale | — Pending |
|
||
| `shared/commonMain` holds: domain models + API DTOs only (no UI, no HTTP, no DB) | Keeps shared dep graph minimal; both client and server depend on `shared/` | — Pending |
|
||
| `composeApp/commonMain` package layout: `app/ navigation/ ui/{theme,components,screens/{recipes,planner,pantry,shopping}} data/{local,remote,repository} domain/` | Groups by UI concern + data layer; resists premature modularization | — Pending |
|
||
|
||
## Evolution
|
||
|
||
This document evolves at phase transitions and milestone boundaries.
|
||
|
||
**After each phase transition** (via `/gsd-transition`):
|
||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||
2. Requirements validated? → Move to Validated with phase reference
|
||
3. New requirements emerged? → Add to Active
|
||
4. Decisions to log? → Add to Key Decisions
|
||
5. "What This Is" still accurate? → Update if drifted
|
||
|
||
**After each milestone** (via `/gsd-complete-milestone`):
|
||
1. Full review of all sections
|
||
2. Core Value check — still the right priority?
|
||
3. Audit Out of Scope — reasons still valid?
|
||
4. Update Context with current state
|
||
|
||
---
|
||
*Last updated: 2026-04-24 after initial tech-stack discussion*
|