Server components
Because a rendered UI is a serializable Node tree, "server components" is not a separate framework — it is the same tree, produced on the JVM and delivered four ways: safe HTML, hydration metadata, streamed patches, and typed server actions.
Open the live server-components demo
The demo above is hosted by this very server. View-source it: server HTML, a hydration plan, a client island, and a streamed update on one page.
1. Safe HTML
val tree = KineticaRuntime().render { ProductPage() }.tree
val html = tree.toSafeHtml()toSafeHtml() serializes a tree with escaping and URL-scheme sanitization built in. Every page of this documentation site is produced exactly this way.
2. Client islands & hydration
Mark an interactive region with a ClientRef — a serializable pointer to a client component plus its serializable props:
emit(ClientRef(
componentId = ADD_TO_CART_COMPONENT_ID,
props = JsonObject(mapOf("productId" to JsonPrimitive("runtime-license"))),
))The server ships the tree's hydration plan alongside the HTML:
val transport = KineticaServerTransport()
val plan = transport.encodeHydrationPlan(tree.hydrationPlan())
// -> <script id="kinetica-hydration-plan" type="application/json">…</script>A Kotlin/JS client reads the plan, finds the island placeholders, and mounts real Kinetica browser apps into them — the rest of the page stays static HTML with zero client JS cost.
3. Streaming
Slow parts of a page defer and stream in as patches against the tree — the diff format is the same serializable value used everywhere else:
tree.toServerRenderStream(
subtrees = listOf(
ServerRenderDeferredSubtree(path = listOf(3)) {
delay(100) // load recommendations…
FragmentNode(children = listOf(TextNode("Recommended for you: …")))
},
),
manifest = manifest,
) // -> List<ServerRenderChunk>: the initial chunk, then a Patch per resolved subtreeEach chunk serializes with transport.encodeChunk(chunk); the demo endpoint joins them with newlines into NDJSON, which the client applies as path-addressed subtree replacements.
4. Typed server actions
Client islands call back into the server through declared, schema-validated actions:
val dispatcher = KineticaServerActionDispatcher(
stubs = listOf(
serverActionStub(
registration = ServerActionRegistration(
actionId = ADD_TO_CART_ACTION_ID,
functionFqName = "shop.addToCart",
invalidates = listOf("cart"),
),
inputSerializer = AddToCartInput.serializer(),
outputSerializer = AddToCartResult.serializer(),
) { input -> addToCart(input) },
),
verifyCapabilityToken = { it.value == expectedCapability },
verifyCsrfToken = { it?.value == expectedCsrf },
)
// POST /actions/{id} ->
dispatcher.dispatch(transport.decodeActionRequest(body))Inputs are validated against the serializer-derived schema before your handler runs; capability and CSRF tokens are checked by the dispatcher; handler exceptions return a generic failure message (no stack-trace leaks); invalidates keys plug into the resource loop on the client.
The manifest
ClientComponentManifest declares which client components and actions exist (componentId → componentFqName, props types, action registrations) — the contract both sides agree on, and what the compiler plugin is designed to generate from @ClientComponent / @ServerAction annotations.