Backend-driven UI for Kotlin Multiplatform

kompot is a backend-driven UI toolkit for Kotlin Multiplatform: the server describes a screen as a serialisable tree of components, the client renders it, and a new screen ships without a new client release. The protocol modules publish for the JVM, Android, iOS and wasmJs; the Compose renderers for all of those minus the Intel simulator.

Why it exists

Backend-driven UI is easy to start and hard to keep honest. The usual failure is a toolkit that knows your components: a sealed hierarchy of every widget the product happens to have, a client that must be released whenever the server learns a new one, and a serialiser that turns an unfamiliar type into a crash on exactly the installs a staged rollout cannot reach.

kompot is built the other way round. The core knows about a tree, actions, modifiers and design tokens — and nothing about what a "product card" is. Everything concrete arrives as a plug-in module, and an unknown type degrades to a placeholder instead of taking the screen down. The degradation is observable: a sink receives every component the client could not draw and every action it could not act on, because a crash is reported by every crash reporter ever written and a silent hole is reported by nobody.

What you get

  • Open contractsKompotComponent, KompotAction, FieldValue, ValidationRule are interfaces; the application assembles the concrete set.
  • Generated registration — one @KompotComponentMarker and KSP writes the polymorphic registration and the renderer entry; no hand-maintained list.
  • Forms that stay client-side — validation, visibility and cross-field rules run locally; a server-relevant change asks the backend for a targeted patch instead of a new screen.
  • Server-driven theming — open string tokens with a theme that says what to paint them with, down to text colour, without touching the shapes a design system owns.
  • Live updates — a screen names its own update channel, so updates stay per-user.
  • Multi-step flows as pure functions — a wizard graph is (session, transition, draft) -> session, unit-testable with no HTTP in sight.

Install

repositories { maven("https://reposilite.kotlin.website/snapshots") }

val kompotVersion = "0.33.1.88"

dependencies {
    implementation(platform("io.github.youndie:kompot-bom:$kompotVersion"))
    implementation("io.github.youndie:kompot-core")
    implementation("io.github.youndie:kompot-standard")
    implementation("io.github.youndie:kompot-ktor")
}

The platform matters more than brevity: a version carries the CI run number, so any two publishes differ, and kompot-core:0.19.0.26 beside kompot-client:0.19.0.27 would resolve quietly into a combination nobody ever built. Through the BOM that combination cannot be written down.

What it looks like

@Serializable
@SerialName("product_card")
@KompotComponentMarker
data class ProductCardComponent(
    override val id: String,
    val title: String,
    val onClick: KompotAction? = null,
) : KompotComponent
get("/catalogue") {
    call.respondKompotComponent(
        column {
            text("Catalogue", style = TypographyToken("title_large"))
            items.forEach { productCard(title = it.title, onClick = navigate(it.url)) }
        },
    )
}

How the contract is kept honest

The JSON Schema for every protocol module is generated from the same SerialDescriptors kotlinx.serialization encodes a response with, so the schema cannot fall behind the types; the schemas, and the specification itself, travel inside the published artefact. The rules the schema cannot express are numbered, and KompotSpecResources.rules() resolves "9.4.3" to the sentence that states it — from the jar, not from a checkout.

Two conformance kits hold the two sides of the wire. kompot-tck walks a running server over HTTP with twelve checks — schema conformance, form connectivity, ETag revalidation, pagination termination, idempotency, the update-stream framing. kompot-client-tck is a corpus of eighteen input-to-outcome cases a client answers through a seven-operation adapter, in any language; a case that asserts nothing is reported as a violation, and a coverage report says which of the 35 numbered rules of the forms section each case holds — 19 today, with every uncovered rule assigned to the instrument that does hold it.

Each release is also read back from the repository by proba the way a stranger's build resolves it; the verdict badge in the README is written by the same run that published the version it describes.

Limitations

  • The Compose renderers cover the standard, form, wizard and image components; the renderer of a component you invent is yours, and so is the SwiftUI side — kompot-swift-interop is a bridge over the ObjC export gaps, not a renderer.
  • The live-update contract is here; the SSE or WebSocket transport is not.
  • form-core covers what a form can decide locally. Limits and balances belong to the server; the client only highlights the field a refusal names.
  • The Compose half is built against Compose Multiplatform 1.11.1 with material3 1.11.0-alpha07 — that line has no stable material3 — and mixing Compose lines fails at runtime, not at resolution.
  • iosX64 is unreachable for anything depending on Compose: compose.runtime last published it at 1.11.0-alpha01. Device and Apple-silicon simulator targets are there.