prompts/skills/clojure-sente/SKILL.md
Realtime bidirectional communications between Clojure server and ClojureScript client. Use when building apps where BOTH client and server use sente - NOT for connecting to third-party WebSocket APIs. Provides server push, realtime updates, and reliable async messaging with automatic WebSocket/Ajax fallback.
npx skillsauth add ramblurr/nix-devenv clojure-senteInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Realtime web communications library for Clojure/Script using WebSockets with automatic Ajax fallback.
Sente provides bidirectional async communications between a Clojure server and ClojureScript browser client, with automatic protocol selection (WebSocket or long-polling), reconnection handling, and event batching. Send arbitrary Clojure values between client and server with efficient serialization.
NOTE: Sente is for communication between your own Clojure server and ClojureScript client - both sides must use sente. It is NOT a generic WebSocket client for connecting to third-party WebSocket APIs. For connecting to external WebSocket servers, use hato, http-kit client, or gniazdo instead.
deps.edn:
com.taoensso/sente {:mvn/version "1.21.0"}
Leiningen:
[com.taoensso/sente "1.21.0"]
See https://clojars.org/com.taoensso/sente for the latest version.
Server (Clojure):
(ns my-app.server
(:require
[taoensso.sente :as sente]
[taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[compojure.core :refer [defroutes GET POST]]))
;; Create channel socket server
(let [{:keys [ch-recv send-fn connected-uids
ajax-post-fn ajax-get-or-ws-handshake-fn]}
(sente/make-channel-socket-server! (get-sch-adapter) {})]
(def ring-ajax-post ajax-post-fn)
(def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn)
(def ch-chsk ch-recv) ; Receive channel
(def chsk-send! send-fn) ; Send function
(def connected-uids connected-uids)) ; Atom of connected users
;; Add routes for channel socket
(defroutes my-routes
(GET "/chsk" req (ring-ajax-get-or-ws-handshake req))
(POST "/chsk" req (ring-ajax-post req)))
;; Wrap with necessary middleware
(def app
(-> my-routes
wrap-keyword-params
wrap-params
wrap-anti-forgery ; Important for security
wrap-session))
Client (ClojureScript):
(ns my-app.client
(:require
[taoensso.sente :as sente :refer [cb-success?]]))
;; Get CSRF token from page
(def ?csrf-token
(when-let [el (.getElementById js/document "sente-csrf-token")]
(.getAttribute el "data-csrf-token")))
;; Create channel socket client
(let [{:keys [chsk ch-recv send-fn state]}
(sente/make-channel-socket-client!
"/chsk" ; Must match server route
?csrf-token
{:type :auto})] ; :auto, :ws, or :ajax
(def chsk chsk)
(def ch-chsk ch-recv) ; Receive channel
(def chsk-send! send-fn) ; Send function
(def chsk-state state)) ; Watchable state atom
HTML (include CSRF token):
;; In your Hiccup/HTML template
(let [csrf-token (force ring.middleware.anti-forgery/*anti-forgery-token*)]
[:div#sente-csrf-token {:data-csrf-token csrf-token}])
Event - Messages have the form [event-id event-data]:
[:my-app/some-event {:data "value"}]
Event-msg - Events arrive wrapped in maps with metadata:
;; Server event-msg
{:event [:my-app/some-event {:data "value"}]
:id :my-app/some-event
:?data {:data "value"}
:ring-req {...} ; Ring request map
:?reply-fn (fn [reply]) ; Present when client requested reply
:uid "user-123" ; User ID
:client-id "..."} ; Specific client/tab
;; Client event-msg
{:event [:my-app/some-event {:data "value"}]
:id :my-app/some-event
:?data {:data "value"}
:send-fn chsk-send!}
User vs Client - Sente distinguishes between users and clients:
Client to Server (with optional reply):
;; Fire and forget
(chsk-send! [:my-app/request {:user-input "data"}])
;; With callback for reply
(chsk-send!
[:my-app/request {:user-input "data"}]
5000 ; Timeout in ms
(fn [reply]
(if (sente/cb-success? reply) ; Check for :chsk/closed, :chsk/timeout, :chsk/error
(println "Success:" reply)
(println "Failed"))))
Server to User (push):
;; Send to all clients of a specific user
(chsk-send! "user-id" [:my-app/notification {:msg "New update!"}])
;; Send returns true if at least one client received the message
(when-not (chsk-send! user-id event)
(println "User not connected"))
Server Reply to Client Request:
;; In your event handler on server
(defn handle-request [{:keys [?reply-fn ?data]}]
(when ?reply-fn
(?reply-fn {:status :ok :result "processed"})))
Use a multimethod to dispatch events by ID:
Client:
(defmulti -event-msg-handler :id)
(defmethod -event-msg-handler :default
[{:keys [event]}]
(println "Unhandled event:" event))
(defmethod -event-msg-handler :chsk/state
[{:keys [?data]}]
(let [[old-state new-state] ?data]
(if (:first-open? new-state)
(println "Channel socket successfully established!")
(println "Channel socket state change:" new-state))))
(defmethod -event-msg-handler :my-app/notification
[{:keys [?data]}]
(println "Received notification:" ?data))
;; Start event router
(defonce router
(sente/start-client-chsk-router! ch-chsk -event-msg-handler))
Server:
(defmulti -event-msg-handler :id)
(defmethod -event-msg-handler :default
[{:keys [event]}]
(println "Unhandled event:" event))
(defmethod -event-msg-handler :my-app/request
[{:keys [?data ?reply-fn uid ring-req]}]
(println "Request from user" uid ":" ?data)
(when ?reply-fn
(?reply-fn {:status :ok})))
;; Start event router
(defonce router
(sente/start-server-chsk-router! ch-chsk -event-msg-handler))
Set user ID in one of two ways:
;; In your login handler
{:status 200
:session (assoc session :uid "user-123")}
(sente/make-channel-socket-server!
(get-sch-adapter)
{:user-id-fn (fn [ring-req]
(get-in ring-req [:params :user-id]))})
For anonymous/per-session users, use a random UUID:
{:session (assoc session :uid (str (java.util.UUID/randomUUID)))}
Watch the connected-uids atom to track who's online:
;; Server-side
(add-watch connected-uids :watcher
(fn [_ _ old-state new-state]
(when (not= old-state new-state)
(println "Connected users changed:")
(println " WebSocket:" (:ws new-state)) ; Set of user IDs
(println " Ajax:" (:ajax new-state)) ; Set of user IDs
(println " All:" (:any new-state))))) ; Set of all user IDs
Client-side state changes trigger :chsk/state events:
;; Client receives [:chsk/state [old-state new-state]] events
(defmethod -event-msg-handler :chsk/state
[{:keys [?data]}]
(let [[old new] ?data]
(if (:first-open? new)
(println "Connected!")
(when (not= (:open? old) (:open? new))
(if (:open? new)
(println "Reconnected")
(println "Disconnected"))))))
State map keys:
:open? - Is connection currently open?:first-open? - First successful connection?:ever-opened? - Has ever connected successfully?:type - Current protocol (:ws or :ajax):uid - User ID from server:csrf-token - CSRF token from serverBroadcast to all connected users:
;; Server
(doseq [uid (:any @connected-uids)]
(chsk-send! uid [:my-app/broadcast {:msg "System announcement"}]))
Request/response with timeout:
;; Client
(chsk-send!
[:my-app/fetch-data {:id 123}]
3000
(fn [reply]
(if (sente/cb-success? reply)
(update-ui! reply)
(show-error! "Request timed out"))))
Wait for connection before sending:
;; Client - wait for first connection
(defonce connected? (atom false))
(defmethod -event-msg-handler :chsk/state
[{:keys [?data]}]
(when (:first-open? (second ?data))
(reset! connected? true)
(chsk-send! [:my-app/init {}])))
Lifecycle management with component:
;; Both start-client-chsk-router! and start-server-chsk-router!
;; return a (fn stop []) for cleanup
(defonce router
(sente/start-server-chsk-router! ch-chsk event-msg-handler))
;; Later, on shutdown:
(router) ; Stops the router
Sente supports multiple web servers via adapters:
;; http-kit
[taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]
;; Immutant
[taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]]
;; Aleph
[taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]]
;; nginx-clojure
[taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]]
;; Jetty 9+
[taoensso.sente.server-adapters.jetty9 :refer [get-sch-adapter]]
All use the same (get-sch-adapter) function.
Sente uses "packers" for serialization. Default is edn:
;; Using Transit (recommended for performance + binary data)
(require '[taoensso.sente.packers.transit :as sente-transit])
(sente/make-channel-socket-server!
(get-sch-adapter)
{:packer (sente-transit/get-packer :json)}) ; or :msgpack
;; Custom Transit handlers (e.g., for Joda Time)
(def packer
(sente-transit/->TransitPacker
:json
{:handlers {org.joda.time.DateTime my-write-handler}}
{:handlers {"m" my-read-handler}}))
Client and server must use the same packer.
Event ordering - Sente does NOT guarantee event ordering. Events may arrive out of order due to buffering, async serialization, etc. Don't depend on ordering.
Large payloads - Do NOT use Sente for payloads > 1MB:
Security requirements:
User ID for push - Server push requires a user ID. Client->server requests don't need one, but server->client push does. Set via Ring session :uid key or custom :user-id-fn.
Session modification - WebSocket events use the INITIAL handshake request's session. To modify sessions (login/logout), use regular HTTP Ajax, not WebSocket events.
Router lifecycle - The router functions return a stop function. Call it on shutdown to clean up (though the cost of not doing so is minimal - just a parked go thread).
For these features, consult the official documentation:
testing
Use this OCP when executing or preparing to execute commands that change a live or important system, service reloads/restarts, package changes, deployments, migrations, firewall/network/access changes, credential rotation, NixOS switch/test/boot/deploy, or incident mitigation. It guides safe operations with a persisted ledger for scope, preflight, baseline, rollback, validation, and evidence.
development
Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
documentation
Naming conventions for workflow documents in prompts/. Use when creating plans, PRDs, research reports, idea capture or other workflow documents. Triggers on (1) creating new planning documents, (2) naming PRDs or research reports, (3) questions about document organization in prompts/.
testing
Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.