Server-rendered islands for Datastar. An island is a function returning hiccup, defined with
defisland. Each browser tab has a session on the server, which renders the tab's islands again when something they
read changes, and sends them over one SSE stream for Datastar to morph into the page.
Three guarantees come with the rendering:
assert included, renders an error view in the island's
place. Its parent never sees the exception, and the rest of the page goes on. The default view shows only a
reference, which the exception is logged under, so nothing the exception says reaches the browser.co.multiply/tropical {:mvn/version "0.8.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,
without rendering app, 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. The first reloads a page
whose stream the route's middleware refused, as when its login expired, and the second serves the scripts islands
declare. Body parsers let actions past (ring/request-kind).Serving pages has the whole app map, the CSP, compression, and running it behind a load balancer.
Writing an app:
observe, and how a resource lives.ui helpers.Running it:
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 also carries a skill for coding agents, tropical, in the open Agent Skills
format. It gives an agent 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. An agent loads
it only when the work involves Tropical.
For Codex, Cursor, Gemini CLI, GitHub Copilot and other agents that read skills, the
skills CLI installs it in the project, or with -g, for yourself:
npx skills add multiplyco/tropical
Or copy its directory, plugins/tropical/skills/tropical, to the project's .agents/skills/, where Codex and several
other agents look for skills.
For Claude Code, the repository is a plugin marketplace:
/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. For working on
Tropical itself, the design notes record why it is built this way and the alternatives rejected, and
the logbook has dated measurements and findings.
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 |