Skip to content

feat: add dynamic URL plugin, Supabase config, and PlatformBuildConfig#115

Open
therajanmaurya wants to merge 5 commits intoopenMF:devfrom
therajanmaurya:feat/generic-dynamic-url-plugin
Open

feat: add dynamic URL plugin, Supabase config, and PlatformBuildConfig#115
therajanmaurya wants to merge 5 commits intoopenMF:devfrom
therajanmaurya:feat/generic-dynamic-url-plugin

Conversation

@therajanmaurya
Copy link
Member

@therajanmaurya therajanmaurya commented Feb 10, 2026

Summary

This PR adds a generic dynamic URL plugin and Supabase configuration client for runtime server configuration management.

Key Features

  • DynamicBaseUrlPlugin: Ktor plugin for runtime base URL switching without recreating HTTP clients
  • SupabaseConfigClient: Configured Supabase client for fetching remote configurations
  • SupabaseConfigConventionPlugin: Gradle plugin to generate Supabase credentials from environment variables
  • PlatformBuildConfig: Cross-platform debug build detection using expect/actual pattern
  • Gesture Detectors: Multi-tap, long-press, and double-tap gesture utilities using Modifier.composed

New Files

core-base/network

  • DynamicBaseUrlPlugin.kt - Ktor plugin for dynamic URL switching
  • SupabaseConfigClient.kt - Supabase client wrapper with Postgrest support

core-base/platform

  • PlatformBuildConfig.kt - Cross-platform debug detection (expect/actual)
    • Android: checks ApplicationInfo.FLAG_DEBUGGABLE
    • Non-Android: checks if assertions are enabled

core-base/ui

  • GestureDetector.kt - Multi-tap, long-press, double-tap gesture utilities

build-logic/convention

  • SupabaseConfigConventionPlugin.kt - Generates SupabaseCredentials object from env vars

Usage

// Dynamic URL switching
val plugin = DynamicBaseUrlPlugin()
plugin.updateBaseUrl("https://new-api.example.com")

// Debug detection
if (PlatformBuildConfig.isDebug) {
    // Show debug UI
}

// Gesture detection
Modifier.detectMultiTapGesture(tapCount = 5) {
    // Triggered after 5 taps
}

Environment Variables for Supabase

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key

Test Plan

  • Build compiles successfully
  • DynamicBaseUrlPlugin updates URLs at runtime
  • PlatformBuildConfig correctly detects debug builds
  • Gesture detectors work with Modifier.composed

🤖 Generated with Claude Code

therajanmaurya and others added 2 commits February 10, 2026 22:39
Add reusable networking and UI components:

Network:
- DynamicUrlConfigProvider: Interface for runtime URL configuration
- MultiUrlConfigProvider: Extended interface for multi-endpoint apps
- DynamicBaseUrlPlugin: Ktor plugin for dynamic URL switching
- DynamicLoggableHosts: Dynamic list for Ktor logging filter
- RemoteConfigLoader: Generic interface for remote config loading
- BaseRemoteConfigLoader: Abstract base with fallback support
- RemoteConfigCredentials: Configuration holder for remote services

UI:
- detectMultiTapGesture: Detects N taps within timeout (for hidden features)
- detectLongPressGesture: Long press detection
- detectDoubleTapGesture: Double tap detection

These components enable:
- Runtime server switching without recreating HTTP clients
- Multi-tenant app support with dynamic endpoints
- Hidden developer features via gesture detection
- Flexible remote configuration loading

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove abstract RemoteConfigLoader (too project-specific)
- Add SupabaseConfigClient for generic Supabase integration
- Add SupabaseCredentials interface for credentials management
- Add Supabase postgrest dependency to core-base/network
- Add core-base/common dependency for DataState

SupabaseConfigClient provides:
- Lazy Supabase client initialization
- Generic fetchSingle<T> and fetchList<T> methods
- Custom query support via fetchWithQuery<T>
- Built-in error handling with DataState
- Credential validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Adds Supabase support and dynamic networking to the KMP project: a Gradle convention plugin that generates Supabase credentials from a secrets file, new Supabase client and dynamic base-URL abstractions/plugins, dependency additions, platform build config objects, and Compose gesture utilities.

Changes

Cohort / File(s) Summary
Gradle Plugin Registration
build-logic/convention/build.gradle.kts
Registered new plugin ID org.convention.kmp.supabase.config mapped to SupabaseConfigConventionPlugin.
Supabase Credential Generation Plugin
build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt
Added SupabaseConfigConventionPlugin and SupabaseConfigExtension: reads secrets/supabaseCredentialsFile.json, parses url/anonKey, generates SupabaseCredentials.kt into a generated source dir, wires the dir into commonMain, adds supabase-postgrest dependency, and ensures task ordering before Kotlin/KSP compilation.
Version & Dependency Catalog
gradle/libs.versions.toml
Added supabase = "3.1.1", library alias supabase-postgrest, and plugin entry for kmp-supabase-config.
Runtime Classpath Additions
cmp-android/dependencies/prodReleaseRuntimeClasspath*.txt
Added Supabase artifacts (postgrest-kt, auth-kt, supabase-kt at 3.1.1) and transitive deps (kotlin-reflect, atomicfu, secure-random, Ktor clients, etc.).
Core Network Dependency
core-base/network/build.gradle.kts
Added api(libs.supabase.postgrest) to commonMain dependencies.
Dynamic URL Abstractions
core-base/network/src/commonMain/kotlin/.../DynamicUrlConfigProvider.kt
Introduced DynamicUrlConfigProvider and MultiUrlConfigProvider interfaces (UrlType enum) for runtime base-URL selection and loggable hosts.
DynamicBaseUrl Ktor Plugin
core-base/network/src/commonMain/kotlin/.../DynamicBaseUrlPlugin.kt
Added DynamicBaseUrlPlugin (HttpClient plugin) that selects base URL from providers before each request, rebuilds request URL preserving path/query/fragment, and exposes DynamicLoggableHosts.
Supabase Client Wrapper
core-base/network/src/commonMain/kotlin/.../SupabaseConfigClient.kt
Added SupabaseConfigClient and SupabaseCredentials interface: lazy-initialized Supabase client, postgrest accessor, and isConfigured checks.
Platform Build Config
core-base/platform/src/commonMain/.../PlatformBuildConfig.kt, .../PlatformBuildConfig.android.kt, .../PlatformBuildConfig.nonAndroid.kt
Added expect/actual PlatformBuildConfig with isDebug property: Android implementation via Koin-obtained context; non-Android via assert-enabled detection.
UI Gesture Utilities
core-base/ui/src/commonMain/kotlin/template/core/base/ui/GestureDetector.kt
Added Modifier extensions: detectMultiTapGesture, detectLongPressGesture, and detectDoubleTapGesture for Compose pointer input handling.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Gradle
participant PluginTask as SupabaseGenTask
participant FS as FileSystem
participant Generator as SourceGenerator
participant KotlinCompile
Gradle->>PluginTask: run generateSupabaseConfig
PluginTask->>FS: read secrets/supabaseCredentialsFile.json
FS-->>PluginTask: file content / missing
PluginTask->>Generator: parse JSON -> produce SupabaseCredentials.kt
Generator-->>FS: write generatedDir/.../SupabaseCredentials.kt
PluginTask->>Gradle: register generatedDir as commonMain source
Gradle->>KotlinCompile: compile with generated sources

mermaid
sequenceDiagram
participant HttpClient
participant DynamicPlugin as DynamicBaseUrlPlugin
participant Provider as DynamicUrlConfigProvider
participant Request as OriginalRequest
HttpClient->>DynamicPlugin: before request
DynamicPlugin->>Provider: getBaseUrl() / getBaseUrl(type)
Provider-->>DynamicPlugin: baseUrl string
DynamicPlugin->>Request: rebuildUrl(originalUrl, baseUrl)
DynamicPlugin-->>HttpClient: proceed with modified request

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hopping through modules with a grin,

Secrets turned to Kotlin tucked within,
URLs that shift when requests take flight,
Gestures catch taps in the soft twilight,
Postgrest waits — the rabbit says "Goodnight!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes all three major features introduced: dynamic URL plugin, Supabase config, and PlatformBuildConfig. It is concise, clear, and reflects the main changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ionPlugin

- Remove fetchSingle, fetchList, fetchWithQuery from SupabaseConfigClient
  - Keep it simple: just provide client and postgrest access
  - Project-specific fetching logic belongs in the app
- Add SupabaseConfigConventionPlugin for generating credentials
  - Reads from secrets/supabaseCredentialsFile.json
  - Generates SupabaseCredentials implementing the interface
  - Configurable package name via extension
- Register plugin as org.convention.kmp.supabase.config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace @composable modifier functions with Modifier.composed { ... }
as recommended by Compose rules to avoid unnecessary recompositions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt`:
- Around line 109-133: The generated Kotlin interpolates raw url and anonKey
into double-quoted string literals (in the outputFile.writeText block creating
object SupabaseCredentials), which can break the produced source if values
contain ", \, $, or control chars; add and call a helper like
escapeKotlinString(value: String): String that escapes backslashes, double
quotes, dollar signs and control/newline characters (or encodes them) and use
escapeKotlinString(url) and escapeKotlinString(anonKey) in the template
interpolation so the resulting Kotlin source is always syntactically valid.
- Around line 71-89: The task eagerly reads extension.packageName, generatedDir,
and secretsFile during registration (generateTask), which breaks lazy
configuration; instead make the extension expose a Property<String> for
packageName and wire task inputs/outputs to Providers so values are resolved at
execution time — replace direct reads of extension.packageName,
generatedDir.get(), and secretsFile.readText() with provider-backed values
(e.g., use extension.packageName.orElse(...).map { ... } and
project.providers.fileProperty()/map to compute packagePath/outputFile and use
inputs.file(secretsFile) or inputs.property("credentialsContent",
secretsFile.asFile.map { it.readText() }), or defer the reads into doLast) so
inputs.property and outputs.file receive provider-backed values and up-to-date
checks work correctly.

In `@cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt`:
- Around line 2077-2105: The transitive supabase/postgrest dependency tree shows
ktor-client-core/content-negotiation/serialization at 3.3.3 while
postgrest-kt:3.1.1 and supabase-kt:3.1.1 target Ktor 3.0.3, so either upgrade
postgrest-kt/supabase-kt to a release built for Ktor 3.3.3 or force Ktor back to
3.0.3 with a Gradle dependency constraint/resolution so versions converge;
locate references to postgrest-kt, supabase-kt and the Ktor modules
(ktor-client-core, ktor-client-content-negotiation,
ktor-serialization-kotlinx-json) in your build.gradle(.kts) and update the
dependency version or add the constraint, then run the integration test suite to
verify runtime compatibility.

In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicBaseUrlPlugin.kt`:
- Around line 99-125: The rebuildUrl function drops query params and fragment
from the parsed base URL (baseUrlParsed) — fix by merging
baseUrlParsed.parameters into the URLBuilder.parameters before appending
originalUrl.parameters (so base params are present but allow originalUrl
parameters to override by appending originals after), and set fragment to
originalUrl.fragment if present otherwise to baseUrlParsed.fragment; adjust
URLBuilder usage in rebuildUrl (encodedPath, parameters, fragment) accordingly
to preserve base URL query params and fragment.
- Around line 172-181: The DynamicLoggableHosts class currently calls
configProvider.getLoggableHosts() separately from size and get, causing TOCTOU
inconsistencies during iteration; fix it by changing DynamicLoggableHosts to
delegate to a snapshot-per-access model: implement it as an
AbstractCollection<String> and List<String>, add a private inline fun snapshot()
= configProvider.getLoggableHosts(), and override size, get(index), iterator(),
listIterator(), and listIterator(index) to all use snapshot() so a single
consistent list is used for each iteration or access; keep the class name
DynamicLoggableHosts and the configProvider reference to locate and replace the
existing implementation.

In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicUrlConfigProvider.kt`:
- Line 8: Remove the duplicated word in the license header comment (the line
that currently reads "See See https://...") by deleting the extra "See" so it
reads "See https://..." in the file's top-of-file license/header comment; locate
the header comment block near the top of DynamicUrlConfigProvider.kt and correct
that single-word duplication.

In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/SupabaseConfigClient.kt`:
- Line 8: The copyright header in SupabaseConfigClient.kt contains a duplicated
word "See See" at the start of the URL comment; remove the extra "See" so the
header reads "See
https://github.com/openMF/kmp-project-template/blob/main/LICENSE" (i.e., replace
"See See" with a single "See").

In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/GestureDetector.kt`:
- Line 8: The file header in GestureDetector.kt contains a duplicated word "See"
in the license comment; open the top-of-file comment where the license URL is
referenced (near the GestureDetector declaration) and remove the extra "See" so
the header reads "See
https://github.com/openMF/kmp-project-template/blob/main/LICENSE".
- Around line 69-89: The pointerInput(Unit) coroutine captures stale values
(onGestureDetected, tapCount, tapTimeoutMs) so updates on recomposition are
ignored; fix by using rememberUpdatedState for any callback and changing
parameters used inside the pointerInput scope to their current-state holders
(e.g., val currentOnGesture = rememberUpdatedState(onGestureDetected), val
currentTapCountTarget = rememberUpdatedState(tapCount), val currentTapTimeout =
rememberUpdatedState(tapTimeoutMs)) and then reference currentOnGesture.value(),
currentTapCountTarget.value, currentTapTimeout.value inside the
detectTapGestures block; apply the same pattern for detectLongPressGesture and
detectDoubleTapGesture by wrapping onLongPress and onDoubleTap with
rememberUpdatedState and using their .value inside the pointerInput coroutine.

In `@gradle/libs.versions.toml`:
- Around line 71-72: Update the Supabase Kotlin SDK entry by changing the
version string for the supabase dependency key from "3.1.1" to the latest stable
"3.3.0"; locate the supabase property in gradle/libs.versions.toml (the line
reading supabase = "3.1.1") and replace the value with "3.3.0" so the project
uses the current SDK.
🧹 Nitpick comments (7)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/GestureDetector.kt (1)

69-69: Consider migrating from Modifier.composed to Modifier.Node.

Modifier.composed allocates a new composition for every element it's applied to, which makes it more expensive at scale. The Compose team recommends Modifier.Node as the modern, more performant alternative. Not urgent, but worth considering as a follow-up.

Also applies to: 110-110, 136-136

core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicUrlConfigProvider.kt (1)

60-67: UrlType enum is domain-specific for a generic/reusable plugin.

The enum values SELF_SERVICE and INTERBANK are specific to a financial services domain. For a generic KMP project template, consider making this extensible (e.g., a String-keyed map or a sealed interface that consumers can extend) so downstream projects aren't forced to use irrelevant enum values.

core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicBaseUrlPlugin.kt (1)

132-149: Config allows setting both providers simultaneously — consider validation.

Both configProvider and multiConfigProvider can be set, with multiConfigProvider silently winning. Consider logging a warning or throwing in prepare() if both are set, to avoid confusing misconfiguration.

core-base/network/build.gradle.kts (1)

28-28: Consider whether api scope is appropriate for supabase-postgrest in a base networking module.

Using api here exposes the Supabase Postgrest dependency to every consumer of core-base:network. This adds Supabase (and its transitive dependencies including auth-kt, supabase-kt, kotlincrypto, atomicfu, etc.) to all modules — even those that don't use Supabase.

Two alternatives to consider:

  1. Use implementation if only SupabaseConfigClient needs it internally and consumers don't need direct access to Postgrest types.
  2. Extract to a separate module (e.g., core-base:supabase) if Supabase is optional, keeping the base network module lean.
Option 1: Change to implementation
-            api(libs.supabase.postgrest)
+            implementation(libs.supabase.postgrest)
core-base/network/src/commonMain/kotlin/template/core/base/network/SupabaseConfigClient.kt (1)

55-68: Accessing client or postgrest when isConfigured == false will throw.

The lazy block calls createSupabaseClient with potentially empty/blank URL and key. If a consumer forgets the isConfigured guard and accesses postgrest directly, this will crash at runtime.

Consider either:

  • Throwing a descriptive exception in the lazy block when credentials are invalid, so the error is clear.
  • Making client/postgrest nullable or returning a Result type.
Option: Add a guard with a clear error message
     val client: SupabaseClient by lazy {
+        require(credentials.isConfigured) {
+            "Supabase credentials are not configured. " +
+                "Ensure secrets/supabaseCredentialsFile.json contains valid 'url' and 'anonKey' fields."
+        }
         createSupabaseClient(
             supabaseUrl = credentials.url,
             supabaseKey = credentials.anonKey,
build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt (2)

138-144: supabase-postgrest dependency is also declared as api in core-base/network/build.gradle.kts.

Modules that depend on core-base:network will already get supabase-postgrest transitively. Adding it again here means modules applying this plugin will have it declared twice. Not harmful, but redundant if those modules already depend on core-base:network.

Consider whether both declarations are needed, or if one should be removed to keep the dependency graph cleaner.


160-168: Regex-based JSON parsing is pragmatic but fragile.

The comment explains the rationale (avoiding a serialization dependency in build-logic). This is fine for Supabase URLs and base64 keys, but note that escaped quotes in values (e.g., \") would break the regex [^"]*. Consider adding a brief comment noting this limitation.

Also, Gradle's build-logic classpath already has access to org.json or Gson via transitive dependencies from the Android Gradle Plugin. Using one of those would be more robust without adding a new dependency.

Comment on lines +71 to +89
val generateTask = tasks.register("generateSupabaseConfig") {
// Determine package name from extension or derive from namespace
val packageName = extension.packageName
?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
?: "${project.group}.config"

val packagePath = packageName.replace(".", "/")
val outputFile = generatedDir.get().asFile
.resolve("$packagePath/SupabaseCredentials.kt")

// Track file content as input property for proper up-to-date checking
val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) {
secretsFile.readText()
} else {
""
}
inputs.property("credentialsContent", fileContent)
inputs.property("packageName", packageName)
outputs.file(outputFile)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Extension properties are evaluated eagerly at configuration time — packageName set after plugin application will be ignored.

Lines 73-75 read extension.packageName during task registration (configuration phase). If the user configures the extension after applying the plugin (which is the standard Gradle pattern), packageName will still be null.

Similarly, the outputFile path (line 78-79) and secretsFile content (lines 82-86) are computed eagerly. This violates Gradle's configuration avoidance principle and breaks up-to-date checking when inputs change.

The idiomatic fix is to use Property<String> in the extension and providers in the task:

Suggested approach — use lazy evaluation
 open class SupabaseConfigExtension {
-    var packageName: String? = null
+    val packageName: Property<String> = objects.property(String::class.java)
 }

In the task registration, defer evaluation to doLast or use task inputs with providers:

 val generateTask = tasks.register("generateSupabaseConfig") {
-    val packageName = extension.packageName
-        ?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
-        ?: "${project.group}.config"
-    val packagePath = packageName.replace(".", "/")
-    val outputFile = generatedDir.get().asFile
-        .resolve("$packagePath/SupabaseCredentials.kt")
-    val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) {
-        secretsFile.readText()
-    } else {
-        ""
-    }
-    inputs.property("credentialsContent", fileContent)
-    inputs.property("packageName", packageName)
-    outputs.file(outputFile)
+    val resolvedPackageName = extension.packageName.orElse(
+        provider {
+            project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
+                ?: "${project.group}.config"
+        }
+    )
+    inputs.property("packageName", resolvedPackageName)
+    inputs.file(secretsFile).optional()
+    outputs.dir(generatedDir)
     doLast {
-        val currentContent = inputs.properties["credentialsContent"] as String
-        val currentPackage = inputs.properties["packageName"] as String
+        val currentPackage = resolvedPackageName.get()
+        val currentContent = if (secretsFile.exists()) secretsFile.readText() else ""
         // ... rest of doLast
     }
 }
🤖 Prompt for AI Agents
In `@build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt`
around lines 71 - 89, The task eagerly reads extension.packageName,
generatedDir, and secretsFile during registration (generateTask), which breaks
lazy configuration; instead make the extension expose a Property<String> for
packageName and wire task inputs/outputs to Providers so values are resolved at
execution time — replace direct reads of extension.packageName,
generatedDir.get(), and secretsFile.readText() with provider-backed values
(e.g., use extension.packageName.orElse(...).map { ... } and
project.providers.fileProperty()/map to compute packagePath/outputFile and use
inputs.file(secretsFile) or inputs.property("credentialsContent",
secretsFile.asFile.map { it.readText() }), or defer the reads into doLast) so
inputs.property and outputs.file receive provider-backed values and up-to-date
checks work correctly.

Comment on lines +109 to +133
outputFile.writeText(
"""
|/*
| * Copyright 2025 Mifos Initiative
| *
| * This Source Code Form is subject to the terms of the Mozilla Public
| * License, v. 2.0. If a copy of the MPL was not distributed with this
| * file, You can obtain one at https://mozilla.org/MPL/2.0/.
| *
| * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
| */
|package $currentPackage
|
|import template.core.base.network.SupabaseCredentials as BaseSupabaseCredentials
|
|/**
| * Generated Supabase credentials from secrets/supabaseCredentialsFile.json
| * DO NOT EDIT - This file is generated by SupabaseConfigConventionPlugin
| */
|object SupabaseCredentials : BaseSupabaseCredentials {
| override val url: String = "$url"
| override val anonKey: String = "$anonKey"
|}
""".trimMargin()
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Parsed url and anonKey are interpolated into generated Kotlin source without escaping — risk of broken output or code injection.

If the JSON values contain characters like ", \, or $, the generated Kotlin file will have syntax errors or unintended behavior. While Supabase URLs and keys are typically safe, this is a correctness gap for a reusable plugin.

Proposed fix: escape special characters before interpolation
+    private fun escapeKotlinString(value: String): String =
+        value.replace("\\", "\\\\")
+            .replace("\"", "\\\"")
+            .replace("\$", "\\\$")
+
     // In the doLast block:
-    |    override val url: String = "$url"
-    |    override val anonKey: String = "$anonKey"
+    |    override val url: String = "${escapeKotlinString(url)}"
+    |    override val anonKey: String = "${escapeKotlinString(anonKey)}"
🤖 Prompt for AI Agents
In `@build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt`
around lines 109 - 133, The generated Kotlin interpolates raw url and anonKey
into double-quoted string literals (in the outputFile.writeText block creating
object SupabaseCredentials), which can break the produced source if values
contain ", \, $, or control chars; add and call a helper like
escapeKotlinString(value: String): String that escapes backslashes, double
quotes, dollar signs and control/newline characters (or encodes them) and use
escapeKotlinString(url) and escapeKotlinString(anonKey) in the template
interpolation so the resulting Kotlin source is always syntactically valid.

Comment on lines +2077 to +2105
| | | +--- io.github.jan-tennert.supabase:postgrest-kt:3.1.1
| | | | \--- io.github.jan-tennert.supabase:postgrest-kt-android:3.1.1
| | | | +--- io.github.jan-tennert.supabase:auth-kt:3.1.1
| | | | | \--- io.github.jan-tennert.supabase:auth-kt-android:3.1.1
| | | | | +--- androidx.startup:startup-runtime:1.2.0 (*)
| | | | | +--- androidx.browser:browser:1.8.0 -> 1.9.0 (*)
| | | | | +--- io.github.jan-tennert.supabase:supabase-kt:3.1.1
| | | | | | \--- io.github.jan-tennert.supabase:supabase-kt-android:3.1.1
| | | | | | +--- androidx.lifecycle:lifecycle-process:2.8.7 -> 2.9.4 (*)
| | | | | | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 -> 0.7.1 (*)
| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 -> 1.10.2 (*)
| | | | | | +--- co.touchlab:kermit:2.0.5 -> 2.0.8 (*)
| | | | | | +--- io.ktor:ktor-client-core:3.0.3 -> 3.3.3 (*)
| | | | | | +--- io.ktor:ktor-client-content-negotiation:3.0.3 -> 3.3.3 (*)
| | | | | | +--- io.ktor:ktor-serialization-kotlinx-json:3.0.3 -> 3.3.3 (*)
| | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.27.0
| | | | | | | \--- org.jetbrains.kotlinx:atomicfu-jvm:0.27.0
| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:{prefer 2.1.0} -> 2.2.21 (*)
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | | +--- com.squareup.okio:okio:3.10.2 -> 3.16.4 (*)
| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | | +--- com.russhwolf:multiplatform-settings-no-arg:1.3.0 (*)
| | | | | +--- com.russhwolf:multiplatform-settings-coroutines:1.3.0 (*)
| | | | | \--- org.kotlincrypto:secure-random:0.3.2
| | | | | \--- org.kotlincrypto:secure-random-jvm:0.3.2
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.2.21 (*)
| | | | +--- org.jetbrains.kotlin:kotlin-reflect:2.1.10
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and understand the repo structure
find . -name "build.gradle.kts" -o -name "build.gradle" -o -name "libs.versions.toml" | head -20

Repository: openMF/kmp-project-template

Length of output: 744


🏁 Script executed:

# Look for the cmp-android module structure
fd -t d "cmp-android" | head -5

Repository: openMF/kmp-project-template

Length of output: 83


🏁 Script executed:

# Check the dependency tree file itself
wc -l cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt 2>/dev/null || echo "File path may vary"

Repository: openMF/kmp-project-template

Length of output: 137


🏁 Script executed:

# Search for where these dependencies are declared
rg "postgrest-kt|supabase-kt" --type gradle --type toml -A 2 -B 2

Repository: openMF/kmp-project-template

Length of output: 504


🏁 Script executed:

# Read the versions file to see actual version values
cat gradle/libs.versions.toml | head -100

Repository: openMF/kmp-project-template

Length of output: 2205


🏁 Script executed:

# Find where Ktor, Kotlin, Coroutines versions are defined
rg "ktor|kotlin|coroutines|supabase" gradle/libs.versions.toml -i

Repository: openMF/kmp-project-template

Length of output: 5989


🏁 Script executed:

# Check the cmp-android build file
cat cmp-android/build.gradle.kts

Repository: openMF/kmp-project-template

Length of output: 5137


🌐 Web query:

postgrest-kt 3.1.1 ktor 3.3.3 compatibility

💡 Result:

postgrest-kt:3.1.1 is not a clean match for io.ktor:ktor-* :3.3.3.

  • postgrest-kt-jvm:3.1.1 is built against Kotlin stdlib 2.1.10 (and auth-kt-jvm:3.1.1). [1]
  • auth-kt:3.1.1 pulls Ktor server artifacts at 3.0.3 (ktor-server-core, ktor-server-cio). [2]
  • Ktor 3.3.3 updates Kotlin to 2.2.20. [3]

What this means

  • If your app uses ktor-* :3.3.3, Gradle will likely resolve Ktor to 3.3.3 while postgrest-kt:3.1.1 expects 3.0.3-era Ktor (and older Kotlin). That combination can work sometimes, but it’s not guaranteed (possible binary/source incompatibilities and dependency convergence issues). [2][3]

Recommended options

  1. Upgrade postgrest-kt to a newer version that aligns better with newer Ktor/Kotlin (preferred). (3.1.1 is from Feb 2025; newer lines exist.) [4]
  2. If you must stay on postgrest-kt:3.1.1, use Gradle dependency alignment (e.g., enforce io.ktor:ktor-bom:3.3.3) and run tests—treat it as an unsupported override.

Sources: [1] [2] [3] [4]


🌐 Web query:

supabase postgrest-kt 3.1.1 security vulnerabilities CVE

💡 Result:

No public CVE (or GitHub Security Advisory / GHSA) specifically targeting io.github.jan-tennert.supabase:postgrest-kt version 3.1.1 turned up in major public sources I checked (NVD + common package vulnerability aggregators). MavenRepository’s entry for postgrest-kt-jvm also shows no known/indexed vulnerabilities for 3.1.1 (released 2025‑02‑05). [1]

A CVE that sometimes gets cited in “Supabase + PostgREST” searches—CVE‑2024‑24213—is disputed and concerns a SQL-query endpoint in the Supabase dashboard/pg_meta context, not the Kotlin client library postgrest-kt. [2]

References

  • [1] MavenRepository – io.github.jan-tennert.supabase:postgrest-kt-jvm (shows 3.1.1 release date and no vulnerabilities listed). (mvnrepository.com)
  • [2] NVD – CVE‑2024‑24213 (disputed; scoped to Supabase dashboard/pg_meta query endpoint). (nvd.nist.gov)

Citations:


🌐 Web query:

Jan-Tennert supabase 3.1.1 ktor client compatibility

💡 Result:

For Jan Tennert’s supabase-kt version 3.1.1, the published artifacts are built against Ktor 3.0.3 (for example, supabase-kt-linuxx64:3.1.1 depends on io.ktor:ktor-client-core-* / content-negotiation-* / serialization-kotlinx-json-* 3.0.3) [1].

So, for best Ktor client compatibility, use Ktor 3.0.3 for your client engine dependencies (e.g., ktor-client-okhttp, ktor-client-cio, ktor-client-darwin, etc.). The project also explicitly recommends using the same Ktor version as supabase-kt to avoid dependency/version mismatches [2].

Sources: [1] [2]


Resolve Ktor version compatibility with PostgREST dependency.

The dependency tree shows Ktor 3.3.3, but postgrest-kt:3.1.1 (and transitive supabase-kt:3.1.1) are built for Ktor 3.0.3. While Gradle will converge to 3.3.3, this mismatch is unsupported and risks compatibility issues at runtime. Either upgrade postgrest-kt to a version compatible with Ktor 3.3.3 (or constrain Ktor back to 3.0.3 if that's a requirement). Verify the resolution with integration tests.

No known CVEs exist for these library versions.

🤖 Prompt for AI Agents
In `@cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt` around lines
2077 - 2105, The transitive supabase/postgrest dependency tree shows
ktor-client-core/content-negotiation/serialization at 3.3.3 while
postgrest-kt:3.1.1 and supabase-kt:3.1.1 target Ktor 3.0.3, so either upgrade
postgrest-kt/supabase-kt to a release built for Ktor 3.3.3 or force Ktor back to
3.0.3 with a Gradle dependency constraint/resolution so versions converge;
locate references to postgrest-kt, supabase-kt and the Ktor modules
(ktor-client-core, ktor-client-content-negotiation,
ktor-serialization-kotlinx-json) in your build.gradle(.kts) and update the
dependency version or add the constraint, then run the integration test suite to
verify runtime compatibility.

Comment on lines +99 to +125
private fun rebuildUrl(originalUrl: Url, baseUrl: String): Url {
val baseUrlParsed = Url(baseUrl)

return URLBuilder().apply {
protocol = baseUrlParsed.protocol
host = baseUrlParsed.host
port = baseUrlParsed.port

// Combine base path with original path
val basePath = baseUrlParsed.encodedPath.trimEnd('/')
val originalPath = originalUrl.encodedPath.trimStart('/')

encodedPath = if (originalPath.isNotEmpty()) {
"$basePath/$originalPath"
} else {
basePath
}

// Preserve query parameters
originalUrl.parameters.forEach { name, values ->
parameters.appendAll(name, values)
}

// Preserve fragment
fragment = originalUrl.fragment
}.build()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Query parameters from the base URL are silently dropped.

rebuildUrl extracts protocol, host, port, and encodedPath from the parsed base URL, but ignores any query parameters or fragment that might be present in baseUrl. If a consumer configures a base URL like https://api.com/v1?apiKey=xyz, the query param will be lost. Consider either merging base URL query params or documenting this limitation.

🤖 Prompt for AI Agents
In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicBaseUrlPlugin.kt`
around lines 99 - 125, The rebuildUrl function drops query params and fragment
from the parsed base URL (baseUrlParsed) — fix by merging
baseUrlParsed.parameters into the URLBuilder.parameters before appending
originalUrl.parameters (so base params are present but allow originalUrl
parameters to override by appending originals after), and set fragment to
originalUrl.fragment if present otherwise to baseUrlParsed.fragment; adjust
URLBuilder usage in rebuildUrl (encodedPath, parameters, fragment) accordingly
to preserve base URL query params and fragment.

Comment on lines +172 to +181
class DynamicLoggableHosts(
private val configProvider: DynamicUrlConfigProvider,
) : AbstractList<String>() {

override val size: Int
get() = configProvider.getLoggableHosts().size

override fun get(index: Int): String =
configProvider.getLoggableHosts()[index]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

TOCTOU bug: size and get call getLoggableHosts() independently, risking inconsistency.

Each call to size and get(index) invokes configProvider.getLoggableHosts() separately. If the provider's list changes between these calls (e.g., during iteration), get(index) can throw IndexOutOfBoundsException or return stale data. Since this is designed for dynamic/runtime-changing hosts, this is a real risk.

Snapshot the list once per access to ensure consistency:

Proposed fix: snapshot the delegate list
 class DynamicLoggableHosts(
     private val configProvider: DynamicUrlConfigProvider,
 ) : AbstractList<String>() {
 
+    private val currentHosts: List<String>
+        get() = configProvider.getLoggableHosts()
+
     override val size: Int
-        get() = configProvider.getLoggableHosts().size
+        get() = currentHosts.size
 
     override fun get(index: Int): String =
-        configProvider.getLoggableHosts()[index]
+        currentHosts[index]
 }

Actually, the above still has the same issue. A proper fix would be:

 class DynamicLoggableHosts(
     private val configProvider: DynamicUrlConfigProvider,
-) : AbstractList<String>() {
-
-    override val size: Int
-        get() = configProvider.getLoggableHosts().size
-
-    override fun get(index: Int): String =
-        configProvider.getLoggableHosts()[index]
-}
+) : List<String> by object : AbstractList<String>() {
+    // This still won't fix it. See below for the right approach.
+}

The simplest correct approach—delegate to a List implementation that snapshots on each iteration:

class DynamicLoggableHosts(
    private val configProvider: DynamicUrlConfigProvider,
) : AbstractCollection<String>(), List<String> {
    private inline fun snapshot() = configProvider.getLoggableHosts()

    override val size: Int get() = snapshot().size
    override fun get(index: Int): String = snapshot()[index]
    override fun iterator(): Iterator<String> = snapshot().iterator()
    override fun listIterator(): ListIterator<String> = snapshot().listIterator()
    override fun listIterator(index: Int): ListIterator<String> = snapshot().listIterator(index)
}

The key fix is overriding iterator() / listIterator() to snapshot once, so a single iteration pass is consistent. The size/get pair from AbstractList is still racy, but iterators (the common access pattern for Ktor's logging filter) will be safe.

🤖 Prompt for AI Agents
In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicBaseUrlPlugin.kt`
around lines 172 - 181, The DynamicLoggableHosts class currently calls
configProvider.getLoggableHosts() separately from size and get, causing TOCTOU
inconsistencies during iteration; fix it by changing DynamicLoggableHosts to
delegate to a snapshot-per-access model: implement it as an
AbstractCollection<String> and List<String>, add a private inline fun snapshot()
= configProvider.getLoggableHosts(), and override size, get(index), iterator(),
listIterator(), and listIterator(index) to all use snapshot() so a single
consistent list is used for each iteration or access; keep the class name
DynamicLoggableHosts and the configProvider reference to locate and replace the
existing implementation.

* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor typo: duplicated "See" in license header.

Line 8 reads See See https://... — one "See" should be removed.

🤖 Prompt for AI Agents
In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/DynamicUrlConfigProvider.kt`
at line 8, Remove the duplicated word in the license header comment (the line
that currently reads "See See https://...") by deleting the extra "See" so it
reads "See https://..." in the file's top-of-file license/header comment; locate
the header comment block near the top of DynamicUrlConfigProvider.kt and correct
that single-word duplication.

* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: duplicate "See" in the copyright header.

- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
🤖 Prompt for AI Agents
In
`@core-base/network/src/commonMain/kotlin/template/core/base/network/SupabaseConfigClient.kt`
at line 8, The copyright header in SupabaseConfigClient.kt contains a duplicated
word "See See" at the start of the URL comment; remove the extra "See" so the
header reads "See
https://github.com/openMF/kmp-project-template/blob/main/LICENSE" (i.e., replace
"See See" with a single "See").

* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: duplicate "See" in license header.

- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
🤖 Prompt for AI Agents
In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/GestureDetector.kt`
at line 8, The file header in GestureDetector.kt contains a duplicated word
"See" in the license comment; open the top-of-file comment where the license URL
is referenced (near the GestureDetector declaration) and remove the extra "See"
so the header reads "See
https://github.com/openMF/kmp-project-template/blob/main/LICENSE".

Comment on lines +69 to +89
): Modifier = composed {
var currentTapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) }

this.pointerInput(Unit) {
detectTapGestures(
onTap = {
val currentTime = Clock.System.now().toEpochMilliseconds()
if (currentTime - lastTapTime > tapTimeoutMs) {
currentTapCount = 0
}
currentTapCount++
lastTapTime = currentTime

if (currentTapCount >= tapCount) {
onGestureDetected()
currentTapCount = 0
}
},
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale closure: pointerInput(Unit) won't pick up new callback or parameter values.

pointerInput(Unit) launches its gesture-detection coroutine once and never restarts it, even if onGestureDetected, tapCount, or tapTimeoutMs change on recomposition. This means callers who pass a new lambda (e.g., one that closes over updated state) will silently run the old one.

Use rememberUpdatedState for the callback (and parameters if they can change):

Proposed fix
 ): Modifier = composed {
+    val updatedOnGestureDetected by rememberUpdatedState(onGestureDetected)
+    val updatedTapCount by rememberUpdatedState(tapCount)
+    val updatedTapTimeoutMs by rememberUpdatedState(tapTimeoutMs)
     var currentTapCount by remember { mutableIntStateOf(0) }
     var lastTapTime by remember { mutableLongStateOf(0L) }
 
     this.pointerInput(Unit) {
         detectTapGestures(
             onTap = {
                 val currentTime = Clock.System.now().toEpochMilliseconds()
-                if (currentTime - lastTapTime > tapTimeoutMs) {
+                if (currentTime - lastTapTime > updatedTapTimeoutMs) {
                     currentTapCount = 0
                 }
                 currentTapCount++
                 lastTapTime = currentTime
 
-                if (currentTapCount >= tapCount) {
-                    onGestureDetected()
+                if (currentTapCount >= updatedTapCount) {
+                    updatedOnGestureDetected()
                     currentTapCount = 0
                 }
             },
         )
     }
 }

The same pattern applies to detectLongPressGesture and detectDoubleTapGesture below — wrap onLongPress / onDoubleTap with rememberUpdatedState as well.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
): Modifier = composed {
var currentTapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) }
this.pointerInput(Unit) {
detectTapGestures(
onTap = {
val currentTime = Clock.System.now().toEpochMilliseconds()
if (currentTime - lastTapTime > tapTimeoutMs) {
currentTapCount = 0
}
currentTapCount++
lastTapTime = currentTime
if (currentTapCount >= tapCount) {
onGestureDetected()
currentTapCount = 0
}
},
)
}
): Modifier = composed {
val updatedOnGestureDetected by rememberUpdatedState(onGestureDetected)
val updatedTapCount by rememberUpdatedState(tapCount)
val updatedTapTimeoutMs by rememberUpdatedState(tapTimeoutMs)
var currentTapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) }
this.pointerInput(Unit) {
detectTapGestures(
onTap = {
val currentTime = Clock.System.now().toEpochMilliseconds()
if (currentTime - lastTapTime > updatedTapTimeoutMs) {
currentTapCount = 0
}
currentTapCount++
lastTapTime = currentTime
if (currentTapCount >= updatedTapCount) {
updatedOnGestureDetected()
currentTapCount = 0
}
},
)
}
🤖 Prompt for AI Agents
In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/GestureDetector.kt`
around lines 69 - 89, The pointerInput(Unit) coroutine captures stale values
(onGestureDetected, tapCount, tapTimeoutMs) so updates on recomposition are
ignored; fix by using rememberUpdatedState for any callback and changing
parameters used inside the pointerInput scope to their current-state holders
(e.g., val currentOnGesture = rememberUpdatedState(onGestureDetected), val
currentTapCountTarget = rememberUpdatedState(tapCount), val currentTapTimeout =
rememberUpdatedState(tapTimeoutMs)) and then reference currentOnGesture.value(),
currentTapCountTarget.value, currentTapTimeout.value inside the
detectTapGestures block; apply the same pattern for detectLongPressGesture and
detectDoubleTapGesture by wrapping onLongPress and onDoubleTap with
rememberUpdatedState and using their .value inside the pointerInput coroutine.

Comment on lines +71 to +72
# Supabase
supabase = "3.1.1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

supabase-kt latest version 2025 2026 kotlin sdk releases

💡 Result:

Latest supabase-kt version (as of Feb 10, 2026)

  • Stable latest: 3.3.0 (released Jan 5, 2026) (github.com)
  • Maven Central also shows 3.3.0 published Jan 4, 2026 (date can differ slightly from GitHub release timezones) (mvnrepository.com)

Gradle coordinate (example):
io.github.jan-tennert.supabase:<module>:3.3.0 (github.com)

Notable 2025–2026 releases (from GitHub/Maven)

Citations:


Update Supabase Kotlin SDK to version 3.3.0 (latest stable).

Version 3.1.1 is outdated. The latest stable release is 3.3.0 (released Jan 5, 2026), with additional patch releases (3.1.4, 3.2.6) available between the current version and latest.

🤖 Prompt for AI Agents
In `@gradle/libs.versions.toml` around lines 71 - 72, Update the Supabase Kotlin
SDK entry by changing the version string for the supabase dependency key from
"3.1.1" to the latest stable "3.3.0"; locate the supabase property in
gradle/libs.versions.toml (the line reading supabase = "3.1.1") and replace the
value with "3.3.0" so the project uses the current SDK.

…ction

Add expect/actual PlatformBuildConfig object that provides isDebug flag:
- Android: checks ApplicationInfo.FLAG_DEBUGGABLE via Koin context
- Non-Android: checks if assertions are enabled (disabled in release)

This enables commonMain code to detect debug/release builds without
platform-specific parameters.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@core-base/platform/src/androidMain/kotlin/template/core/base/platform/PlatformBuildConfig.android.kt`:
- Around line 19-24: PlatformBuildConfig as an object implements KoinComponent
and uses inject() which creates an implicit initialization-order dependency on
startKoin(); this can throw IllegalStateException if isDebug is accessed early.
Change PlatformBuildConfig to remove the Koin dependency by adding a manual
init(context: Context) function that stores a private nullable Context used by
the isDebug getter (and stop using KoinComponent/inject()), or alternatively
convert PlatformBuildConfig into a class instantiated after startKoin; ensure
references to PlatformBuildConfig.isDebug check/init the context or document
that startKoin() must run before first access.

In
`@core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/PlatformBuildConfig.nonAndroid.kt`:
- Around line 21-27: The checkAssertionsEnabled() implementation using
assert(...) is not cross-platform and fails on Kotlin/Native and Kotlin/JS;
replace this trick with platform-specific detection: remove assert-based logic
in checkAssertionsEnabled() and implement per-target solutions—use
kotlin.native.Platform.isDebugBinary (via `@OptIn`(ExperimentalNativeApi::class))
for native/iOS, provide a JS-specific runtime/build-time flag or source-set
implementation for JS, and for JVM keep a simple check based on assertions if
desired; alternatively expose a single expected/actual isDebug or
checkAssertionsEnabled() API across source sets and implement it in each target
source set or via a Gradle-generated BuildConfig constant so all targets compile
and correctly detect debug/release.

Comment on lines +19 to +24
actual object PlatformBuildConfig : KoinComponent {
private val context: android.content.Context by inject()

actual val isDebug: Boolean
get() = (context.applicationInfo.flags and
android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Koin dependency on a global object creates an implicit initialization order requirement.

PlatformBuildConfig is an object (singleton) that implements KoinComponent. Any access to isDebug before startKoin() completes (e.g., during Application.onCreate setup, content providers, or static initializers) will throw an IllegalStateException. Since this is a low-level utility, it may be accessed earlier than typical DI-wired components.

Consider either:

  • Documenting the requirement that Koin must be initialized before first access, or
  • Accepting Context via a manual init(context) method to decouple from DI timing.
🤖 Prompt for AI Agents
In
`@core-base/platform/src/androidMain/kotlin/template/core/base/platform/PlatformBuildConfig.android.kt`
around lines 19 - 24, PlatformBuildConfig as an object implements KoinComponent
and uses inject() which creates an implicit initialization-order dependency on
startKoin(); this can throw IllegalStateException if isDebug is accessed early.
Change PlatformBuildConfig to remove the Koin dependency by adding a manual
init(context: Context) function that stores a private nullable Context used by
the isDebug getter (and stop using KoinComponent/inject()), or alternatively
convert PlatformBuildConfig into a class instantiated after startKoin; ensure
references to PlatformBuildConfig.isDebug check/init the context or document
that startKoin() must run before first access.

Comment on lines +21 to +27
private fun checkAssertionsEnabled(): Boolean {
var assertionsEnabled = false
// This assignment will only happen if assertions are enabled
@Suppress("KotlinConstantConditions", "AssertionInFunctionCall")
assert(true.also { assertionsEnabled = true })
return assertionsEnabled
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Kotlin Native assert function behavior release debug builds

💡 Result:

In Kotlin/Native, kotlin.assert(...) is disabled by default and will only throw AssertionError if you enable runtime assertions at compile time. The stdlib docs explicitly say that for Native assertions must be “enabled during compilation.” [1]

Debug vs Release

  • Debug (linkDebug*) vs Release (linkRelease*) mainly controls things like debug info (for example, -g / DWARF symbols) and optimization levels—not assertions automatically. Debug binaries are typically produced by linkDebug* tasks, but that alone doesn’t imply assertions are on. [2]
  • Assertions are controlled separately by a compiler flag.

How to enable assertions (Native)

Compile with the Kotlin/Native compiler option:

  • -enable-assertions (alias: -ea) — “Enable runtime assertions in the generated code.” [3]

In Gradle (Kotlin Multiplatform), this typically means adding -ea to the Native compilation’s freeCompilerArgs.

Summary: Kotlin/Native assert() behavior is not inherently “debug vs release”; it’s on only when you compile with -ea. [1][3]

References:
[1] Kotlin stdlib assert (Native behavior) [kotlinlang.org] (kotlinlang.org)
[2] Debug binaries via linkDebug* tasks (debug info behavior) [kotlinlang.org] (kotlinlang.org)
[3] Kotlin/Native compiler option -enable-assertions (-ea) [kotlinlang.org] (kotlinlang.org)

Citations:


🌐 Web query:

Kotlin/JS assert function behavior debug release

💡 Result:

  • kotlin.assert() is not part of Kotlin/JS stdlib, so there is no “debug vs release” runtime-assert toggle like JVM’s -ea. In the official API docs, assert is provided only for JVM and Native targets (no JS/Wasm). [1]
  • If you want assertions in Kotlin/JS tests, use kotlin.test assertion functions (assertEquals, assertTrue, etc.). These are regular test checks and don’t depend on a “release/debug” switch. [2]
  • If your goal is debug-only checks in JS production bundles, you’ll need your own mechanism (for example a build-time constant/flag and dead-code elimination), because Kotlin/JS doesn’t offer a built-in “enable assertions” runtime switch for assert(). (The Kotlin Power-assert plugin can improve assertion messages, but it doesn’t create a JS runtime enable/disable semantics by itself.) [3]

Sources:
[1] Kotlin stdlib assert API (platform tabs show only JVM/Native) https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/assert.html
[2] Kotlin kotlin-test overview (assertion functions for JS tests) https://kotlinlang.org/api/core/kotlin-test/index.html
[3] Kotlin Power-assert compiler plugin docs https://kotlinlang.org/docs/power-assert.html


🌐 Web query:

Kotlin Platform.isDebugBinary Native debug detection

💡 Result:

In Kotlin/Native you can detect whether the current binary was built as a debug binary via kotlin.native.Platform.isDebugBinary:

// nativeMain / iosMain
import kotlin.native.ExperimentalNativeApi
import kotlin.native.Platform

`@OptIn`(ExperimentalNativeApi::class)
val isDebug: Boolean = Platform.isDebugBinary
  • Platform.isDebugBinary is a Kotlin/Native-only API (@ExperimentalNativeApi) and is true if the binary was compiled in debug mode. [1]
  • In Gradle, “debug binaries” typically come from linkDebug* tasks (vs linkRelease*), which aligns with Kotlin/Native DEBUG/RELEASE build types. [2][3]

KMP pattern (commonMain):

// commonMain
expect val isDebug: Boolean
// iosMain (or other native*)
actual val isDebug: Boolean = Platform.isDebugBinary

Sources: [1] [2] [3]


🏁 Script executed:

# Search for any existing debug detection patterns in the repo
rg "isDebugBinary|assert.*also" --type kotlin -A 3

Repository: openMF/kmp-project-template

Length of output: 594


🏁 Script executed:

# Check the actual implementation context
cat -n core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/PlatformBuildConfig.nonAndroid.kt | head -40

Repository: openMF/kmp-project-template

Length of output: 1288


🏁 Script executed:

# Find all PlatformBuildConfig implementations
fd "PlatformBuildConfig" --type f -e kt

Repository: openMF/kmp-project-template

Length of output: 370


🏁 Script executed:

# Check which source sets exist for the platform module
fd "src" core-base/platform --type d

Repository: openMF/kmp-project-template

Length of output: 94


🏁 Script executed:

# Check build.gradle or similar to see target configurations
fd "build.gradle" core-base/platform -A 5

Repository: openMF/kmp-project-template

Length of output: 301


🏁 Script executed:

# Find build.gradle files in core-base/platform
find core-base/platform -name "build.gradle*" -type f

Repository: openMF/kmp-project-template

Length of output: 106


🏁 Script executed:

# Check the commonMain expect declaration
cat -n core-base/platform/src/commonMain/kotlin/template/core/base/platform/PlatformBuildConfig.kt

Repository: openMF/kmp-project-template

Length of output: 878


🏁 Script executed:

# Check if this is part of a multiplatform build
find . -name "build.gradle.kts" -path "*/core-base/platform/*" -o -name "build.gradle.kts" -path "*/core-base/*" | head -5

Repository: openMF/kmp-project-template

Length of output: 262


🏁 Script executed:

# Check the platform module's build.gradle.kts
cat -n core-base/platform/build.gradle.kts

Repository: openMF/kmp-project-template

Length of output: 2201


🏁 Script executed:

# Also check the androidMain implementation for reference
cat -n core-base/platform/src/androidMain/kotlin/template/core/base/platform/PlatformBuildConfig.android.kt

Repository: openMF/kmp-project-template

Length of output: 1096


The assert() trick is not cross-platform compatible and won't work reliably on non-JVM targets.

On Kotlin/Native, assert() is disabled by default and only runs if the code is compiled with the -ea compiler flag—it does not automatically match debug vs. release builds. Use Platform.isDebugBinary instead, which correctly detects debug binaries compiled via linkDebug* tasks:

// iosMain / nativeMain
import kotlin.native.ExperimentalNativeApi
import kotlin.native.Platform

`@OptIn`(ExperimentalNativeApi::class)
actual val isDebug: Boolean = Platform.isDebugBinary

On Kotlin/JS, assert() does not exist in the standard library, so this code will not compile for JS targets. You'll need platform-specific implementations for each target (Desktop, iOS, Web).

Consider splitting into separate source sets with appropriate APIs per target, or use a build-time constant generated via Gradle (e.g., a BuildConfig-style plugin that writes a const val).

🤖 Prompt for AI Agents
In
`@core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/PlatformBuildConfig.nonAndroid.kt`
around lines 21 - 27, The checkAssertionsEnabled() implementation using
assert(...) is not cross-platform and fails on Kotlin/Native and Kotlin/JS;
replace this trick with platform-specific detection: remove assert-based logic
in checkAssertionsEnabled() and implement per-target solutions—use
kotlin.native.Platform.isDebugBinary (via `@OptIn`(ExperimentalNativeApi::class))
for native/iOS, provide a JS-specific runtime/build-time flag or source-set
implementation for JS, and for JVM keep a simple check based on assertions if
desired; alternatively expose a single expected/actual isDebug or
checkAssertionsEnabled() API across source sets and implement it in each target
source set or via a Gradle-generated BuildConfig constant so all targets compile
and correctly detect debug/release.

@therajanmaurya therajanmaurya changed the title feat: add generic dynamic URL plugin and Supabase config client feat: add dynamic URL plugin, Supabase config, and PlatformBuildConfig Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant