diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index ee2a39b..78b7a71 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -113,6 +113,8 @@ A mobile-first meal planning app for a small household — pick recipes for the ## Key Decisions +### Product & scope + | Decision | Rationale | Outcome | |----------|-----------|---------| | KMP with Compose Multiplatform for UI | iOS-primary + Android secondary + Desktop/Wasm optional; single codebase for 90%+ of UI | — Pending | @@ -126,6 +128,52 @@ A mobile-first meal planning app for a small household — pick recipes for the | 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/Desktop/Wasm; skill transferable to Android | — 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/blur effects: Haze (`dev.chrisbanes.haze:haze`) | Purpose-built for glass UI in CMP; handles content capture + efficient re-blur; multiplatform | — Pending | +| Mobile OIDC: AppAuth (Android) + ASWebAuthenticationSession wrapper (iOS), exposed via KMP interface | Platform-native OAuth flows; no cross-platform auth library mature enough yet for this in 2026 | — 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. @@ -144,4 +192,4 @@ This document evolves at phase transitions and milestone boundaries. 4. Update Context with current state --- -*Last updated: 2026-04-23 after initialization* +*Last updated: 2026-04-24 after initial tech-stack discussion*