Turmeric's web stack is four composable spices:
| Spice | Role |
|---|---|
tur-httpd |
Threaded HTTP/1.1 server |
tur-tls |
TLS termination (mbedTLS) |
tur-tourist |
Routing + middleware micro-framework |
tur-tourist-session |
Cookie-backed sessions with pluggable stores |
tur-template |
ERB/EJS-style string templating |
Each layer can be used independently. This guide wires them all together into a single application: HTTPS server, routed handlers, session-based authentication, and template-rendered views.
The individual guides cover each layer in depth: web-stack-guide, httpd-tls-guide, tourist-routing-guide, tourist-session-guide.
:spices #map{
"httpd" #map{:url "https://github.com/turmeric-lang/turmeric-spices"
:ref "httpd-v0.1.0"
:subdir "spices/httpd"}
"tls" #map{:url "https://github.com/turmeric-lang/turmeric-spices"
:ref "tls-v0.1.0"
:subdir "spices/tls"}
"tourist" #map{:url "https://github.com/turmeric-lang/turmeric-spices"
:ref "tourist-v0.2.5"
:subdir "spices/tourist"}
"tourist-session" #map{:url "https://github.com/turmeric-lang/turmeric-spices"
:ref "tourist-session-v0.1.0"
:subdir "spices/tourist-session"}
"template" #map{:url "https://github.com/turmeric-lang/turmeric-spices"
:ref "template-v0.2.0"
:subdir "spices/template"}
}
Then tur fetch.
A simple authenticated web app with three pages:
/ -- public home page (template-rendered)/login -- POST sets a session, redirects to /dashboard/dashboard -- session-gated, shows the user's name/logout -- destroys the session, redirects to /Generate a self-signed certificate for development:
../turmeric-spices/spices/tls/tools/gen-cert.sh /tmp/tls-smoke-test
Then load it in the application:
(import tls/ctx :refer [tls-ctx-new tls-ctx-free
tls-ctx-load-cert-pem tls-ctx-load-key-pem])
(import tls/httpd :refer [tls-httpd-init])
(defn setup-tls [] : int
(tls-httpd-init)
(let [ctx (tls-ctx-new)]
(tls-ctx-load-cert-pem ctx "/tmp/tls-smoke-test/test-cert.pem")
(tls-ctx-load-key-pem ctx "/tmp/tls-smoke-test/test-key.pem")
ctx))
tls-httpd-init registers the spice's TLS functions with stdlib/httpd.
Call it once before starting the server. The ctx handle is passed to
httpd-new-tls (or threaded through tourist) to enable HTTPS.
For production, use Let's Encrypt certificates:
/etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem. See
the TLS guide for the full details.
(import session/mw :refer [session-mw])
(import session/config :refer [default-session-config])
(import session/memory-store :refer [memory-store-new])
(let [store (memory-store-new)
cfg (default-session-config)]
(session-mw store cfg))
session-mw returns a tourist after-middleware item. Place it directly in
the tourist item list -- it is already a middleware handle, not a value
you wrap in use!.
For production, swap the memory store for a file store:
(import session/file-store :refer [file-store-new])
(let [store (file-store-new "/var/lib/myapp/sessions")]
(session-mw store (default-session-config)))
The default-session-config sets Secure on, HttpOnly on, 24-hour TTL,
SameSite=Lax. For development over plain HTTP, use dev-session-config
which sets Secure off so the cookie survives http://.
(import template/render :refer [render render-file])
(import template/env :refer [env-new env-set env-set-list env-free])
(defn render-page [template-path : cstr title : cstr user : cstr] : cstr
(let [e (env-new)]
(env-set e "title" title)
(env-set e "user" user)
(let [r (render-file template-path e)]
(env-free e)
(if (ok? r)
(ok-val r)
"render error"))))
render-file returns (Result cstr cstr) -- ok is a heap-allocated
string the caller frees, err is a static diagnostic. The template
engine supports <%= var %> interpolation, <% if cond %>...<% else %>...<% end %> conditionals, <% for [x list] %>...<% end %> loops, and
<% let x val %>...<% end %> bindings.
A simple view template (views/home.html.tur):
<h1><%= title %></h1>
<% if user %>
<p>Welcome back, <%= user %>. <a href="/logout">Logout</a></p>
<% else %>
<p><a href="/login">Login</a></p>
<% end %>
(import tourist/dsl :refer [get! post!])
(import tourist/helpers :refer [html redirect status text])
(import tourist/param :refer [capture param])
(import session/ctx :refer [session-get session-set! session-destroy!
session-id])
(import httpd/request :refer [req-body])
;; Home page -- public, template-rendered
(defn home-handler [ctx : int] : int
(let [user-res (session-get ctx "user")
user (if (ok? user-res) (ok-val user-res) "")]
(html (render-page "views/home.html.tur" "Home" user))))
;; Login form (GET) and login action (POST)
(defn login-get [ctx : int] : int
(html "<form method='post'><input name='username'><button>Login</button></form>"))
(defn login-post [ctx : int] : int
(let [username (ok-val (param ctx "username"))]
(if (= username 0)
(status 400 (text "missing username"))
(do
(session-set! ctx "user" username)
(redirect "/dashboard")))))
;; Dashboard -- session-gated
(defn dashboard-handler [ctx : int] : int
(let [user-res (session-get ctx "user")]
(if (ok? user-res)
(html (render-page "views/dashboard.html.tur" "Dashboard" (ok-val user-res)))
(redirect "/login"))))
;; Logout -- destroy session
(defn logout-handler [ctx : int] : int
(session-destroy! ctx)
(redirect "/"))
| Call | Returns | Notes |
|---|---|---|
(session-get ctx key) |
(Result cstr cstr) |
ok value or err "missing" |
(session-set! ctx key val) |
:void |
Marks session dirty; triggers save + Set-Cookie |
(session-del! ctx key) |
:void |
Marks dirty |
(session-destroy! ctx) |
:void |
Deletes session, emits expiring Set-Cookie |
(session-id ctx) |
:cstr |
64-char hex session ID (borrowed) |
A read-only session-get does not dirty the session, so a request that
only reads generates no store write and no Set-Cookie (unless
rolling? is on).
A use! middleware that gates access to protected routes:
(import tourist/middleware :refer [use!])
(import stdlib/option :refer [none-value some])
(defn require-auth [ctx : int] : int
(let [user-res (session-get ctx "user")]
(if (ok? user-res)
(none-value) ;; continue to route
(some (redirect "/login"))))) ;; short-circuit to login
use! items run in declaration order before any route handler. A
middleware function receives the context and returns option<int>:
none-value continues, some(resp) short-circuits.
(import tourist/app :refer [tourist])
(import tourist/dsl :refer [get! post!])
(import tourist/middleware :refer [use!])
(import tourist/static :refer [serve-static!])
(import tourist/routing :refer [url-map! mount!])
(import httpd/server :refer [server-stop])
(import tls/ctx :refer [tls-ctx-free])
(import tls/httpd :refer [tls-httpd-init])
(import session/mw :refer [session-mw])
(import session/config :refer [default-session-config])
(import session/memory-store :refer [memory-store-new])
(defn main [] : int
;; TLS
(tls-httpd-init)
(let [tls-ctx (tls-ctx-new)]
(tls-ctx-load-cert-pem tls-ctx "/tmp/tls-smoke-test/test-cert.pem")
(tls-ctx-load-key-pem tls-ctx "/tmp/tls-smoke-test/test-key.pem")
(let [;; Sessions
store (memory-store-new)
sess-mw (session-mw store (default-session-config))
;; Routes
public-routes
(url-map!
(mount! "/" (get! "/" home-handler))
(mount! "/login"
(url-map!
(mount! "/" (get! "/" login-get)
(post! "/" login-post)))))
protected-routes
(url-map!
(mount! "/dashboard" (get! "/" dashboard-handler))
(mount! "/logout" (get! "/" logout-handler)))
;; Server
srv (tourist 8443
sess-mw
(serve-static! "/assets" "./public")
(use! (fn [ctx]
(let [path (req-path ctx)]
;; gate /dashboard and /logout behind auth
(if (or (str-prefix? path "/dashboard")
(str-prefix? path "/logout"))
(require-auth ctx)
(none-value)))))
public-routes
protected-routes)]
;; run until killed
(server-stop srv)
(tls-ctx-free tls-ctx)
0)))
tls-httpd-init registers the TLS ops with stdlib/httpd.
The tls-ctx loads the cert and key.session-mw installs the store and cookie
config. It is an after-middleware: it loads the session lazily on first
session-get/session-set! and persists on the way out.serve-static! serves /assets/** from ./public/,
rejecting path traversal.use! gates /dashboard and /logout behind
require-auth, which checks the session and redirects to /login if
absent.url-map! + mount! split public and
protected routes into sub-apps. The matched prefix is stripped before
the inner sub-app runs, so handlers see paths relative to their mount
point.| Concern | Recommendation |
|---|---|
| TLS certificates | Use Let's Encrypt; see TLS guide |
| Session store | File store for single-host; custom vtable for Redis/Valkey |
| Cookie config | default-session-config: Secure, HttpOnly, SameSite=Lax |
| Static files | serve-static! with a dedicated mount point |
| Thread pool | tourist uses server-start-pool internally; tune pool size |
| Graceful shutdown | server-stop closes the listening socket and joins threads |
| Session fixation | Destroy + re-create on privilege change (no session-rotate! yet) |
tur-tls v0.1.0.session-rotate! is planned but not shipped.tur-tourist-ws with
ws-route!; see the WebSocket guide.