Server-rendered islands for Datastar. An island is a function returning hiccup, defined with
defisland. Each browser tab has a session on the server that renders its islands, and only what changed is rendered
and sent: an island renders again when its arguments change, or when something it read changed, and Datastar morphs
each changed island into the page by id, over one SSE stream per tab.
Two guarantees come with the rendering:
co.multiply/tropical {:mvn/version "0.1.0"}
Requires Clojure 1.12+ and JDK 25+. The wire follows Datastar's SDK specification, tested against the Datastar 1.0.4 client.
(ns my.app
(:require
[co.multiply.tropical.action :refer [use-action]]
[co.multiply.tropical.island :refer [defisland use-watch]]
[co.multiply.tropical.ring :as ring]
[dev.onionpancakes.chassis.core :as h]))
(defonce !clicks (atom 0))
(defisland counter
[]
(let [n (use-watch !clicks)
click (use-action :click (fn [_] (swap! !clicks inc) nil))]
[:p "Clicked " n " times " [:button {:data-on:click click} "+1"]]))
(defisland app
[uid]
[:main [:h1 "Hello, " uid] (counter)])
(defn page
"The page, rendered from the session's first frame, which the body holds
(`ring/mount`). Only its islands are patched from then on."
[_]
(h/html
[h/doctype-html5
[:html
[:head [:script {:type "module"
:src "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"}]]
(ring/mount :body)]]))
(def tropical
{:root (fn [{:keys [uid]}] (app uid))
:page page
;; Your authentication: the user a request is made on behalf of, or nil.
:uid current-user})
(def routes
;; The page, the stream it opens and its actions, all at the page's URL.
[["/" {:handler (ring/handler tropical)}]])
Every tab that shows the counter sees each click: counter read !clicks, so each session renders counter again and
sends that one island.
observe opens a subscription, a poll loop or a query, once
for every reader of its call site and arguments, across sessions, and closes it after the last leaves.
(Reading from outside the page):root.:uid returns the user a request is made on behalf of, or nil.(ring/mount :body attrs): the element holds the session's first frame, opens the
stream, and keeps the state the browser owns. Its head takes the islands' scripts with (ring/scripts nonce).ring/wrap-refused-streams and ring/wrap-scripts, outside the routes, and let actions past body
parsers (ring/request-kind).Serving pages has the whole app map, the CSP, compression, and running it behind a load balancer.
observe, and how a resource lives.ui helpers.The jar carries a clj-kondo config that lints defisland as defn
and defcookie, defstorage, defscript and defshared as def, so editors resolve islands, their params and
states. Import it into your project's .clj-kondo with
clj-kondo --lint "$(clojure -Spath)" --dependencies --copy-configs --skip-lint.
The repository is also a Claude Code plugin, whose tropical skill gives Claude
what it can't infer from the API when it writes islands: what renders when, what a resource is shared by, what the
browser is trusted with, and the mistakes a guess from React or Electric makes. It loads only when the conversation
is working with Tropical. From Claude Code:
/plugin marketplace add multiplyco/tropical
/plugin install tropical@tropical
That installs it for yourself. To offer it to everyone working in a project, commit this to the project's
.claude/settings.json instead; collaborators are asked to install it when they trust the repository. Both keys are
needed, since enabledPlugins alone names a marketplace a fresh clone has never heard of.
{
"extraKnownMarketplaces": {
"tropical": {
"source": { "source": "github", "repo": "multiplyco/tropical" }
}
},
"enabledPlugins": {
"tropical@tropical": true
}
}
bb dev serves a demo page that walks through what the library does, and bb test runs the tests:
Development has the tasks, the demo and how the native libraries are built. Dated measurements
and findings are in the logbook.
Eclipse Public License 2.0. Copyright (c) 2026 Multiply. See LICENSE.
The jar includes libzstd, built from Zstandard's release source
(native/build-zstd.sh), under its BSD license: see
zstd-LICENSE. And it includes libbrotlienc, built from
Brotli's release source (native/build-brotli.sh), under its MIT license: see
brotli-LICENSE.
Authored by @kauppilainen and @eneroth
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |