# Backend-driven UI for Kotlin Multiplatform

> kompot: the server describes a screen as a tree of components, the client renders it, and a new screen ships without a client release. Open hierarchies, KSP registration, and two conformance kits.

Page: https://kotlin.website/blog/backend-driven-ui-for-kotlin-multiplatform · Published: 2026-08-27

[kompot](https://github.com/youndie/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`, `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

```kotlin
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

```kotlin
@Serializable
@SerialName("product_card")
@KompotComponentMarker
data class ProductCardComponent(
    override val id: String,
    val title: String,
    val onClick: KompotAction? = null,
) : KompotComponent
```

```kotlin
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 `SerialDescriptor`s
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](https://github.com/youndie/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.
