Server-Driven UI (SDUI) / Backend-Driven UI (BDUI) framework for Kotlin Multiplatform (KMP).
Blueprint is an enterprise-grade framework that shifts UI rendering control to the backend. Instead of hardcoding screens in your mobile or desktop app, you describe the entire UI tree—layout, components, styling, and interactions—as serializable data models. The server delivers these blueprints at runtime, and the client renders them natively using Jetpack Compose.
This enables instant UI updates without app store releases, centralized business logic, and a cryptographically verifiable chain of state for zero-trust architectures.
- Architecture Overview
- Module Structure
- Key Concepts
- Server-Side Usage
- Client-Side Usage
- Extensibility
- Real-World Example
- Getting Started
┌─────────────────────────────────────────────────────────┐
│ SERVER │
│ ┌───────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Kotlin │ │ Business │ │ Cryptographic │ │
│ │ DSL │──▶ Logic │──▶ Utilities │ │
│ │ (UI Tree)│ │ (Resolve) │ │ (Hash & Sign) │ │
│ └───────────┘ └──────────────┘ └──────────────────┘ │
│ │ │
│ Resolution (JSON/ProtoBuf) │
└────────────────────────┼───────────────────────────────┘
│ HTTP / gRPC / WebSocket
┌────────────────────────┼───────────────────────────────┐
│ CLIENT │
│ ┌─────────────┐ ┌────┴──────────┐ ┌───────────────┐ │
│ │ Blueprint │ │ Compose │ │ Blueprint │ │
│ │ Chain │◀─│ Renderer │──▶ Registry │ │
│ │ (State) │ │ (Native UI) │ │ (Extensible) │ │
│ └─────────────┘ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
The server owns the UI definition and state. The client is a thin rendering layer that translates data into native components and sends user interactions back as intents.
| Module | Description |
|---|---|
runtime |
Core data models: Blueprint, BlueprintNode, component payloads, Intent, Effect, Dynamic* types. Pure Kotlin Multiplatform. |
dsl |
Type-safe Kotlin DSL for building UI trees on the server side. |
renderer |
Abstract rendering interface and CompositionLocal providers (state, intent handler, error handler). |
renderer-compose |
Jetpack Compose implementation: renders all layout and Material components from blueprints. |
chain |
Cryptographic BlueprintChain for zero-trust state management with hash verification and delta patching. |
protocol |
ProtoBuf schema definitions for all models. |
A Blueprint is the top-level container describing a screen:
@Serializable
data class Blueprint(
val id: String,
val version: String = "1.0",
val metadata: BlueprintMetadata? = null,
val state: Map<String, String> = emptyMap(),
val root: BlueprintNode,
val hash: String = "",
val previousHash: String? = null
)The UI tree is built from recursive BlueprintNodes:
@Serializable
data class BlueprintNode(
val key: String,
val payload: ComponentPayload,
val modifiers: List<NodeModifier> = emptyList(),
val children: List<BlueprintNode> = emptyList(),
val slots: Map<String, BlueprintNode> = emptyMap()
)Nodes can be traversed and searched:
findNodeByKey(key)— recursive search by keyfindNodesByPayloadType<T>()— filter all nodes by payload type
Components can bind to server-provided state without hardcoding values. Three sealed types bridge static literals and dynamic state keys:
// DynamicString, DynamicBool, DynamicFloat
sealed interface DynamicString {
data class Literal(val value: String) : DynamicString
data class StateKey(val key: String) : DynamicString
}Resolution on the client:
fun DynamicString.resolve(state: Map<String, String>): String = when (this) {
is Literal -> value
is StateKey -> state[key] ?: ""
}This allows the server to control text, booleans, and floats remotely by simply updating the state map.
All UI components are described as serializable data classes implementing ComponentPayload.
Layout Components (LayoutPayload): Box, Column, Row, Spacer, LazyColumn, LazyRow
Material Components (MaterialPayload): Text, Button, Card, Icon, TextField, Checkbox, Switch,
ProgressIndicator, Image
Example:
MaterialPayload.Text(
content = DynamicString.StateKey("welcome_message"),
size = TextSize.HEADLINE_MEDIUM,
color = ColorRole.PRIMARY
)Styling and layout modifiers mirror Compose's modifier system:
sealed interface NodeModifier {
data class Padding(val start: Float, val top: Float, val end: Float, val bottom: Float) : NodeModifier
data class Background(val colorHex: String) : NodeModifier
data class CornerRadius(val radius: Float) : NodeModifier
data class Alpha(val alpha: Float) : NodeModifier
data class Weight(val weight: Float) : NodeModifier
data class FillMaxWidth(val fraction: Float) : NodeModifier
// ... and more
}Intents represent user actions sent from the client to the server:
data class Intent(
val id: String,
val type: String,
val nodeKey: String,
val payload: IntentPayload = IntentPayload.Empty,
val timestampMs: Long,
val priority: IntentPriority = NORMAL
)Effects are server instructions for the client:
sealed interface Effect {
data class Navigation(val blueprint: Blueprint?, val type: Type) : Effect
data class Snackbar(val message: String, val isError: Boolean, ...) : Effect
data class Dialog(val title: String, val message: String, ...) : Effect
}Instead of sending entire blueprints for every state change, the server can send minimal patches:
data class StateDeltaBlock(
val patches: Map<String, String>,
val previousHash: String,
val newHash: String,
val signature: String = ""
)The Resolution combines deltas and effects:
data class Resolution(
val deltaBlocks: List<StateDeltaBlock> = emptyList(),
val effects: List<Effect> = emptyList(),
val targetNode: String? = null
)The BlueprintChain on the client maintains a cryptographic history of screens:
data class BlueprintChain(
val links: List<Blueprint> = emptyList(),
val lastAction: Action = IDLE,
val strictMode: Boolean = true,
val verifier: SignatureVerifier? = null
)Enterprise security features:
- Hash chaining: Each blueprint includes a
hashof its content and apreviousHashlinking to the previous screen. - Man-in-the-middle detection:
push()andreplace()verify that the incoming blueprint'spreviousHashmatches the current state hash. - State delta verification:
applyDeltaBlocks()verifies that delta patches target the correct state hash. - Cryptographic signatures: Delta blocks can be RSA-signed by the server and verified client-side via
SignatureVerifier.
// Server side
val deltaBlock = CryptoUtils.createDeltaBlock(currentHash, patches)
// deltaBlock.signature contains an RSA SHA256withRSA signature
// Client side
val chain = BlueprintChain(
strictMode = true,
verifier = object : SignatureVerifier {
override fun verify(payload: String, signatureBase64: String): Boolean {
// Verify using server's public key
}
}
)The DSL module provides a type-safe builder that feels like Compose:
val screen = blueprint("order_list") {
metadata(title = "My Orders", description = "Track all your orders")
state("total_orders" to "5", "username" to "John")
root {
Column(
verticalArrangement = Arrangement.START,
horizontalAlignment = Alignment.ALIGN_START,
modifiers = {
background("#F8F9FA")
fillMaxSize()
}
) {
Text(
content = bindString("username"),
size = TextSize.HEADLINE_LARGE
)
LazyColumn(
verticalArrangement = Arrangement.START,
contentPadding = 24f
) {
Card(
variant = CardVariant.FILLED,
onClickIntentId = "navigate_detail:1",
modifiers = {
cornerRadius(20f)
padding(bottom = 16f)
}
) {
Text(content = "Order #1", size = TextSize.TITLE_MEDIUM)
}
}
Button(
text = "Refresh",
variant = ButtonVariant.FILLED,
onClickIntentId = "refresh_orders"
)
}
}
}when (intent.id) {
"start" -> {
val blueprint = CryptoUtils.createSignedBlueprint(screen, null)
Resolution(
effects = listOf(Effect.Navigation(blueprint, Effect.Navigation.Type.REPLACE))
)
}
"refresh_orders" -> {
val patches = mapOf("total_orders" to "10")
val delta = CryptoUtils.createDeltaBlock(currentHash, patches)
Resolution(deltaBlocks = listOf(delta))
}
"navigate_detail:1" -> {
val detailScreen = createDetailScreen()
val blueprint = CryptoUtils.createSignedBlueprint(detailScreen, currentHash)
Resolution(effects = listOf(Effect.Navigation(blueprint)))
}
}For production, use ProtoBuf serialization (schemas in protocol/) for compact, high-performance wire format. JSON with
kotlinx.serialization is also supported for debugging.
@Composable
fun Application(client: HttpClient) {
val renderer = remember { createDefaultBlueprintRegistry() }
val state by store.state.collectAsState()
MaterialTheme {
when (val blueprint = state.chain.current) {
null -> LoadingScreen()
else -> {
renderer.render(
blueprint = blueprint,
intentHandler = { intent -> store.dispatch(intent) }
)
}
}
}
}The renderer automatically captures user interactions (button clicks, text changes, checkbox toggles) and calls
intentHandler.onIntent(intent).
For lazy lists with infinite scroll:
LazyColumn(
onLoadMoreIntentId = "load_more",
loadMoreThreshold = 3
) {
// items
}
// When the user scrolls near the end, an Intent is fired automatically.class ApplicationStore {
private fun handleResolution(resolution: Resolution) {
_state.update { currentState ->
var chain = currentState.chain
// Apply navigation effects
resolution.effects.forEach { effect ->
when (effect) {
is Effect.Navigation.PUSH -> chain = chain.push(effect.blueprint!!)
is Effect.Navigation.POP -> chain = chain.pop()
is Effect.Navigation.REPLACE -> chain = chain.replace(effect.blueprint!!)
is Effect.Snackbar -> showSnackbar(effect)
is Effect.Dialog -> showDialog(effect)
}
}
// Apply state delta patches (with hash verification)
chain = chain.applyDeltaBlocks(resolution.deltaBlocks)
currentState.copy(chain = chain, isLoading = false)
}
}
}- Define a custom payload:
@Serializable
@SerialName("custom.WeatherCard")
data class WeatherCard(
val temperature: DynamicFloat,
val condition: DynamicString
) : ComponentPayload- Create a renderer:
object WeatherCardRenderer : ComponentRenderer<WeatherCard> {
@Composable
override fun render(node: BlueprintNode, payload: WeatherCard, renderer: BlueprintRenderer) {
val state = LocalBlueprintState.current
Card(modifier = node.modifiers.toComposeModifier()) {
Column {
Text("${payload.temperature.resolve(state)}°C")
Text(payload.condition.resolve(state))
}
}
}
}- Register the renderer:
val registry = createDefaultBlueprintRegistry()
registry.register(WeatherCard::class, WeatherCardRenderer)- Use in the DSL:
// Add extension function
fun BlueprintDsl.WeatherCard(temperature: DynamicFloat, condition: DynamicString, ...) {
node(payload = WeatherCard(temperature, condition), ...)
}A complete order tracking application is included in the example/ directory:
- Server (Ktor): Generates blueprints with cryptographic hashes, handles intents, returns resolutions.
- Client (Compose Desktop): Renders blueprints, manages the
BlueprintChain, dispatches user intents.
example/
├── server/
│ ├── Application.kt # Ktor server entry point
│ ├── Module.kt # Intent routing & session management
│ ├── CryptoUtils.kt # SHA-256 hashing & RSA signing
│ ├── SessionManager.kt # Per-user hash chain storage
│ ├── OrderListScreen.kt # DSL-built list screen
│ ├── OrderDetailScreen.kt # DSL-built detail screen
│ ├── TrackingScreen.kt # DSL-built tracking screen
│ └── OrderRepository.kt # Mock data
└── client/
├── Application.kt # Compose UI entry point
├── ApplicationStore.kt # Chain state management
└── Main.kt # Desktop window launcher
Gradle dependency coordinates (add to your build.gradle.kts):
// Runtime models (shared across server and client)
implementation("io.github.numq.blueprint:runtime:1.0.0")
// DSL for building blueprints (server-side)
implementation("io.github.numq.blueprint:dsl:1.0.0")
// Compose renderer (client-side)
implementation("io.github.numq.blueprint:renderer-compose:1.0.0")
// Cryptographic chain (client-side)
implementation("io.github.numq.blueprint:chain:1.0.0")Minimal example:
// Server
val myScreen = blueprint("home") {
root {
Text(content = "Hello, World!")
}
}
// Client
renderer.render(
blueprint = myScreen,
intentHandler = { /* send to server */ }
)Apache-2.0 License - see LICENSE file for details.
