Safsaf is a Guile web framework, written using Claude Code running Claude Opus 4.6, based off of the Guix Data Service, Nar Herder and Guix Build Coordinator codebases.
6.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Safsaf is a web framework for Guile Scheme, built on Guile Fibers using the Guile Knots web server.
Environment Setup
The project uses direnv with Guix. The .envrc runs use guix -D -f guix-dev.scm, which pulls in all dependencies: guile-knots, guile-webutils, guile-lib, guile-json-4, guile-squee, guile-sqlite3, guile-gcrypt.
Run direnv allow to activate the environment. All Guile dependencies are on GUILE_LOAD_PATH via the Guix profile.
Key Dependencies
Guile Knots
(knots web-server)—run-knots-web-server: the HTTP server. Handler signature is(request body-port) → (values response body).body-portis the port for reading the request body lazily.(knots resource-pool)—with-resource-from-pool: DB/resource connection pooling.(knots parallelism)—fibers-let,fibers-parallel,fibers-map: concurrent work in handlers.(knots thread-pool)—call-with-thread: offload blocking/CPU-bound work.(knots timeout)—with-fibers-timeout,with-port-timeouts: request and I/O timeouts.(knots web)—call-with-connection-cache: outbound HTTP with connection pooling.(knots)—call-with-sigint,format/knots,spawn-fiber/knots.(knots web-server)also exportsmake-chunked-output-port/knots,sanitize-response,request-body-port/knots,read-request-body/knots.
Guile Webutils
(webutils multipart)—parse-request-body,<part>record,parts-ref,parts-ref-string.(webutils cookie)—set-cookie,delete-cookie. RegistersCookie/Set-Cookieheader parsers with(web http).(webutils sessions)—<session-manager>, HMAC-signed cookie sessions. Format:signature$expires$base64-data.(webutils date)— RFC3339 and HTTP date conversions.
Guile JSON (v4.7.3)
(json)— Re-exports everything from parser, builder, and record modules.(json parser)—json->scm(from port),json-string->scm(from string). Options:#:null(default'null),#:ordered(preserve key order).(json builder)—scm->json(to port),scm->json-string(to string). Options:#:pretty,#:unicode,#:validate.(json record)—define-json-mapping: bidirectional SRFI-9 record ↔ JSON conversion.- Data mapping: objects ↔ alists, arrays ↔ vectors, strings ↔ strings, numbers ↔ numbers,
true/false↔#t/#f,null↔'null.
Guile Gcrypt
(gcrypt random)— cryptographic random bytes (used for CSRF token generation).
Guile Lib
(htmlprag)— HTML/SHTML parsing and generation.html->shtml: parse HTML to SXML.shtml->html: render SXML to HTML string.write-shtml-as-html: write SXML to port.(logging logger)— Logging framework.(logging port-log)— log to ports.(logging rotating-log)— rotating file logs.(md5)— MD5 hashing.(container async-queue)—make-async-queue,async-enqueue!,async-dequeue!.(string transform),(string wrap),(string completion)— String utilities.
Guile Standard Library
(web request),(web response),(web uri),(web http)— Guile's built-in HTTP types.(srfi srfi-9)— Record types.(srfi srfi-64)— Test framework.(srfi srfi-71)— Extendedletwith multiple values; prefer over(srfi srfi-11)let-values.
Architecture
Handler signature throughout is (request body-port) → (values response body), using Guile's <request> directly. body-port is the port for reading the request body lazily. Context is threaded via Guile parameters, not a wrapper record.
Safsaf wraps run-knots-web-server with:
- Parameters for context —
current-route-params(alist of matched route bindings),current-reverse-routes(forpath-for). Handler wrappers add their own parameters (e.g.current-csrf-token,current-session). - Router — data-driven route table using
(route method pattern handler). Patterns are lists of segments: strings (literal match), symbols (capture), or(predicate name)pairs. Dotted-tail patterns (e.g.'("api" . rest)) capture remaining segments. Routes can be organized with(route-group prefix ...). Named routes support reverse routing viapath-for. - Handler wrappers — convention:
(foo-handler-wrapper handler) → handler'. A handler wrapper transforms the request on the way in and the response/body on the way out. Wrappers that need configuration provide a(make-foo-handler-wrapper ...)constructor. Applied to route trees viawrap-routes, which accepts one or more wrappers. - Entry point —
(run-safsaf routes #:key host port method-not-allowed? method-not-allowed-handler connection-buffer-size)compiles the route table, builds the dispatch handler, and starts the HTTP server viarun-knots-web-server. When called outside a Fibers scheduler, it wraps everything inrun-fibersand blocks until Ctrl-C. When called inside an existing scheduler (e.g. withinrun-fibers), it just starts the server and returns immediately.method-not-allowed?defaults to#t, enabling automatic 405 responses. Handler wrappers are applied to routes viawrap-routesbefore passing torun-safsaf.
Finding Guile Library Sources
To read source code for Guile dependencies, look them up via GUILE_LOAD_PATH. The first entry is the project's Guix profile directory containing all dependencies. Do not search /gnu/store directly — it is slow and noisy.
A Guile module path like (knots web-server) maps to the file knots/web-server.scm under a load path directory. To find it:
ls "$GUILE_LOAD_PATH" | head # see what's available
cat "$(echo $GUILE_LOAD_PATH | cut -d: -f1)/knots/web-server.scm" # read a specific module
Or use the Read/Glob tools directly against the first GUILE_LOAD_PATH entry (e.g. /gnu/store/...-profile/share/guile/site/3.0/). Module path segments map to directories, with the final segment as <name>.scm. For example:
(json parser)→json/parser.scm(webutils multipart)→webutils/multipart.scm(srfi srfi-9)→srfi/srfi-9.scm
Guile Conventions
- Predicates end with
?. Setters useset-prefix. Constructors usemake-. - Records defined with
define-record-typefrom(srfi srfi-9). - Modules use
define-modulewith#:use-moduleand#:export. - Use
valuesfor multiple return values,call-with-valuesorreceiveto consume them.