
Signals flow in both directions. The client declares them with data-signals; the server pushes new values with patch-signals. Input elements create signals with data-bind: the attribute value is the signal name.
(:input :data-bind "name") ; creates $name, two-way bound to the inputBy design, Datastar sends all current signal values with every request, except those staring with _: the server always has the required client state.
Three options, from simplest to most ergonomic (especially for multiple signals):
; 1) One signal by key string
(d*:read-signal hunchentoot:*request* "name")
; 2) All signals into a hash-table
(let ((signals (d*:read-signals hunchentoot:*request*)))
(gethash "name" signals))
; 3) Multiple bindings with defaults in one call
(d*:with-signals ((name "name" "stranger")
(greeting "greeting" "Hello"))
hunchentoot:*request*
(format nil "~A, ~A!" greeting name))init-signals builds a data-signals JSON string from a plist, converting keyword names to camelCase:
(:body :data-signals
(d*:init-signals :time "" :ticks 0 :name "" :message ""))On Clack the API is identical; pass the environment instead of the Hunchentoot request:
(defun hi-handler (env)
(lambda (responder)
(let ((name (d*:read-signal env "name")))
(d*:with-sse (gen (env responder))
(d*:patch-signals gen
(list "message" (format nil "Hello, ~A!" name)))))))Choose a greeting and enter a name. The server reads both signals and patches the message.
;;;; -*- Mode: LISP; fill-column: 80; coding: utf-8 -*-
;;;; DEMO.LISP --- Minimal Hunchentoot example
;;;;
;;;; Copyright (C) 2025, 2026 Frederico Muñoz / ΛↃ lambda combine
;;;;
;;;; This file is part of datastar-cl, the Common Lisp SDK for Datastar
;;;;
;;;; License: MIT
(ql:quickload '(:hunchentoot :spinneret :datastar-cl/hunchentoot))
(defpackage #:clock
(:use #:cl #:hunchentoot)
(:local-nicknames (:sp :spinneret) (:d* :datastar-cl) (:ht :hunchentoot)))
(in-package #:clock)
(ht:define-easy-handler (index :uri "/") ()
(setf (ht:content-type*) "text/html")
(sp:with-html-string
(:doctype)
(:html
(:head (:script :type "module" :src (d*:datastar-url)))
(:body :data-signals (d*:init-signals :time "--:--:--" :ticks 0
:name "" :greeting "Hello" :message "")
:data-init (d*:sse-get "/sse")
(:h1 "Datastar-CL SSE Demo")
(:div :id "clock" :data-text "$time")
(:div "Ticks: " (:strong :data-text "$ticks"))
(:p (:select :data-bind "greeting"
(:option :value "Hello" "Hello")
(:option :value "Hi" "Hi")
(:option :value "Hey" "Hey")
(:option :value "Greetings" "Greetings"))
" "
(:input :placeholder "Your name" :data-bind "name")
" "
(:button :|data-on:click| (d*:sse-get "/hi") "Say hi")
(:p :data-text "$message"))))))
(ht:define-easy-handler (sse-handler :uri "/sse") ()
(let ((tick 0))
(d*:with-sse (gen ht:*request*)
(loop
(multiple-value-bind (s m h) (decode-universal-time (get-universal-time))
(d*:patch-signals gen
(list "time" (format nil "~2,'0d:~2,'0d:~2,'0d" h m s)
"ticks" (incf tick))))
(sleep 1)))))
;;; Signal reading progression (simplest ---> most ergonomic):
;;;
;;; 1) READ-SIGNAL: read one signal by key string
;;; (d*:read-signal ht:*request* "name")
;;;
;;; 2) READ-SIGNALS: parse all signals into a hash-table (once)
;;; (let ((signals (d*:read-signals ht:*request*)))
;;; (gethash "name" signals))
;;;
;;; 3) WITH-SIGNALS: bind N variables in one call (below)
;;; (d*:with-signals ((name "name" "default") ...) req body)
;;;
;;; Below we use WITH-SIGNALS to read greeting + name in one parse, with default
;;; values when either signal is absent.
(ht:define-easy-handler (hi :uri "/hi") ()
(d*:with-signals ((name "name" "stranger")
(greeting "greeting" "Hello"))
ht:*request*
(d*:with-sse (gen ht:*request*)
(d*:patch-signals gen
(list "message"
(format nil "~A, ~A!" greeting name))))))
(ht:start (make-instance 'ht:easy-acceptor :port 8989))
(format t "~&Server started on http://localhost:8989~%")
The same example on Clack, showing the identical signal-reading API with a different backend:
;;;; -*- Mode: LISP; fill-column: 80; coding: utf-8 -*-
;;;; DEMO-CLACK.LISP --- Minimal Clack example
;;;;
;;;; Copyright (C) 2025, 2026 Frederico Muñoz / ΛↃ lambda combine
;;;;
;;;; This file is part of datastar-cl, the Common Lisp SDK for Datastar
;;;;
;;;; License: MIT
;;; NOTE: A blocking ~with-sse~ body (e.g. loop ... sleep) blocks the Clack
;;; worker thread for the duration of the connection _if using Woo_ -- by
;;; design. Clack+Woo is suitable for the one-shot ~/hi~ endpoint out-of-the box
;;; but for long-lived streaming check the documentation: blocking the event
;;; loop will block Woo. This example uses Hunchentoo as the backend server for
;;; Clack, but Woo is also tested (changeable in :backend below)
(ql:quickload '(:clack :spinneret :datastar-cl/clack))
(defpackage #:clock-clack
(:use #:cl)
(:local-nicknames (:sp :spinneret) (:d* :datastar-cl)))
(in-package #:clock-clack)
(defun render-index ()
(sp:with-html-string
(:doctype)
(:html
(:head (:script :type "module" :src (d*:datastar-url)))
(:body :data-signals "{time: '--:--:--', ticks: 0, name: '', message: ''}"
:data-init (d*:sse-get "/sse")
(:h1 "Datastar-CL SSE Demo (Clack)")
(:div :id "clock" :data-text "$time")
(:div "Ticks: " (:strong :data-text "$ticks"))
(:p (:input :placeholder "Your name" :data-bind "name")
(:button :|data-on:click| (d*:sse-get "/hi") "Say hi")
(:p :data-text "$message"))))))
(defun sse-handler (env)
(lambda (responder)
(let ((tick 0))
(d*:with-sse (gen (env responder))
(loop
(multiple-value-bind (s m h) (decode-universal-time (get-universal-time))
(d*:patch-signals gen
(list "time" (format nil "~2,'0d:~2,'0d:~2,'0d" h m s)
"ticks" (incf tick))))
(sleep 1))))))
(defun hi-handler (env)
(lambda (responder)
(let ((name (d*:read-signal env "name")))
(d*:with-sse (gen (env responder))
(d*:patch-signals gen
(list "message"
(format nil "Hello, ~A!"
(if (zerop (length name))
"stranger"
name))))))))
(defun app (env)
(let ((path (getf env :path-info)))
(cond
((string= path "/") `(200 (:content-type "text/html; charset=utf-8")
(,(render-index))))
((string= path "/sse") (sse-handler env))
((string= path "/hi") (hi-handler env))
(t '(404 (:content-type "text/plain") ("Not Found"))))))
(clack:clackup #'app :server :hunchentoot :port 8989)
(format t "~&Server started on http://localhost:8989~%")
(defroute guide-interaction (:get :text/html))