Files
recipe/.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md

13 KiB

Phase 1: Project Infrastructure & Module Wiring - Context

Gathered: 2026-04-24 Status: Ready for planning

## Phase Boundary

Stand up a KMP client + Ktor server whose build is "boring correct" from day 1 — Gradle version catalog, build-logic/ convention plugins, iOS binary flags, a pure-Kotlin shared/ module, foundational DI + logging bootstrap, and a minimally-running Ktor server — so every later phase slots into an already-configured system. Scope is infrastructure only; no feature logic, no auth, no DB tables, no UI beyond the template screens.

## Implementation Decisions

Target matrix

  • D-01: Drop the js target from composeApp and shared. Keep wasmJs as the strategic future-web bet (per PROJECT.md "possible future target").
  • D-02: Skip iosX64 (Intel simulator / iPhone 5S-SE1). User is on Apple Silicon; no Intel-Mac contributors anticipated. Saves a full iOS compile per build.
  • D-03: Keep jvm target in composeApp for Desktop — as a dev tool only (hot-reload iteration loop). No Compose Desktop packaging config; not a release surface; not a v1 deliverable per PROJECT.md.
  • D-04: shared/ ships the exact same target set as composeApp: androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs. Plus jvm covers the server dependency.
  • D-05: Final target matrix repo-wide: androidTarget, iosArm64, iosSimulatorArm64, jvm (Desktop + Server), wasmJs.

Convention plugins (build-logic/)

  • D-06: Fine-grained plugin split (5 plugins). Each module applies only what it needs:
    • recipe.kotlin.multiplatform — KMP target matrix + JVM toolchain + common-test deps
    • recipe.compose.multiplatform — Compose Multiplatform setup (layers on top of KMP)
    • recipe.android.application — Android-app-only config (namespace, compileSdk, minSdk, targetSdk from catalog)
    • recipe.jvm.server — Ktor server JVM config
    • recipe.quality — Spotless + ktlint + compiler strictness (reusable across all modules)
  • D-07: recipe.kotlin.multiplatform locks in: the D-05 target set, JVM toolchain, framework basename convention (ComposeApp / Shared), and kotlin-test as a common-test dep. New KMP modules apply this plugin and get everything.
  • D-08: JVM toolchain: JVM 21 for server, desktop, and shared/jvm. Android bytecode target stays JVM 11 (Android 7 minSdk constraint per template). Document this split in the convention plugin comments.
  • D-09: All library versions live in gradle/libs.versions.toml. Hard rule: grep for a non-test version literal inside any build.gradle.kts returns zero matches. This is INFRA-01 Success Criterion #2. Plugin versions also routed through the catalog (aliases).

Code-quality toolchain (recipe.quality plugin)

  • D-10: Minimal baseline — ship ktlint via Spotless only. Spotless handles Kotlin + Gradle files + markdown. Commands: ./gradlew spotlessCheck, ./gradlew spotlessApply. No Detekt, no Konsist in Phase 1.
  • D-11: allWarningsAsErrors = true everywhere (configured in recipe.kotlin.multiplatform). Any Kotlin/compiler warning fails the build; forces conscious suppression rather than silent drift.
  • D-12: explicitApi() strict on shared/ only. shared/ is structurally a library (consumed by both composeApp and server as a wire-format contract); composeApp and server are app code and stay on Kotlin defaults. Configured in shared/build.gradle.kts directly, not in the KMP plugin (app modules shouldn't inherit it).
  • D-13: No git hooks. ./gradlew check is the local gate; CI gate deferred to Phase 11 (deployment). Local hooks add commit friction and are trivially bypassed.

Phase 1 "running-but-empty" scope — what's wired beyond the template

  • D-14: Koin bootstrap. Add Koin deps (koin-core, koin-compose, koin-compose-viewmodel) via recipe.kotlin.multiplatform. Call startKoin { modules(appModule) } inside App() for composeApp and MainViewController for iOS. Ship an empty appModule placeholder in composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt. Phase 2 adds authModule; Phase 4 adds syncModule; etc.
  • D-15: Kermit logger bootstrap. Add Kermit dep via recipe.kotlin.multiplatform. Set a single top-level tag ("recipe") during app init. Available from day 1 for subsequent phases.
  • D-16: Server: /health endpoint + Flyway scaffold + Postgres conn config.
    • GET /health returns 200 with a trivial JSON body.
    • Flyway Gradle plugin + runtime dep wired into server/build.gradle.kts via recipe.jvm.server; src/main/resources/db/migration/ directory created (empty). Phase 3 drops V1__init.sql into an already-working migrator.
    • application.conf reads DATABASE_URL, DATABASE_USER, DATABASE_PASSWORD from env with localhost defaults matching docker-compose.
    • Server starts and connects to Postgres on boot; fails loudly (not silently) if Postgres is unreachable.
  • D-17: docker-compose.yml at repo root defines a postgres:16 service with a named volume. README.md gets a "Local development" section. Phase 3 does not have to litigate local-Postgres setup. Authentik stays on user's homelab (not in docker-compose) but the compose file is the handle for future local services if they're ever needed.

Locked infrastructure hygiene (from PROJECT.md, enforced in Phase 1)

  • D-18: iOS binary flags added to gradle.properties: kotlin.native.binary.objcDisposeOnMain=false and kotlin.native.binary.gc=cms (INFRA-03, PITFALLS.md #1).
  • D-19: shared/commonMain stays pure: domain models + @Serializable DTOs only; no Ktor, no Compose, no SQLDelight imports. Phase 1 ships an empty package scaffold under dev.ulfrx.recipe.shared ready for Phase 2+ DTOs (INFRA-06).
  • D-20: Namespace dev.ulfrx.recipe (package root). Framework basename ComposeApp for iOS. No feature modules in v1.

Claude's Discretion

  • Exact ordering of plugin application inside each build.gradle.kts
  • Specific spotless { kotlin { ktlint(...) } } ruleset version (pick latest stable from catalog)
  • Whether application.conf or ApplicationConfig.kt code owns env-var parsing
  • Flyway cleanDisabled and baselineOnMigrate flag choices (use sane defaults for dev)
  • Whether Koin bootstrap in MainViewController uses KoinApplication vs startKoin (iOS-specific idiom)
  • Whether docker-compose.yml uses a .env file or inlines localhost defaults
  • The exact sentinel JSON body for /health (empty object is fine)

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Product + scope anchors

  • .planning/PROJECT.md — Locked tech stack (§ Key Decisions), constraints, module structure rules
  • .planning/REQUIREMENTS.md — INFRA-01, INFRA-02, INFRA-03, INFRA-06 are the in-scope requirements for this phase
  • .planning/ROADMAP.md § "Phase 1: Project Infrastructure & Module Wiring" — phase goal + 5 success criteria; ordering rationale for subsequent phases

Architecture + pitfalls

  • .planning/research/ARCHITECTURE.md — Recommended project structure (§ Recommended Project Structure) defines the composeApp/commonMain package layout that Phase 1 scaffolds; § Build Order Implication explains why the foundation-first order matters
  • .planning/research/PITFALLS.md — Phase 1 must prevent pitfalls #1 (K/N GC + objcDisposeOnMain), #2 (legacy freeze/SharedImmutable — Kotlin 2.x only), #5 (newSuspendedTransaction, not relevant in Phase 1 but plugin must not preclude it), #6 (DSL-only Exposed, infra impact only)
  • .planning/research/SUMMARY.md § "Phase 1: Project infrastructure + module wiring" — executive summary of the research-driven rationale

Project convention

  • CLAUDE.md — Non-negotiable conventions (§ Non-negotiable conventions). Items #5 (Exposed DSL only), #7 (iOS binary flags day 1), #8 (shared/commonMain stays light), #9 (strings externalized from day 1 — Phase 1 scaffold only, real copy in Phase 11) all touch Phase 1.

No external ADRs or specs yet — project is greenfield; decisions flow from PROJECT.md + research/ files.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable assets (what the template already gives us)

  • gradle/libs.versions.toml exists and is the catalog. Needs to grow; does not need to be created.
  • gradle.properties exists with basic Gradle memory + Android settings. Missing iOS binary flags (D-18 adds them).
  • settings.gradle.kts already enables TYPESAFE_PROJECT_ACCESSORS — keep it.
  • Compose Multiplatform hot reload already works for Desktop (commit c50d747). The recipe.compose.multiplatform convention plugin should preserve that wiring.
  • composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt is the template App(). Koin startKoin { } call goes here.
  • server/src/main/kotlin/dev/ulfrx/recipe/Application.kt is the template Ktor module. /health route + Flyway bootstrap go here.
  • iosApp/iosApp/iOSApp.swift + ContentView.swift — the MainViewController hookup for iOS lives here; that's where iOS-side startKoin + ComposeUIViewController wiring lands.

Established patterns

  • JetBrains KMP template conventions (plugin application style, source-set DSL) — Phase 1 refactors into convention plugins but must not break template compatibility (future template updates are an informal escape hatch).
  • gradle/libs.versions.toml uses version.ref = "..." aliases — continue that pattern; do not introduce inline versions.

Integration points

  • Each module's build.gradle.kts replaces its plugins { alias(...) } block with plugins { id("recipe.kotlin.multiplatform"); id("recipe.quality"); ... }. The actual alias-based plugins (kotlinMultiplatform, composeMultiplatform, etc.) are applied inside the convention plugins, so modules no longer touch libs.plugins.*.
  • Root build.gradle.kts keeps its apply false declarations for now (Gradle's plugin classloader hint); convention plugins rely on those declarations being present in the root build.
  • build-logic/ is its own included build (includeBuild("build-logic") in settings.gradle.kts) — standard Gradle pattern, not a regular module.

What must NOT change in Phase 1

  • Package namespace (dev.ulfrx.recipe) — locked in CLAUDE.md and every existing file.
  • Android minSdk 24 / compileSdk 36 / targetSdk 36 — locked in libs.versions.toml.
  • Kotlin version (2.3.20), AGP (8.11.2), Compose Multiplatform (1.10.3), Ktor (3.4.1) — current template versions, upgraded only if catalog-wide bump becomes necessary.

</code_context>

## Specific Ideas
  • "Fine-grained conventions" means a module's plugins block reads like a role declaration. composeApp/build.gradle.kts should literally say: "I am a Kotlin Multiplatform module, I use Compose, I am an Android application, I follow the quality rules." No hidden Compose config leaking into shared/.
  • ./gradlew build succeeds green is the verification ritual. Any deviation from Phase 1 AC#1 is a regression. Every plan in this phase should end with that check.
  • Android minSdk 24 stays. Partner's phones are modern enough; Android is secondary anyway. Revisit only if a library requires higher.
  • docker-compose.yml is dev-ergonomics, not deploy infra. Phase 11 handles the real homelab deploy (separate compose file on the homelab, alongside Authentik).
## Deferred Ideas
  • Detekt static analysis — skip day 1; add only if code review starts missing the same classes of bug. Revisit criterion: "we've had 3+ PR comments that Detekt would have caught."
  • Konsist architecture fitness tests — revisit ~Phase 4 (SyncEngine) when cross-layer rules like "repositories never import Ktor Client" or "no HTTP from composeApp/ui/" become meaningful to police. Pattern 2 in ARCHITECTURE.md is the first rule that deserves a fitness test.
  • CI pipeline (GitHub Actions or homelab runner) — Phase 11 per ROADMAP.md. Phase 1 is single-dev, local-build-only.
  • Git hooks — considered and explicitly rejected; revisit only if local formatting drift becomes a recurring problem.
  • explicitApi for composeApp and server — considered; rejected because both are app code, not libraries. Only shared/ gets the discipline.
  • iosX64 target — rejected; revisit only if an Intel-Mac contributor joins.
  • js target — rejected; wasmJs covers the future-web ambition alone.
  • Compose Desktop packaging (dmg/msi/exe) — Desktop is dev-tool only in v1; full packaging is out of scope entirely.
  • Konsist, Detekt, CI listed above are the candidates most likely to be revisited first.

Phase: 01-project-infrastructure-module-wiring Context gathered: 2026-04-24