Keel is a protocol and a small set of libraries for serving MPAs with a frameworked UI from Kotlin. The host owns URLs, page ids, and payload types. The call site passes which pack to render. A pack is a versioned frontend that implements those ids.
You always do the same two things: declare a contract on the host, then pass a pack that implements it into render.
Glue an app. Register pages, write one frontend with any supported adapter (Svelte, React, Vue, Solid, Preact, Lit, or Angular), and ship that pack. Keel is the router between Ktor and the UI (visits, seeds, forms) without a client-side router owning the URL.
Install themes. Keep the same registry. Publish the page ids and serializers as the contract. Authors provide packs typed for that data. The call site passes the pack it wants at render. Visitors never store a theme.
The first host is Ktor. Keel ships seven pack adapters. Svelte 5, React 18/19, Vue 3, Solid 1.9, Preact 10, Lit 3, and Angular 19; Svelte was the first. See Framework adapters for the matrix.
Walkthrough: server → client → Ktor host → pack → first page.
Where Keel sits
Inertia4J speaks the Inertia SPA protocol over Ktor or Spring. Kinetica and Kilua keep the UI in Kotlin (SSR, islands, route meta). Keel keeps routes, page ids, and the typed seed on the host, and ships the UI as swappable Vite packs (.feb) that implement those ids. Same server contract, different frontends, including the page’s document head.
The split
- Keel, product, CLI, Maven group
dev.kolektiv.keel - Pack, a versioned frontend (
.febzip + manifest) keyed by page id, not path - Adapter,
mount/unmount/updatefor a framework (Svelte, React, Vue, Solid, Preact, Lit, Angular)
What the host does
- Declares
page<T>(id, path), id + payload type + path - Declares
action<I, O>(id), typed writes, JSON in / JSON out - Passes the pack that implements that id into render (one pack, or one of several installed packs)
- Renders a shell with
#__keel_seed(andseed.headin the document) or answers a visit (X-Keel-Visit) with the same JSON
What a pack must not do
- Own URL patterns
- Fetch its own page payload as a second source of truth, actions are writes; reads stay the seed
- Parse the seed outside bootstrap
- Choose its own pack, or read a visitor theme preference from the browser
{
"v": 1,
"page": "home",
"path": "/",
"data": {
"greeting": "Hello"
},
"theme": {
"id": "harbor",
"version": "1.0.0"
},
"entry": "/__keel/pack/harbor/pages/harbor.home.js",
"host": "#__keel_root"
}
theme is the serving pack’s { id, version }; entry is the page module
Keel loads from that pack.