31 KiB
Phase 2: Authentication Foundation - Pattern Map
Mapped: 2026-04-27 Files analyzed: 56 new/modified files or file groups Analogs found: 48 / 56
File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
gradle/libs.versions.toml |
config | build-config | gradle/libs.versions.toml |
exact |
shared/build.gradle.kts |
config | build-config | server/build.gradle.kts + shared/build.gradle.kts |
role-match |
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt |
config | transform | shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt |
role-match |
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt |
model | request-response | server/src/main/kotlin/dev/ulfrx/recipe/Application.kt Health DTO |
partial |
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt |
model | request-response | server/src/main/kotlin/dev/ulfrx/recipe/Application.kt Health DTO |
partial |
shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt |
test | transform | shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt |
role-match |
server/build.gradle.kts |
config | build-config | server/build.gradle.kts |
exact |
server/src/main/resources/application.conf |
config | request-response | server/src/main/resources/application.conf |
exact |
server/src/main/resources/db/migration/V1__users.sql |
migration | CRUD | server/src/main/resources/db/migration/.gitkeep + Database.kt Flyway path |
partial |
server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthConfig.kt |
config | request-response | server/src/main/resources/application.conf + Database.kt config reads |
role-match |
server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt |
middleware | request-response | server/src/main/kotlin/dev/ulfrx/recipe/Application.kt plugin install pattern |
role-match |
server/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.kt |
service | CRUD | server/src/main/kotlin/dev/ulfrx/recipe/Database.kt fail-loud DB boundary |
partial |
server/src/main/kotlin/dev/ulfrx/recipe/auth/UsersTable.kt |
model | CRUD | V1__users.sql planned migration + Exposed DSL decision |
no existing analog |
server/src/main/kotlin/dev/ulfrx/recipe/auth/MeRoute.kt |
route | request-response | server/src/main/kotlin/dev/ulfrx/recipe/Application.kt configureRouting() |
exact |
server/src/main/kotlin/dev/ulfrx/recipe/Application.kt |
controller | request-response | server/src/main/kotlin/dev/ulfrx/recipe/Application.kt |
exact |
server/src/main/kotlin/dev/ulfrx/recipe/Database.kt |
service | file-I/O | server/src/main/kotlin/dev/ulfrx/recipe/Database.kt |
exact |
server/src/test/kotlin/dev/ulfrx/recipe/auth/JwtAuthTest.kt |
test | request-response | server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt |
exact |
server/src/test/kotlin/dev/ulfrx/recipe/auth/JwtTestSupport.kt |
test utility | transform | server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt |
partial |
composeApp/build.gradle.kts |
config | build-config | composeApp/build.gradle.kts |
exact |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt |
service | event-driven | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt expect-free common seam style |
partial |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt |
model | event-driven | shared DTO style + kotlin.test examples |
partial |
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt |
service | event-driven | composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt |
role-match |
composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt |
service | event-driven | composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt |
role-match |
composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt |
service | transform | composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt |
role-match |
composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt |
service | transform | composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt |
model | event-driven | shared model/test shape |
partial |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt |
service | event-driven | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt Koin singleton hook |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/TokenStore.kt |
service | file-I/O | tools/verify-shared-pure.sh persistence-boundary guard |
partial |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthHttpClient.kt |
service | request-response | no Ktor client analog; use research Ktor bearer pattern | no existing analog |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/MeClient.kt |
service | request-response | server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt client GET pattern |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt |
provider | dependency-injection | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt |
exact |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt |
provider | dependency-injection | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt |
exact |
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SettingsFactory.android.kt |
service | file-I/O | MainApplication.kt Android context injection |
role-match |
composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SettingsFactory.ios.kt |
service | file-I/O | KoinIos.kt iOS actual bridge |
partial |
composeApp/src/*Main/kotlin/dev/ulfrx/recipe/auth/HttpClientEngine.*.kt |
utility | request-response | platform main.kt target-specific files |
role-match |
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt |
controller | event-driven | MainActivity.kt |
exact |
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt |
provider | dependency-injection | MainApplication.kt |
exact |
composeApp/src/androidMain/AndroidManifest.xml |
config | event-driven | AndroidManifest.xml |
exact |
iosApp/iosApp/Info.plist |
config | event-driven | Info.plist |
exact |
iosApp/iosApp/iOSApp.swift |
controller | event-driven | iOSApp.swift |
exact |
iosApp/Podfile |
config | build-config | no existing Podfile; use composeApp/build.gradle.kts iOS framework block |
no existing analog |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt |
component | transform | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt MaterialTheme wrapper |
exact |
composeApp/src/commonMain/composeResources/values/strings.xml |
config | transform | composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml |
partial |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt |
component | event-driven | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt |
component | event-driven | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt |
controller | event-driven | no ViewModel analog; use Koin/viewmodel deps and UI-SPEC method-per-action contract | no existing analog |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt |
component | event-driven | composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt |
role-match |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt |
controller | event-driven | no ViewModel analog; use same shape as LoginViewModel |
no existing analog |
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt |
test | event-driven | composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ComposeAppCommonTest.kt |
role-match |
docs/authentik-setup.md |
docs | manual-UAT | README.md local development pattern if present; otherwise phase docs style |
partial |
Pattern Assignments
Shared DTO + Constants Files
Applies to:
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.ktshared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.ktshared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.ktshared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt
Analog: shared/build.gradle.kts, shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt, server/src/main/kotlin/dev/ulfrx/recipe/Application.kt, shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt
Shared purity pattern (shared/build.gradle.kts lines 16-20):
sourceSets {
commonMain.dependencies {
// Phase 1: intentionally empty. Domain models + DTOs land Phase 2+.
// D-19 / INFRA-06: No Ktor, Compose, SQLDelight, Koin, or Kermit here - EVER.
}
}
Public constant pattern (shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt lines 1-3):
package dev.ulfrx.recipe
public const val SERVER_PORT: Int = 8080
Serializable DTO pattern (server/src/main/kotlin/dev/ulfrx/recipe/Application.kt lines 12, 19-22):
import kotlinx.serialization.Serializable
@Serializable
private data class Health(
val status: String,
)
Test skeleton pattern (shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt lines 1-10):
package dev.ulfrx.recipe
import kotlin.test.Test
import kotlin.test.assertEquals
class SharedCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
Planner note: New shared symbols must be public because shared has explicitApi() enabled at shared/build.gradle.kts lines 9-10. Keep only Kotlin stdlib and kotlinx.serialization imports in shared DTOs.
Gradle Catalog + Module Build Files
Applies to:
gradle/libs.versions.tomlshared/build.gradle.ktsserver/build.gradle.ktscomposeApp/build.gradle.kts
Analog: existing build files
Version catalog organization (gradle/libs.versions.toml lines 1-24, 27-43, 68-79):
[versions]
kotlin = "2.3.20"
kotlinx-serialization = "1.7.3"
ktor = "3.4.1"
[libraries]
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
ktor-serverCore = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Server dependency pattern (server/build.gradle.kts lines 1-8, 27-39):
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ktor)
alias(libs.plugins.flywayPlugin)
application
id("recipe.quality")
}
dependencies {
implementation(libs.ktor.serverCore)
implementation(libs.ktor.serverNetty)
implementation(libs.ktor.serverContentNegotiation)
implementation(libs.ktor.serializationKotlinxJson)
implementation(projects.shared)
testImplementation(libs.ktor.serverTestHost)
testImplementation(libs.kotlin.testJunit)
}
Compose dependency pattern (composeApp/build.gradle.kts lines 48-69):
sourceSets {
commonMain.dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.composeViewmodel)
implementation(libs.kermit)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.components.resources)
implementation(projects.shared)
}
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
implementation(libs.koin.android)
}
}
No version literal guard (tools/verify-no-version-literals.sh lines 1-20):
VIOLATIONS=$(grep -rn -E 'version[[:space:]]*=[[:space:]]*"[0-9]' --include='*.gradle.kts' . 2>/dev/null \
| grep -v 'build-logic/build.gradle.kts' \
| grep -vE ':[0-9]+:version[[:space:]]*=[[:space:]]*"[0-9]' \
|| true)
Planner note: Put new dependency versions in gradle/libs.versions.toml; do not add library versions directly in *.gradle.kts except for explicitly justified test-only literals already called out by the plan.
Client Koin + Logging + Auth Module
Applies to:
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.ktcomposeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt- Auth singleton wiring in
AuthSession.kt
Analog: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt, Koin.kt, Logging.kt, platform bootstraps
Koin module placeholder pattern (AppModule.kt lines 1-9):
package dev.ulfrx.recipe.di
import org.koin.dsl.module
// Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
val appModule =
module {
// intentionally empty in Phase 1
}
Koin startup pattern (Koin.kt lines 1-10):
package dev.ulfrx.recipe.di
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication =
startKoin {
config?.invoke(this)
modules(appModule)
}
Client logging bootstrap (Logging.kt lines 1-8):
package dev.ulfrx.recipe.logging
import co.touchlab.kermit.Logger
fun configureLogging() {
Logger.setTag("recipe")
// Platform log writers (OSLog iOS, LogCat Android, System.out JVM/Wasm) install by default.
}
Platform init order (MainApplication.kt lines 8-15, KoinIos.kt lines 5-8):
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
configureLogging()
initKoin {
androidContext(this@MainApplication)
}
}
}
fun doInitKoin() {
configureLogging()
initKoin()
}
Planner note: Add authModule without starting Koin from composables. Wire modules from the existing initKoin path, preserving the one-start-per-platform rule.
Compose App Shell + Auth Screens
Applies to:
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.ktcomposeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.ktSplashScreen.ktLoginScreen.ktLoginViewModel.ktPostLoginPlaceholderScreen.ktPostLoginViewModel.ktcomposeApp/src/commonMain/composeResources/values/strings.xml
Analog: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt, composeResources/drawable/compose-multiplatform.xml
Current app wrapper to preserve/replace (App.kt lines 25-52):
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier =
Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
}
}
}
Resource import pattern (App.kt lines 21-23):
import org.jetbrains.compose.resources.painterResource
import recipe.composeapp.generated.resources.Res
import recipe.composeapp.generated.resources.compose_multiplatform
Compose resource file placement analog (composeResources/drawable/compose-multiplatform.xml lines 1-7):
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="450dp"
android:height="450dp"
android:viewportWidth="64"
android:viewportHeight="64">
UI contract to copy from 02-UI-SPEC.md lines 149-161:
AuthState.Loading -> SplashScreen()
AuthState.Unauthenticated -> LoginScreen(viewModel = koinViewModel())
AuthState.Authenticated(user, householdId) -> PostLoginPlaceholderScreen(user, viewModel = koinViewModel())
Layout contract to copy from 02-UI-SPEC.md lines 185-197:
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.safeContentPadding()
.padding(horizontal = 16.dp)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize(),
)
Planner note: Existing App.kt is template code. Keep the @Composable, @Preview, MaterialTheme shape, but replace the button/greeting body with the auth gate. All strings come from Compose Resources, not raw Polish literals.
Server Application, Config, Flyway, and Routes
Applies to:
server/src/main/kotlin/dev/ulfrx/recipe/Application.ktserver/src/main/kotlin/dev/ulfrx/recipe/Database.ktserver/src/main/resources/application.confserver/src/main/resources/db/migration/V1__users.sqlserver/src/main/kotlin/dev/ulfrx/recipe/auth/MeRoute.ktserver/src/main/kotlin/dev/ulfrx/recipe/auth/AuthConfig.kt
Analog: existing server files
Application install and routing pattern (Application.kt lines 24-38):
fun Application.module() {
install(ContentNegotiation) {
json()
}
Database.migrate(this)
configureRouting()
}
fun Application.configureRouting() {
routing {
get("/health") {
call.respond(Health(status = "ok"))
}
}
}
HOCON env override pattern (application.conf lines 1-18):
ktor {
deployment {
port = 8080
port = ${?PORT}
}
}
database {
url = "jdbc:postgresql://localhost:5432/recipe"
url = ${?DATABASE_URL}
user = "recipe"
user = ${?DATABASE_USER}
password = "recipe"
password = ${?DATABASE_PASSWORD}
}
Flyway runtime pattern (Database.kt lines 10-39):
fun migrate(app: Application) {
val url = app.environment.config.property("database.url").getString()
val user = app.environment.config.property("database.user").getString()
val password = app.environment.config.property("database.password").getString()
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
runCatching {
Flyway
.configure()
.dataSource(url, user, password)
.locations("classpath:db/migration")
.baselineOnMigrate(true)
.validateOnMigrate(true)
.cleanDisabled(true)
.load()
.migrate()
}.onFailure { ex ->
log.error("Flyway migration failed", ex)
throw IllegalStateException("Database unreachable or migration failed", ex)
}
}
Planner note: Insert auth install in Application.module() after ContentNegotiation/CallLogging and before protected routing. Keep configureRouting() testable without invoking real Postgres where possible.
Server JWT Auth + Principal Resolver
Applies to:
server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.ktserver/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.ktserver/src/main/kotlin/dev/ulfrx/recipe/auth/UsersTable.kt
Analog: Application.kt plugin install, Database.kt DB boundary. No existing JWT or Exposed analog exists yet.
Plugin install style to copy (Application.kt lines 24-27):
install(ContentNegotiation) {
json()
}
DB config/logging boundary to copy (Database.kt lines 7-9, 24-39):
object Database {
private val log = LoggerFactory.getLogger(Database::class.java)
fun migrate(app: Application) {
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
runCatching {
Flyway.configure().dataSource(url, user, password).load().migrate()
}.onFailure { ex ->
log.error("Flyway migration failed", ex)
throw IllegalStateException("Database unreachable or migration failed", ex)
}
}
}
Required auth shape from 02-CONTEXT.md lines 62-64:
install(Authentication) {
jwt("authentik") {
// verifier(jwkProvider, issuer), withIssuer, withAudience, acceptLeeway(30)
// validate block must reject null or blank sub
}
}
Required JIT upsert from 02-CONTEXT.md lines 81-91:
INSERT INTO users (sub, email, display_name)
VALUES (:sub, :email, :name)
ON CONFLICT (sub) DO UPDATE
SET email = EXCLUDED.email,
display_name = EXCLUDED.display_name,
updated_at = now()
RETURNING *;
Planner note: Use Exposed DSL only and suspend transaction APIs for request-handling DB work. There is no local Exposed analog yet; the plan must treat PrincipalResolver as the first canonical server CRUD service.
Server Tests
Applies to:
server/src/test/kotlin/dev/ulfrx/recipe/auth/JwtAuthTest.ktserver/src/test/kotlin/dev/ulfrx/recipe/auth/JwtTestSupport.ktserver/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.ktif added
Analog: server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt
Ktor test pattern (ApplicationTest.kt lines 14-29):
class ApplicationTest {
@Test
fun `health endpoint returns 200 with status ok`() =
testApplication {
application {
install(ContentNegotiation) {
json()
}
configureRouting()
}
val response = client.get("/health")
assertEquals(HttpStatusCode.OK, response.status)
val body = response.bodyAsText()
assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body")
assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body")
}
}
Planner note: Compose test modules directly with testApplication { application { ... } }. Avoid calling Application.module() in tests that should not require a real Postgres unless the test sets up an in-memory DB first.
OIDC Platform Bootstrap
Applies to:
OidcClient.ktOidcResult.kt- platform
OidcClient.*.kt composeApp/src/androidMain/AndroidManifest.xmliosApp/iosApp/Info.plistiosApp/iosApp/iOSApp.swiftiosApp/Podfile
Analog: platform entry points and manifests
Android activity pattern (MainActivity.kt lines 10-19):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
Android manifest application/activity pattern (AndroidManifest.xml lines 1-23):
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
iOS SwiftUI bootstrap pattern (iOSApp.swift lines 1-15):
import SwiftUI
import ComposeApp
@main
struct iOSApp: App {
init() {
KoinIosKt.doInitKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
iOS plist pattern (Info.plist lines 1-8):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
Target-specific main patterns (jvmMain/main.kt lines 8-18, webMain/main.kt lines 8-14):
fun main() {
configureLogging()
initKoin()
application {
Window(
onCloseRequest = ::exitApplication,
title = "recipe",
) {
App()
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
configureLogging()
initKoin()
ComposeViewport {
App()
}
}
Planner note: Preserve existing Koin bootstrap while adding AppAuth callback wiring. For recipe://callback, register Android AppAuth receiver and iOS CFBundleURLTypes; do not replace unrelated app metadata.
Client AuthSession, Token Store, HTTP Client, and Common Tests
Applies to:
AuthState.ktAuthSession.ktTokenStore.ktAuthHttpClient.ktMeClient.ktSettingsFactory.*.ktHttpClientEngine.*.ktAuthSessionTest.kt
Analog: Koin module pattern, platform main files, ComposeAppCommonTest.kt. There is no existing Ktor client or settings storage analog.
Common test skeleton (ComposeAppCommonTest.kt lines 1-10):
package dev.ulfrx.recipe
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
Koin module registration analog (AppModule.kt lines 5-9):
// Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
val appModule =
module {
// intentionally empty in Phase 1
}
Required Ktor client bearer shape from 02-RESEARCH.md lines 313-329:
install(Auth) {
bearer {
loadTokens {
authSession.currentBearerTokens()
}
refreshTokens {
authSession.refreshBearerTokens()
}
sendWithoutRequest { request ->
request.url.host == apiHost
}
}
}
Required auth state shape from 02-CONTEXT.md lines 97-107:
sealed class AuthState {
data object Loading : AuthState()
data object Unauthenticated : AuthState()
data class Authenticated(
val user: User,
val householdId: HouseholdId? = null,
) : AuthState()
}
Planner note: This group becomes the client auth canonical pattern for later phases. Keep collaborators injectable behind small interfaces so common tests can fake OIDC and /me without platform AppAuth.
Shared Patterns
Shared Module Purity
Source: tools/verify-shared-pure.sh lines 1-15
Apply to: all files under shared/src/commonMain
# Enforces INFRA-06 / D-19: shared/commonMain must not import Ktor, Compose, SQLDelight.
# Runs grep against shared/src/commonMain/ only. Allowed imports: kotlin.*, kotlinx.serialization, kotlinx.datetime.
VIOLATIONS=$(grep -rn -E '^import[[:space:]]+(io\.ktor|androidx\.compose|org\.jetbrains\.compose|app\.cash\.sqldelight)' shared/src/commonMain/ 2>/dev/null || true)
Server Startup Order
Source: server/src/main/kotlin/dev/ulfrx/recipe/Application.kt lines 24-30
Apply to: Application.kt, auth plugin install, route wiring
fun Application.module() {
install(ContentNegotiation) {
json()
}
Database.migrate(this)
configureRouting()
}
Phase 2 should extend this to:
ContentNegotiation -> CallLogging redaction -> Database.migrate -> Database.connect -> configureAuth -> configureRouting
Server Logging
Source: server/src/main/kotlin/dev/ulfrx/recipe/Database.kt lines 7-8, 24, 36-39
Apply to: server DB/auth services
object Database {
private val log = LoggerFactory.getLogger(Database::class.java)
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
log.error("Flyway migration failed", ex)
throw IllegalStateException("Database unreachable or migration failed", ex)
}
Client Logging
Source: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt lines 1-8
Apply to: AuthSession, OIDC client wrappers
import co.touchlab.kermit.Logger
fun configureLogging() {
Logger.setTag("recipe")
// Platform log writers (OSLog iOS, LogCat Android, System.out JVM/Wasm) install by default.
}
Use Logger.withTag("auth") for auth flow diagnostics, but never log token bodies or Authorization headers.
Koin Startup
Source: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt lines 7-10 and platform callers
Apply to: authModule, ViewModels, OIDC clients, settings factories
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication =
startKoin {
config?.invoke(this)
modules(appModule)
}
HOCON Env Vars
Source: server/src/main/resources/application.conf lines 11-18
Apply to: oidc.issuer, oidc.audience, oidc.jwksUrl, oidc.leewaySeconds
database {
url = "jdbc:postgresql://localhost:5432/recipe"
url = ${?DATABASE_URL}
user = "recipe"
user = ${?DATABASE_USER}
password = "recipe"
password = ${?DATABASE_PASSWORD}
}
Ktor Route Tests
Source: server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt lines 17-29
Apply to: JwtAuthTest, MeRouteTest
testApplication {
application {
install(ContentNegotiation) {
json()
}
configureRouting()
}
val response = client.get("/health")
assertEquals(HttpStatusCode.OK, response.status)
}
No Analog Found
| File | Role | Data Flow | Reason |
|---|---|---|---|
server/src/main/kotlin/dev/ulfrx/recipe/auth/UsersTable.kt |
model | CRUD | No Exposed table exists yet. Phase 2 establishes first server table DSL pattern. |
server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt JWT details |
middleware | request-response | Ktor server exists, but no Authentication/JWT plugin exists yet. Use 02-CONTEXT.md D-21/D-22 and Ktor docs from research. |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthHttpClient.kt |
service | request-response | No Ktor client code exists yet. Use 02-RESEARCH.md Ktor bearer shape. |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/TokenStore.kt |
service | file-I/O | No multiplatform-settings usage exists yet. Keep explicit platform store seam. |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginViewModel.kt |
controller | event-driven | Koin ViewModel deps exist, but no ViewModel classes exist yet. Use method-per-action StateFlow convention from project docs/UI spec. |
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginViewModel.kt |
controller | event-driven | Same as LoginViewModel. |
iosApp/Podfile |
config | build-config | No existing Podfile. Follow Plan 03 CocoaPods DSL and iOS deployment target from composeApp/build.gradle.kts. |
Metadata
Analog search scope: composeApp/, server/, shared/, iosApp/, build-logic/, gradle/, tools/, Phase 1 summaries and Phase 2 plan drafts.
Files scanned: 75+ code/config/planning files via rg --files, find, and targeted nl -ba reads.
Pattern extraction date: 2026-04-27