kabel (German for "cable/wire") is a minimal, modern connection library for building peer-to-peer applications in Clojure and ClojureScript. It models a bidirectional wire to pass Clojure values between symmetric peers over WebSockets.
:auth alias — see Authenticationkabel provides the network layer for several replikativ projects:
Add to your dependencies:
;; deps.edn
{:deps {org.replikativ/kabel {:mvn/version "LATEST"}}}
(ns my-app.core
(:require [kabel.peer :as peer]
[kabel.http-kit :as http-kit]
[superv.async :refer [<?? go-try go-loop-try <? >? S]]
[clojure.core.async :refer [chan]]))
;; Server: echo messages back to client
(def server-id #uuid "05a06e85-e7ca-4213-9fe5-04ae511e50a0")
(def url "ws://localhost:8080")
(defn echo-middleware [[S peer [in out]]]
(go-loop-try S [msg (<? S in)]
(when msg
(>? S out msg)
(recur (<? S in))))
[S peer [(chan) (chan)]])
(def server
(peer/server-peer S
(http-kit/create-http-kit-handler! S url server-id)
server-id
echo-middleware
identity)) ;; or use transit/fressian middleware
(<?? S (peer/start server))
;; Client: send messages and receive responses
(def client-id #uuid "c14c628b-b151-4967-ae0a-7c83e5622d0f")
(def client
(peer/client-peer S client-id
(fn [[S peer [in out]]]
(go-try S
(>? S out {:msg "Hello, kabel!"})
(println "Response:" (<? S in)))
[S peer [(chan) (chan)]])
identity))
(<?? S (peer/connect S client url))
kabel includes a topic-based publish/subscribe system with built-in backpressure for initial synchronization.
(ns my-app.server
(:require [kabel.peer :as peer]
[kabel.http-kit :as http-kit]
[kabel.pubsub :as pubsub]
[kabel.pubsub.protocol :as proto]
[superv.async :refer [S <??]]))
;; Create pubsub context
(def ctx (pubsub/make-context S {:batch-size 10
:batch-timeout-ms 30000}))
;; Register a topic with a sync strategy
(pubsub/register-topic! ctx :notifications
(proto/pub-sub-only-strategy
(fn [payload] (println "Received:" payload))))
;; Create server with pubsub middleware
(def server
(peer/server-peer S
(http-kit/create-http-kit-handler! S "ws://localhost:8080" :server-id)
:server-id
(pubsub/pubsub-middleware ctx)
identity))
(<?? S (peer/start server))
;; Publish to all subscribers
(<?? S (pubsub/publish! ctx :notifications {:event "user-joined" :user "alice"}))
(ns my-app.client
(:require [kabel.peer :as peer]
[kabel.pubsub :as pubsub]
[kabel.pubsub.protocol :as proto]
[superv.async :refer [S <??]]))
;; Create client pubsub context
(def ctx (pubsub/make-context S {}))
;; Define what happens when we receive publishes
(def strategy
(proto/pub-sub-only-strategy
(fn [payload]
(println "Notification:" payload))))
;; Create client with pubsub middleware
(def client
(peer/client-peer S :client-id
(pubsub/pubsub-middleware ctx)
identity))
(<?? S (peer/connect S client "ws://localhost:8080"))
;; Subscribe to topic
(<?? S (pubsub/subscribe! ctx [:notifications] {:notifications strategy}))
For scenarios requiring initial state synchronization (e.g., syncing a database), implement the PSyncStrategy protocol:
(defrecord MySyncStrategy [store]
proto/PSyncStrategy
(-init-client-state [_]
;; Return channel with client's current state
(go {:last-sync-time (get-last-sync store)}))
(-handshake-items [_ client-state]
;; Return channel yielding items newer than client's state
(get-items-since store (:last-sync-time client-state)))
(-apply-handshake-item [_ item]
;; Apply received item to local store
(go (save-item! store item) {:ok true}))
(-apply-publish [_ payload]
;; Handle incremental publish
(go (save-item! store payload) {:ok true})))
Middlewares are composable functions that transform the [S peer [in out]] channel tuple. They can filter, transform, serialize, or route messages.
| Middleware | id | Description |
|---|---|---|
kabel.middleware.cbor/cbor | 14 | boring — RFC 8949 CBOR. Fast, JVM and ClojureScript, and readable by any language |
kabel.middleware.transit/transit | Efficient binary (JSON/MessagePack) with custom type support | |
kabel.middleware.fressian/fressian | Clojure-optimized binary format, JVM only | |
kabel.middleware.json/json | Plain JSON for non-Clojure interop | |
identity | EDN via pr-str/read-string (default) |
(require '[kabel.middleware.cbor :refer [cbor]])
(def server
(peer/server-peer S handler server-id
my-middleware
cbor)) ;; RFC 8949 CBOR on the wire
stringref is off by default here, unlike boring's own default. Tags 25/256 are a schmorp extension that most CBOR libraries do not implement, so leaving it on would make every frame unreadable to exactly the non-Clojure peers the format exists to reach. It buys almost nothing on this wire anyway once permessage-deflate is in play — on one 500-message capture, 95 624 raw bytes became 10 816 deflated with stringref on and 10 787 with it off, a 0.3% difference, because deflate finds the same repetition stringref does.
A frame's leading 4 bytes are the serialization id, so a peer that receives an
id it does not know cannot decode the frame. Switching every peer at once is
not usually possible, so kabel.middleware.dual makes it two deploys:
dual-read-fressian-write — it understands CBOR
while still writing fressian, so old peers keep working;dual-read-cbor-write.kabel.middleware.block-detector): Warns when channels are blocked > 5 secondskabel.middleware.handler): Generic callback middleware for custom transformskabel.middleware.wamp): Experimental WAMP protocol clientkabel ships an optional authentication subsystem under kabel.auth.*, kept
behind the :auth alias so the base library pulls no JSON/JWT/crypto
dependencies. It provides trusted-issuer JWT validation on the WebSocket
handshake, cross-platform (JVM + browser + Node) HS256, JWKS-backed RS256 for
external identity providers (WorkOS, Clerk, Auth0, …), password hashing, reitit
auth routes, and a pluggable identity/session store.
This was previously the separate
kabel-authlibrary; it has been folded into kabel so the transport and its auth layer version and release together. The namespaces movedkabel-auth.* → kabel.auth.*. The old kabel-auth repo is deprecated.
Add the auth dependencies (mirrors kabel's :auth alias — only needed if you use auth):
;; deps.edn
{:aliases {:auth {:extra-deps {metosin/jsonista {:mvn/version "1.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
org.replikativ/geheimnis {:mvn/version "0.2.33"}}}}}
(require '[kabel.auth.jwt :as jwt]
'[kabel.auth.http-kit :as auth-hk]
'[superv.async :refer [S]])
;; A validator is (fn [ring-req] -> principal-map | nil).
(def validate! (jwt/build-bearer-validator {:alg :HS256 :secret "your-secret"}))
(def handler
(auth-hk/create-authenticated-http-kit-handler! S url peer-id validate!))
Authenticated messages carry :kabel/principal (the JWT claims). HS256 signing
(jwt/sign-hs256) and verification work identically on the JVM and in
ClojureScript (browser/Node), so a CLJS peer can both mint and verify tokens.
RS256 verification is JVM-only.
Register multiple issuers keyed by the token iss; the alg is pinned per
issuer (never taken from the token header — this defeats alg:none and
RS256→HS256 downgrades). A JWKS resolver fetches and caches an issuer's rotating
public keys — the WorkOS / Clerk / OIDC path, out of the box:
(require '[kabel.auth.jwks :as jwks])
(def validate!
(jwt/build-bearer-validator
{:issuers {"simmis" {:alg :HS256 :secret secret}
"https://api.workos.com/user_management/CLIENT_ID"
{:alg :RS256 :jwks-url "https://api.workos.com/sso/jwks/CLIENT_ID"}}
:key-resolver (jwks/make-key-resolver)})) ; per-url cache, refetch on kid miss
kabel.auth.store.protocol/AuthStore abstracts party + session storage. A
portable in-memory store ships for tests and lightweight peers
(kabel.auth.store.memory, .cljc — JVM, Node and browser); a datahike-backed
store ships for the JVM (kabel.auth.store.datahike, needs the consumer's
datahike). Password hashing (kabel.auth.password, buddy-hashers) and reitit
auth routes (kabel.auth.routes: login / register / refresh) complete a
server-side credential flow. Auth tests run with clojure -X:auth:test.
WebSockets provide several benefits over REST for peer-to-peer applications:
While WebSocket is the primary transport, kabel's architecture supports pluggable transports. Future versions may include WebRTC for true P2P (no relay server), WebTransport (HTTP/3), Server-Sent Events, or raw TCP/UDP sockets.
The tradeoff is that REST is more standardized and offers better interoperability for non-Clojure clients.

Each connection has a pair of channels, but at the core the peer uses a pub-sub architecture. You can pass messages to other clients through this pub-sub core or subscribe to specific message types:
(let [[bus-in bus-out] (get-in @peer [:volatile :chans])
b-chan (chan)]
(async/sub bus-out :broadcast b-chan)
(async/put! bus-in {:type :broadcast :hello :everybody})
(<!! b-chan))
The project uses deps.edn and tools.build for Clojure, and shadow-cljs for ClojureScript.
# Compile Java helper classes
clj -T:build compile-java
# Install npm dependencies (for ClojureScript)
npm install
# Run the pingpong example
clj -M:pingpong
# Check code formatting
clj -M:format
# Auto-fix formatting
clj -M:ffix
# JVM tests (:auth is required — kabel.auth.jwt will not load without it)
clj -X:auth:test
# ...plus the Jetty half of the adapter parity test, which is what CI runs.
# Without :jetty that half prints SKIPPED instead of running.
clj -X:auth:test:jetty
# ...plus permessage-deflate against an http-kit that has it (see :pmd in
# deps.edn; needs a one-off `clojure -X:deps prep :aliases '[:pmd]'`)
clj -X:auth:test:jetty:pmd
# ClojureScript (Node.js)
npx shadow-cljs compile node-test && node target/node-tests.js
# ClojureScript (Browser)
npx shadow-cljs watch test
# Open http://localhost:8022
# Integration tests (JVM server + Node.js client)
./test-integration.sh
Currently kabel supports WebSockets via:
Server IO lives in kabel.ring-ws, written against
ring.websocket.protocols
rather than any one server. kabel.http-kit and kabel.jetty are thin
namespaces that supply theirs; both take the same arguments and return the same
map, so switching is one line:
(:require [kabel.jetty :as jetty]) ;; instead of kabel.http-kit
(peer/server-peer S
(jetty/create-jetty-handler! S url server-id) ;; instead of create-http-kit-handler!
server-id middleware identity)
http-kit is the default and stays kabel's only declared server dependency: 207 KB with no transitive runtime dependencies, virtual-threaded by default on JDK 21+, and native-image tested across nine platform combinations.
Reach for Jetty when you want what http-kit does not offer: in-process TLS termination, HTTP/2, connection caps, idle timeouts, request-rate limiting, or metrics. It is a provided dependency — add it yourself:
info.sunng/ring-jetty9-adapter {:mvn/version "0.40.2"}
That adapter rather than ring/ring-jetty-adapter: the official one routes
through Jetty's ee9 legacy servlet environment and hardcodes HTTP/1.1, while
this one tracks Jetty 12 core and exposes HTTP/2 and HTTP/3 as options. Options
under :server-opts reach Jetty directly, so :ssl?, :h2?, :max-idle-time
and :thread-pool work by naming them.
One difference worth knowing: Jetty negotiates
permessage-deflate out of the
box, http-kit does not yet
(http-kit#617). On a
fressian/CBOR wire that is a large saving. kabel's Tyrus client offers the
extension via org.replikativ.kabel.PerMessageDeflateExtension, so a JVM client
gets compression against a Jetty-backed peer today.
Copyright © 2015-2025 Christian Weilbach, 2015 Konrad Kühne
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
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 |