---
phase: 02-authentication-foundation
plan: 01
subsystem: auth
tags: [oidc, authentik, kotlinx-serialization, kmp, ktor, gradle-version-catalog, cocoapods, appauth, exposed, testcontainers]

requires:
  - phase: 01-project-infrastructure-module-wiring
    provides: gradle version catalog, recipe.kotlin.multiplatform convention plugin, shared module scaffold (D-19), Koin/Kermit bootstrap, Ktor server skeleton with ContentNegotiation + Database.migrate, verify-shared-pure.sh + verify-no-version-literals.sh

provides:
  - dev.ulfrx.recipe.shared.Constants with OIDC_ISSUER (trailing slash), OIDC_CLIENT_ID = recipe-app, OIDC_REDIRECT_URI = recipe://callback, API_BASE_URL, SERVER_PORT (D-11)
  - dev.ulfrx.recipe.shared.dto.User (domain identity)
  - dev.ulfrx.recipe.shared.dto.MeResponse (@Serializable wire DTO with toUser() per D-27)
  - shared/build.gradle.kts wired with kotlinSerialization plugin and api(libs.kotlinx.serializationJson)
  - Phase 2 dependency aliases in gradle/libs.versions.toml (AppAuth, AndroidX Security Crypto, multiplatform-settings + coroutines, Exposed core/jdbc/java-time, HikariCP, Testcontainers postgresql + junit-jupiter, Ktor server auth/JWT/CallLogging/StatusPages, Ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio + MPP serialization-kotlinx-json) without bumping Ktor (stays 3.4.1)
  - composeApp/build.gradle.kts with kotlinSerialization + kotlin.native.cocoapods applied, cocoapods block bringing AppAuth-iOS via libs.versions.appauth.ios.get(), Phase 2 commonMain/androidMain/iosMain/jvmMain dependency wiring, manifestPlaceholders["appAuthRedirectScheme"] = "recipe", and locked compose.resources packageOfResClass
  - server/build.gradle.kts with Ktor auth/JWT/CallLogging/StatusPages, Exposed DSL trio, Hikari, kotlinx.serialization-json, plus Testcontainers test dependencies
  - docs/authentik-setup.md — reproducible Authentik OIDC provider playbook (D-10) with mandatory sections Provider/Scopes/Redirect URI/Server Env Vars/Logout/Manual UAT/Source Audit, plus an exhaustive multi-source audit table mapping AUTH-01..AUTH-06, CONTEXT D-01..D-34, RESEARCH constraints, UI-SPEC, VALIDATION Wave 0, and PATTERNS file map to either this doc or a downstream Phase 2 plan, with all deferred ideas explicitly excluded

affects:
  - 02-02-PLAN.md (server JWT validation, JIT users, /api/v1/me — depends on shared MeResponse DTO + ktor server auth/jwt/exposed/hikari/testcontainers aliases)
  - 02-03-PLAN.md (common OIDC/store contracts — depends on Constants, multiplatform-settings, ktor client, Coroutines stack)
  - 02-04-PLAN.md (Android AppAuth actual + secure store — depends on libs.appauth + libs.androidx.security.crypto + manifestPlaceholders bootstrap)
  - 02-05-PLAN.md (iOS AppAuth actual — depends on cocoapods AppAuth pod + libs.ktor.clientDarwin)
  - 02-06-PLAN.md (LoginScreen/PostLoginPlaceholder UI — depends on MeResponse DTO and AuthSession contract from 02-03)
  - 02-07-PLAN.md (integration glue / phase verification — depends on every prior plan)

tech-stack:
  added:
    - kotlinx-serialization-json (api scope in shared/commonMain)
    - kotlinSerialization Gradle plugin on shared/composeApp/server (server already had it)
    - kotlin.native.cocoapods plugin on composeApp (applied by id; bundled with KGP)
    - AppAuth-Android (net.openid:appauth 0.11.1)
    - AppAuth-iOS (CocoaPod 2.0.0) — Gradle CocoaPods DSL pulls it via libs.versions.appauth.ios.get()
    - androidx.security:security-crypto 1.1.0 (Android secure AuthState store)
    - com.russhwolf:multiplatform-settings + multiplatform-settings-coroutines 1.3.0
    - Ktor client family (core/auth/content-negotiation/logging) + engines (okhttp Android, darwin iOS, cio JVM)
    - Ktor server auth + auth-jwt + call-logging + status-pages (3.4.1, no patch bump)
    - Exposed 0.55.0 (core + jdbc + java-time) and HikariCP 6.2.1
    - Testcontainers 1.21.4 (postgresql + junit-jupiter)
  patterns:
    - Shared DTO contract pattern — kotlinx.serialization @Serializable data class with explicit camelCase wire keys, decoded with ignoreUnknownKeys for forward compat (Phase 3 will add householdId)
    - Catalog-only version pinning — every Phase 2 dependency declared in gradle/libs.versions.toml; module build files reference libs.* only; verify-no-version-literals.sh enforces it
    - Cocoapods-via-catalog pattern — pod("AppAuth") { version = libs.versions.appauth.ios.get() } keeps the build script literal-free even with native Pod integration
    - Compose Resources package locking — explicit compose.resources { packageOfResClass = "..." } isolates UI code from build-script identity changes (group/version)
    - Authentik provider audit — markdown audit table that traces every locked source (REQ/CONTEXT/RESEARCH/UI-SPEC/VALIDATION/PATTERNS) to either an in-doc anchor or a downstream plan, with deferred ideas explicitly listed

key-files:
  created:
    - shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt
    - shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt
    - shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt
    - shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt
    - docs/authentik-setup.md
  modified:
    - gradle/libs.versions.toml
    - shared/build.gradle.kts
    - composeApp/build.gradle.kts
    - server/build.gradle.kts
    - .gitignore (added *.podspec — generated by cocoapods plugin)

key-decisions:
  - "Apply kotlin.native.cocoapods plugin by id, not via libs.plugins alias — the plugin ships inside the Kotlin Gradle plugin already on the classpath via recipe.kotlin.multiplatform; aliasing it forces a duplicate version request that Gradle rejects with 'already on the classpath, compatibility cannot be checked'."
  - "Add manifestPlaceholders[\"appAuthRedirectScheme\"] = \"recipe\" to composeApp Android defaultConfig from Plan 02-01 (not Plan 02-04). AppAuth-Android's bundled manifest declares a ${appAuthRedirectScheme} placeholder that breaks AGP merge as soon as the dependency is on the classpath, even before any auth wiring. Setting it here is a Rule 3 prerequisite for the dependency to be cataloged."
  - "Lock compose.resources packageOfResClass to the Phase 1 historical name. Adding top-level group = \"dev.ulfrx.recipe\" (required by the cocoapods plugin's podspec generator) shifts the generated Res-class package from recipe.composeapp.generated.resources to dev.ulfrx.recipe.composeapp.generated.resources, breaking Phase 1 App.kt imports. Locking the package keeps the diff inside Plan 02-01's stated files."
  - "Ship a *.podspec gitignore entry. The Kotlin CocoaPods plugin regenerates composeApp/composeApp.podspec on every Gradle sync and that file legitimately contains 'AppAuth', '2.0.0' as a literal pin (CocoaPods semantics). Tracking it would either fail verify-no-version-literals.sh (if the verifier ever extends to *.podspec) or churn on every clean build."
  - "kotlinx.serialization-json declared as api(...) in shared/commonMain so consumers (composeApp, server) inherit the @Serializable runtime without each re-declaring it. shared/commonMain stays free of Ktor / Compose / SQLDelight / Koin / Kermit per D-19 / INFRA-06."
  - "Use the MPP variant of ktor-serialization-kotlinx-json for composeApp/commonMain (io.ktor:ktor-serialization-kotlinx-json) and keep the -jvm variant for the server module. Mixing variants between modules is the supported pattern; introducing a single MPP variant on the server breaks the existing ktor.serializationKotlinxJson alias used by the (jvm-only) server."
  - "Server-side OIDC config (OIDC_ISSUER / OIDC_AUDIENCE / OIDC_JWKS_URL) is documented as env-var-driven in docs/authentik-setup.md (D-12) but the actual application.conf wiring is deferred to Plan 02-02. Plan 02-01 establishes the contract; 02-02 implements it."

patterns-established:
  - "Shared DTO purity: shared/commonMain depends only on kotlin stdlib + kotlinx.serialization. Verified by ./tools/verify-shared-pure.sh."
  - "Catalog discipline: every library and plugin version lives in gradle/libs.versions.toml; Gradle artifact identity (group/version) is allowed at the top of module build files but library/plugin pins are not. Verified by ./tools/verify-no-version-literals.sh."
  - "TDD gate sequence: RED commit (test(02-01)) followed by GREEN commit (feat(02-01)) — the Phase 2 plans don't all use TDD but Plan 02-01's Task 1 sets the precedent for downstream auth plans."
  - "Multi-source audit pattern: docs/authentik-setup.md ## Source Audit table is the template. Future phase docs that span multiple sources (REQ + CONTEXT + RESEARCH + VALIDATION + PATTERNS) should mirror this structure so audits stay reproducible."

requirements-completed: []

duration: 16m
completed: 2026-04-28
---

# Phase 02 Plan 01: Shared Auth Contracts, Dependency Aliases, and Authentik Setup Summary

**Phase 2 foundation: shared MeResponse/User DTOs + Constants, full Phase 2 dependency catalog (AppAuth/Exposed/Testcontainers/Ktor auth) wired into composeApp/server without bumping Ktor 3.4.1, plus the docs/authentik-setup.md reproducible-provider playbook with multi-source audit.**

## Performance

- **Duration:** 16 min
- **Started:** 2026-04-28T08:40:29Z
- **Completed:** 2026-04-28T08:55:58Z
- **Tasks:** 3 (Task 1 ran TDD: RED + GREEN)
- **Files modified:** 9 (5 created, 4 modified)

## Accomplishments

- Locked the `/api/v1/me` wire contract: `MeResponse` is `@Serializable`, decodes with `ignoreUnknownKeys` so Phase 3 can add `householdId` without breaking Phase 2 clients, and round-trips through `toUser()` to a stable domain `User`.
- Stood up `dev.ulfrx.recipe.shared.Constants` with `OIDC_ISSUER` (trailing slash placeholder host), `OIDC_CLIENT_ID = "recipe-app"`, `OIDC_REDIRECT_URI = "recipe://callback"`, plus `API_BASE_URL` and `SERVER_PORT` — the single config object every Phase 2 plan compiles against.
- Cataloged every Phase 2 dependency (AppAuth Android + iOS pod, AndroidX Security Crypto, multiplatform-settings, Ktor client/server auth family, Exposed DSL trio, Hikari, Testcontainers) and wired them into composeApp/server without bumping Ktor off `3.4.1`. CocoaPods integration brings AppAuth-iOS via `libs.versions.appauth.ios.get()` so no version literals leak into build files (`./tools/verify-no-version-literals.sh` stays green).
- Shipped `docs/authentik-setup.md` as a 240-line reproducible Authentik provider playbook covering Provider, Scopes, Redirect URI, Server Env Vars, Logout, Manual UAT (UAT-01..UAT-04), and a Source Audit table that traces every Phase 2 input (GOAL, AUTH-01..AUTH-06, CONTEXT D-01..D-34, RESEARCH, UI-SPEC, VALIDATION Wave 0, PATTERNS) to either this doc or a downstream Phase 2 plan, with all deferred ideas explicitly excluded.

## Task Commits

1. **Task 1 RED — Failing serialization test for MeResponse DTO** — `6504b46` (test)
2. **Task 1 GREEN — Constants and MeResponse/User DTOs in shared** — `7e73a9a` (feat)
3. **Task 2 — Phase 2 dependency aliases without bumping Ktor** — `c1cc713` (feat)
4. **Task 3 — Authentik provider setup and Phase 2 source audit** — `62040d4` (docs)

_Note: TDD task 1 produced two commits (RED then GREEN); no REFACTOR commit was needed because the GREEN implementation is already minimal._

## Files Created/Modified

- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` — created. OIDC + API config object (D-11). Trailing-slash issuer, exact `recipe://callback` redirect URI, `recipe-app` client id (also `aud` per D-07).
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt` — created. Domain identity DTO; id is `String` (server UUID) so shared/commonMain stays free of UUID library deps.
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` — created. `@Serializable` wire DTO for `GET /api/v1/me` (D-27). One-to-one `toUser()` mapper. Forward-compatible with Phase 3 `householdId` via `ignoreUnknownKeys` decoders.
- `shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt` — created. Three-test contract: round-trip via camelCase wire keys, Phase 3 forward-compat decode, `toUser()` no-data-loss mapping.
- `shared/build.gradle.kts` — modified. Applied `alias(libs.plugins.kotlinSerialization)`; added `api(libs.kotlinx.serializationJson)` so consumers inherit the runtime; `shared/commonMain` purity preserved (still no Ktor/Compose/SQLDelight/Koin/Kermit imports).
- `gradle/libs.versions.toml` — modified. Added Phase 2 versions/libraries/plugins per D-13/D-26/research; Ktor stays at 3.4.1.
- `composeApp/build.gradle.kts` — modified. Added kotlinSerialization + kotlin.native.cocoapods plugins; cocoapods block (AppAuth pod via catalog version); per-source-set Phase 2 deps; manifestPlaceholders for AppAuth-Android scheme; `compose.resources.packageOfResClass` lock to keep Phase 1 App.kt imports valid.
- `server/build.gradle.kts` — modified. Added Ktor server auth/JWT/CallLogging/StatusPages, Exposed core/jdbc/java-time, HikariCP, kotlinx.serialization-json, plus Testcontainers postgresql + junit-jupiter test deps.
- `docs/authentik-setup.md` — created. Reproducible Authentik playbook + Phase 2 source audit (D-10).
- `.gitignore` — modified. Ignore `*.podspec` (regenerated on every Gradle sync by the cocoapods plugin).

## Decisions Made

See frontmatter `key-decisions` for the load-bearing list. Highlights:

- **kotlin.native.cocoapods applied by id, not by alias** — the plugin ships inside KGP already on the classpath via `recipe.kotlin.multiplatform`, so a `libs.plugins.kotlinCocoapods` alias triggers a duplicate-version-request error.
- **manifestPlaceholders["appAuthRedirectScheme"] = "recipe"** lands in this plan, not 02-04 — it's a Rule 3 prerequisite for the AppAuth dependency to be cataloged at all.
- **`compose.resources.packageOfResClass` locked to Phase 1's historical package** — adding `group = "dev.ulfrx.recipe"` (mandatory for the cocoapods podspec generator) would otherwise rewrite the generated `Res` package and break `App.kt` imports.
- **Ktor stays at 3.4.1** — Open Question resolved during planning; auth artifacts catalog against the same `version.ref = "ktor"`. Patch bump deferred unless a concrete incompatibility appears.
- **Server OIDC config wiring deferred to Plan 02-02** — Plan 02-01 documents the env-var contract in `docs/authentik-setup.md` (`OIDC_ISSUER`, `OIDC_AUDIENCE`, `OIDC_JWKS_URL`); Plan 02-02 implements it in `application.conf`.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Apply kotlin.native.cocoapods by id, not via `alias(libs.plugins.kotlinCocoapods)`**

- **Found during:** Task 2 verification (`:composeApp:dependencies`)
- **Issue:** `Error resolving plugin [id: 'org.jetbrains.kotlin.native.cocoapods', version: '2.3.20']: The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.` The cocoapods plugin ships bundled with the Kotlin Gradle plugin classpath that `recipe.kotlin.multiplatform` already brings in.
- **Fix:** Apply via `id("org.jetbrains.kotlin.native.cocoapods")` (no version) inside the `plugins { ... }` block of `composeApp/build.gradle.kts`. Kept the `kotlinCocoapods` alias in `gradle/libs.versions.toml` per the plan's stated catalog additions; downstream plans can still reference `libs.versions.kotlinCocoapods.version` if they ever need the version programmatically.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:dependencies` and `:composeApp:compileKotlinIosSimulatorArm64` (with `cinteropAppAuthIosSimulatorArm64`) now pass.
- **Committed in:** `c1cc713`

**2. [Rule 3 - Blocking] Add `manifestPlaceholders["appAuthRedirectScheme"] = "recipe"` to composeApp Android defaultConfig**

- **Found during:** Task 2 (`:composeApp:compileDebugKotlinAndroid`)
- **Issue:** `Manifest merger failed : Attribute data@scheme at AndroidManifest.xml requires a placeholder substitution but no value for <appAuthRedirectScheme> is provided.` AppAuth-Android's bundled manifest declares an unsubstituted `${appAuthRedirectScheme}` placeholder that breaks AGP's merger as soon as the dependency is on the classpath, even before Plan 02-04 wires the full `<intent-filter>`.
- **Fix:** Added the placeholder to `defaultConfig.manifestPlaceholders`. Value is `"recipe"`, byte-for-byte consistent with `Constants.OIDC_REDIRECT_URI = "recipe://callback"`. Plan 02-04 will still land the explicit `<intent-filter>` in the Android manifest; this placeholder satisfies AppAuth's *built-in* manifest entry until then.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:compileDebugKotlinAndroid` now passes.
- **Committed in:** `c1cc713`

**3. [Rule 3 - Blocking] Lock `compose.resources.packageOfResClass` to the Phase 1 historical package**

- **Found during:** Task 2 (`:composeApp:compileDebugKotlinAndroid`, after the AppAuth manifest fix)
- **Issue:** Adding `group = "dev.ulfrx.recipe"` and `version = "1.0.0"` at the top of `composeApp/build.gradle.kts` (mandatory for the Kotlin CocoaPods plugin's podspec generator — `cocoapods` requires `project.version` if the block doesn't override it) shifted the Compose Resources `Res` class generated package from `recipe.composeapp.generated.resources` (Phase 1) to `dev.ulfrx.recipe.composeapp.generated.resources`, breaking `App.kt`'s `import recipe.composeapp.generated.resources.Res` and `compose_multiplatform`.
- **Fix:** Added an explicit `compose.resources { packageOfResClass = "recipe.composeapp.generated.resources" }` block to `composeApp/build.gradle.kts`, locking the generated package to the Phase 1 name regardless of `group`. This keeps Plan 02-01's diff inside its stated files; Plan 02-06 will replace `App.kt`'s template body with the real auth gate (D-30) and can choose to migrate the package then.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:compileDebugKotlinAndroid` now passes; `App.kt` imports unchanged.
- **Committed in:** `c1cc713`

**4. [Rule 3 - Housekeeping] Ignore `*.podspec` files generated by the cocoapods plugin**

- **Found during:** Task 2 (`git status` after first cocoapods Gradle invocation)
- **Issue:** Adding the cocoapods plugin causes `composeApp/composeApp.podspec` to be regenerated on every Gradle sync. The file legitimately embeds `'AppAuth', '2.0.0'` as a literal CocoaPods version pin (CocoaPods Ruby DSL semantics), which would either fail `./tools/verify-no-version-literals.sh` if it ever extended to `.podspec` or churn on every clean build.
- **Fix:** Added `*.podspec` to `.gitignore` with an explanatory comment.
- **Files modified:** .gitignore
- **Verification:** `git status --short` shows no untracked `composeApp.podspec`.
- **Committed in:** `c1cc713`

**5. [Rule 1 - Bug] Strip "version = \"2.0.0\"" substring from a comment in composeApp/build.gradle.kts**

- **Found during:** Task 2 (`./tools/verify-no-version-literals.sh`)
- **Issue:** A comment paraphrased the Plan 02-01 acceptance criterion using the literal text `version = "2.0.0"`. The verifier doesn't distinguish comments from code and flagged the line. The plan's `! grep -q 'version = "2.0.0"' composeApp/build.gradle.kts` acceptance criterion ALSO reads from comments, so the comment was fundamentally incompatible with the rule.
- **Fix:** Rewrote the comment to describe the rule without quoting the forbidden literal pattern.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `./tools/verify-no-version-literals.sh` exits 0; both `! grep` acceptance criteria now hold.
- **Committed in:** `c1cc713`

**6. [Rule 1 - Bug] Use `debugCompileClasspath` instead of nonexistent `androidMainCompileClasspath` in Task 2 verify command**

- **Found during:** Task 2 (`:composeApp:dependencies --configuration androidMainCompileClasspath`)
- **Issue:** Plan 02-01 Task 2 specifies `--configuration androidMainCompileClasspath`, but under the current AGP/Gradle/Kotlin combination the actual configuration name is `debugCompileClasspath` (or `releaseCompileClasspath`). The plan's command name doesn't exist in the configuration container.
- **Fix:** Ran the functionally equivalent command (`./gradlew :composeApp:dependencies --configuration debugCompileClasspath :server:dependencies --configuration runtimeClasspath`) which resolves the same Phase 2 deps the plan was checking for. Documented in the Task 2 commit message so a future planner can update the plan if needed.
- **Files modified:** none — this is a verification-command rename, not a code change.
- **Verification:** Both classpaths resolve cleanly and contain every Phase 2 dep (AppAuth, AndroidX Security Crypto, Ktor client family, Exposed core/jdbc/java-time, Hikari, Ktor server auth-jwt/call-logging/status-pages, Testcontainers).
- **Committed in:** N/A (procedural; no code change)

---

**Total deviations:** 6 auto-fixed (5 × Rule 3 blocking, 1 × Rule 1 bug, 1 × procedural rename).
**Impact on plan:** All deviations were unavoidable consequences of cataloging the AppAuth/CocoaPods dependency stack and integrating it with Phase 1's existing build setup. Net diff stays inside Plan 02-01's `files_modified` frontmatter list (plus `.gitignore`, which is a build-hygiene artifact). Zero scope creep into Plan 02-02..02-07.

## Issues Encountered

- **STATE.md drift from orchestrator init.** Running `gsd-sdk query init.execute-phase` at agent start mutated `.planning/STATE.md` (advanced `current_plan: 0 → 1`, status `planned → executing`). Per the parallel-execution rules, worktree agents must not modify `STATE.md`; the orchestrator owns those writes. Reverted via `git checkout -- .planning/STATE.md` before staging the first commit so the orchestrator's later state update is the single source of truth. No follow-up needed.

## User Setup Required

None — Plan 02-01 is wiring + docs only. The `docs/authentik-setup.md` Manual UAT section documents what the user will need to configure in Authentik before Plan 02-02..02-07 can be exercised end-to-end, but Plan 02-01 itself doesn't require any external service interaction.

## Next Phase Readiness

- **Plan 02-02 (server JWT validation, JIT users, /api/v1/me)** — All catalog dependencies and DTOs are in place. `MeResponse` DTO is importable from `dev.ulfrx.recipe.shared.dto`; Ktor server auth/JWT/CallLogging/StatusPages, Exposed DSL trio, Hikari, and Testcontainers are wired into `server/build.gradle.kts`. `application.conf` env-var contract is documented in `docs/authentik-setup.md` § Server Env Vars; Plan 02-02 implements it.
- **Plan 02-03 (common OIDC/store contracts, JVM/Wasm actuals)** — `Constants` and `multiplatform-settings` (+ coroutines) are available in `shared`/`composeApp/commonMain`. Ktor client core/auth/content-negotiation/logging are wired into commonMain.
- **Plan 02-04 (Android AppAuth actual + secure store)** — `libs.appauth` and `libs.androidx.security.crypto` are wired into `androidMain`. The `appAuthRedirectScheme=recipe` manifest placeholder is already set; Plan 02-04 only needs to add the explicit `<intent-filter>` and the `RedirectUriReceiverActivity` registration.
- **Plan 02-05 (iOS AppAuth actual)** — The cocoapods block is configured with the `AppAuth` pod at the catalog version; `libs.ktor.clientDarwin` is in `iosMain` deps. The `Info.plist` `CFBundleURLTypes` registration is the remaining iOS step.
- **Plan 02-06 (UI: SplashScreen / LoginScreen / PostLoginPlaceholderScreen)** — No blockers; Compose Resources package is locked to the Phase 1 historical name so existing `App.kt` keeps compiling. Plan 02-06 will replace `App.kt`'s template body with the auth gate (D-30) and optionally migrate the resources package then.
- **Plan 02-07 (integration glue / phase verification)** — All Phase 2 source files in `02-VALIDATION.md` Wave 0 will exist by the end of 02-02..02-06; this plan establishes the catalog and DTOs they depend on.

**No outstanding blockers.** Phase 2's per-plan execution can proceed.

## Self-Check: PASSED

- Created files exist:
  - `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` — FOUND
  - `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt` — FOUND
  - `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` — FOUND
  - `shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt` — FOUND
  - `docs/authentik-setup.md` — FOUND
- Modified files reflect intended changes:
  - `gradle/libs.versions.toml` — FOUND (Phase 2 aliases present)
  - `shared/build.gradle.kts` — FOUND (kotlinSerialization plugin + api(libs.kotlinx.serializationJson))
  - `composeApp/build.gradle.kts` — FOUND (cocoapods + Phase 2 deps)
  - `server/build.gradle.kts` — FOUND (Ktor auth/JWT, Exposed, Hikari, Testcontainers)
  - `.gitignore` — FOUND (`*.podspec` ignore)
- Commits exist:
  - `6504b46` (test RED) — FOUND
  - `7e73a9a` (feat GREEN) — FOUND
  - `c1cc713` (Task 2 wiring) — FOUND
  - `62040d4` (Task 3 docs) — FOUND
- Plan-level verification:
  - `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata` — PASS
  - `./tools/verify-shared-pure.sh` — PASS
  - `./tools/verify-no-version-literals.sh` — PASS
  - `./gradlew :composeApp:compileDebugKotlinAndroid :server:compileKotlin` — PASS
  - `./gradlew :composeApp:compileKotlinIosSimulatorArm64` — PASS (`cinteropAppAuthIosSimulatorArm64` exercised the AppAuth pod end-to-end)

## TDD Gate Compliance

Plan 02-01 frontmatter has `type: execute` (not `type: tdd`), so plan-level RED/GREEN/REFACTOR enforcement does not apply. However, Task 1 was tagged `tdd="true"` and produced the expected gate sequence inside its scope:

- RED: `6504b46` (`test(02-01): add failing serialization test for MeResponse DTO`) — confirmed failing on `MeResponse` / `User` unresolved references.
- GREEN: `7e73a9a` (`feat(02-01): land Constants and MeResponse/User DTOs in shared`) — test now passes.
- REFACTOR: omitted; the GREEN implementation is already minimal.

---
*Phase: 02-authentication-foundation*
*Completed: 2026-04-28*
