13 KiB
Phase 1: Project Infrastructure & Module Wiring - Context
Gathered: 2026-04-24 Status: Ready for planning
## Phase BoundaryStand 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.
Target matrix
- D-01: Drop the
jstarget fromcomposeAppandshared. KeepwasmJsas 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
jvmtarget incomposeAppfor 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 ascomposeApp:androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs. Plusjvmcovers 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 depsrecipe.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 configrecipe.quality— Spotless + ktlint + compiler strictness (reusable across all modules)
- D-07:
recipe.kotlin.multiplatformlocks in: the D-05 target set, JVM toolchain, framework basename convention (ComposeApp/Shared), andkotlin-testas 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 anybuild.gradle.ktsreturns 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 = trueeverywhere (configured inrecipe.kotlin.multiplatform). Any Kotlin/compiler warning fails the build; forces conscious suppression rather than silent drift. - D-12:
explicitApi()strict onshared/only.shared/is structurally a library (consumed by both composeApp and server as a wire-format contract);composeAppandserverare app code and stay on Kotlin defaults. Configured inshared/build.gradle.ktsdirectly, not in the KMP plugin (app modules shouldn't inherit it). - D-13: No git hooks.
./gradlew checkis 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) viarecipe.kotlin.multiplatform. CallstartKoin { modules(appModule) }insideApp()for composeApp andMainViewControllerfor iOS. Ship an emptyappModuleplaceholder incomposeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt. Phase 2 addsauthModule; Phase 4 addssyncModule; 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:
/healthendpoint + Flyway scaffold + Postgres conn config.GET /healthreturns 200 with a trivial JSON body.- Flyway Gradle plugin + runtime dep wired into
server/build.gradle.ktsviarecipe.jvm.server;src/main/resources/db/migration/directory created (empty). Phase 3 dropsV1__init.sqlinto an already-working migrator. application.confreadsDATABASE_URL,DATABASE_USER,DATABASE_PASSWORDfrom 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.ymlat repo root defines apostgres:16service with a named volume.README.mdgets 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=falseandkotlin.native.binary.gc=cms(INFRA-03, PITFALLS.md #1). - D-19:
shared/commonMainstays pure: domain models +@SerializableDTOs only; no Ktor, no Compose, no SQLDelight imports. Phase 1 ships an empty package scaffold underdev.ulfrx.recipe.sharedready for Phase 2+ DTOs (INFRA-06). - D-20: Namespace
dev.ulfrx.recipe(package root). Framework basenameComposeAppfor 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.conforApplicationConfig.ktcode owns env-var parsing - Flyway
cleanDisabledandbaselineOnMigrateflag choices (use sane defaults for dev) - Whether Koin bootstrap in
MainViewControllerusesKoinApplicationvsstartKoin(iOS-specific idiom) - Whether
docker-compose.ymluses a.envfile 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 thecomposeApp/commonMainpackage 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.tomlexists and is the catalog. Needs to grow; does not need to be created.gradle.propertiesexists with basic Gradle memory + Android settings. Missing iOS binary flags (D-18 adds them).settings.gradle.ktsalready enablesTYPESAFE_PROJECT_ACCESSORS— keep it.- Compose Multiplatform hot reload already works for Desktop (commit
c50d747). Therecipe.compose.multiplatformconvention plugin should preserve that wiring. composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.ktis the template App(). KoinstartKoin { }call goes here.server/src/main/kotlin/dev/ulfrx/recipe/Application.ktis the template Ktor module./healthroute + Flyway bootstrap go here.iosApp/iosApp/iOSApp.swift+ContentView.swift— the MainViewController hookup for iOS lives here; that's where iOS-sidestartKoin+ComposeUIViewControllerwiring 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.tomlusesversion.ref = "..."aliases — continue that pattern; do not introduce inline versions.
Integration points
- Each module's
build.gradle.ktsreplaces itsplugins { alias(...) }block withplugins { 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 touchlibs.plugins.*. - Root
build.gradle.ktskeeps itsapply falsedeclarations 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")insettings.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.ktsshould 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 intoshared/. ./gradlew buildsucceeds 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).
- 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.
jstarget — rejected;wasmJscovers 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