---
phase: 01-project-infrastructure-module-wiring
plan: 04
subsystem: client-bootstrap
tags: [koin, kermit, di, logging, ios-bridge, android-application, wasm-bootstrap]
requires:
  - 01-02 (build-logic conventions providing Koin + Kermit dependencies via recipe.kotlin.multiplatform)
  - 01-03 (composeApp/build.gradle.kts wired to convention plugin + libs.koin.android in androidMain)
provides:
  - "initKoin(config: KoinAppDeclaration?): KoinApplication — single bootstrap helper"
  - "appModule: Koin Module — empty placeholder; Phase 2+ extends with authModule, syncModule, catalogModule"
  - "configureLogging() — sets Kermit Logger.setTag(\"recipe\")"
  - "KoinIosKt.doInitKoin() — Swift-callable iOS bridge"
  - "MainApplication: Android Application subclass invoking configureLogging + initKoin on process boot"
affects:
  - "All future phases (2-11) plug Koin modules into appModule and call Logger.x { } via Kermit"
  - "Phase 2 (Auth) will register authModule; Phase 4 (SyncEngine) will register syncModule singleton"
tech-stack:
  added: []
  patterns:
    - "Single initKoin() call site per platform entry point (PITFALL #4 — no double-init on iOS)"
    - "configureLogging() ALWAYS precedes initKoin() so Koin module loading can use Kermit"
    - "App.kt (@Composable) NEVER calls startKoin (Pattern 4 anti-pattern guard)"
    - "iOS Kotlin bridge: top-level fun doInitKoin in KoinIos.kt → Swift symbol KoinIosKt.doInitKoin"
    - "Wasm init order: configureLogging → initKoin → ComposeViewport (PITFALL #8)"
key-files:
  created:
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt
    - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt
    - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt
  modified:
    - composeApp/src/androidMain/AndroidManifest.xml
    - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
    - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
    - iosApp/iosApp/iOSApp.swift
  unchanged_by_design:
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt  # anti-pattern guard: no startKoin in @Composable
    - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt  # PITFALL #4: Koin started exclusively in iOSApp.init()
    - iosApp/iosApp/ContentView.swift  # already wraps MainViewControllerKt.MainViewController()
decisions:
  - "Kermit tag = \"recipe\" (D-15) — exact string"
  - "appModule is empty in Phase 1 (D-14); Phase 2+ adds modules"
  - "Single iOS Koin call site is iOSApp.init() (PITFALL #4 mitigation)"
  - "androidContext(this@MainApplication) — qualified `this` because initKoin lambda receiver is KoinApplication"
metrics:
  tasks_completed: 3
  tasks_total: 3
  files_created: 5
  files_modified: 4
  duration: ~10m
  completed: 2026-04-24
---

# Phase 1 Plan 4: Koin + Kermit Bootstrap Wiring — Summary

Wired the Koin DI container and Kermit structured logger across all four composeApp platform entry points (Android Application subclass, iOS SwiftUI App.init, JVM desktop main, Wasm browser main) with a single `initKoin()` helper in commonMain and an empty `appModule` placeholder that Phase 2+ extends.

## What was built

### Task 1 — commonMain DI + logging + iOS bridge (commit `cc5002d`)

Created four files:

- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`** — exports `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { config?.invoke(this); modules(appModule) }`. The optional `config` lambda is how Android passes `androidContext(...)` and how Phase 2+ tests can inject overrides without touching the helper itself.
- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`** — declares `val appModule = module { }` (empty per D-14). Phase 2 adds `authModule`, Phase 4 adds `syncModule`, etc.
- **`composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`** — `fun configureLogging() { Logger.setTag("recipe") }`. Kermit's per-platform writers (OSLog/LogCat/println) install themselves by default; setting the tag is the only required call.
- **`composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`** — `fun doInitKoin() { configureLogging(); initKoin() }`. The top-level `fun` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` automatically (Kotlin/Native generates `<FileName>Kt` for top-level decls).

### Task 2 — Android MainApplication + manifest (commit `8cd608a`)

- **`composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`** — `class MainApplication : Application()` whose `onCreate()` calls `super.onCreate()`, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }`. Qualified `this@MainApplication` is required because the `initKoin { }` lambda receiver is `KoinApplication`, not the `Application`.
- **`composeApp/src/androidMain/AndroidManifest.xml`** — added `android:name=".MainApplication"` as the first attribute on `<application>`. All other attributes and the `<activity>`/`<intent-filter>` subtree preserved verbatim.

### Task 3 — JVM + Wasm + Swift entry points (commit `fd3e7e1`)

- **`composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`** — converted `fun main() = application { ... }` (single-expression) into a body block: `configureLogging()` → `initKoin()` → `application { Window(title = "recipe") { App() } }`. Window title and exit handler preserved.
- **`composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`** — same init order before `ComposeViewport { App() }`. `@OptIn(ExperimentalComposeUiApi::class)` retained. Defensive against PITFALL #8 (Wasm composition running before DI is ready) — Phase 1 has no ViewModels so the symptom would not surface yet, but the shape is correct from day 1.
- **`iosApp/iosApp/iOSApp.swift`** — added `import ComposeApp` (matches framework basename set by `recipe.kotlin.multiplatform`) and `init() { KoinIosKt.doInitKoin() }`. The `WindowGroup { ContentView() }` body is unchanged. `MainViewController.kt` and `ContentView.swift` were intentionally NOT modified — Koin is bootstrapped exclusively from `iOSApp.init()` (PITFALL #4 mitigation).

## Init order invariant (every platform)

```
configureLogging()   → installs Kermit tag "recipe"
initKoin()           → starts Koin with empty appModule
[platform composition entry — application { } / ComposeViewport { } / ComposeUIViewController { } / setContent { }]
```

## Deviations from Plan

None — plan executed exactly as written. All 3 tasks completed; all artifacts produced; all `<acceptance_criteria>` satisfied.

## Confirmations (per `<output>` section of PLAN)

- Kermit tag = `"recipe"` (D-15) — set in `configureLogging()`.
- `appModule` content: empty (D-14) — `val appModule = module { }`.
- `App.kt` NOT modified (anti-pattern guard).
- `MainViewController.kt` NOT modified (PITFALL #4 guard — Koin started outside).
- `ContentView.swift` NOT modified (already wraps `MainViewControllerKt.MainViewController()`).

## Threat Mitigations Verified

| Threat ID | Mitigation in delivered code |
|-----------|------------------------------|
| T-01-04-01 (Koin double-init iOS) | `KoinIosKt.doInitKoin()` is the only init call site on iOS; `MainViewController.kt` does not call `startKoin`. |
| T-01-04-02 (Wasm init order) | webMain `main()` orders `configureLogging() → initKoin() → ComposeViewport { }`. |
| T-01-04-03 (App.kt calling startKoin) | `App.kt` unchanged; verified no `startKoin` reference outside `Koin.kt`. |

## Verification gates

- All three task `<automated>` grep blocks passed.
- No build files modified → `tools/verify-no-version-literals.sh` and `tools/verify-shared-pure.sh` remain at exit 0.
- Compile gates (`./gradlew build`, `:composeApp:jvmTest`) deferred to Plan 07 per the verification block in 01-04-PLAN.md.

## Commits

- `cc5002d` — feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge
- `8cd608a` — feat(01-04): add Android MainApplication + manifest registration
- `fd3e7e1` — feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit

## Self-Check: PASSED

Files verified to exist on disk:
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt
- FOUND: composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt
- FOUND: composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt
- FOUND: composeApp/src/androidMain/AndroidManifest.xml (modified, contains `android:name=".MainApplication"`)
- FOUND: composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (modified)
- FOUND: composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (modified)
- FOUND: iosApp/iosApp/iOSApp.swift (modified)

Commits verified in `git log`:
- FOUND: cc5002d
- FOUND: 8cd608a
- FOUND: fd3e7e1
