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 contracts —
KompotComponent,KompotAction,FieldValue,ValidationRuleare interfaces; the application assembles the concrete set. - Generated registration — one
@KompotComponentMarkerand 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-interopis a bridge over the ObjC export gaps, not a renderer. - The live-update contract is here; the SSE or WebSocket transport is not.
form-corecovers 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. iosX64is unreachable for anything depending on Compose:compose.runtimelast published it at1.11.0-alpha01. Device and Apple-silicon simulator targets are there.