diff --git a/lib/dream/README.md b/lib/dream/README.md
new file mode 100644
index 00000000..d7c533b5
--- /dev/null
+++ b/lib/dream/README.md
@@ -0,0 +1,79 @@
+# dream-on-sx
+
+OCaml's [Dream](https://aantron.github.io/dream/) web framework, reimplemented in
+**plain SX** on the CEK evaluator. Dream is the cleanest middleware-shaped HTTP
+framework in any language, and it maps onto SX with almost no impedance:
+
+| Dream | SX |
+|-------|-----|
+| `handler = request -> response promise` | `(fn (req) … (perform …))` |
+| `middleware = handler -> handler` | `(fn (next) (fn (req) …))` |
+| `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left fold |
+| `Dream.run handler` | `(dream-run handler)` → `(perform (:http/listen …))` |
+
+There are five types — **request, response, route**, and (as plain functions)
+**handler** and **middleware**. Everything else is a function over them.
+
+## Quickstart
+
+```lisp
+(dream-run
+ (dream-make-app
+ (list
+ (dream-get "/" (fn (req) (dream-html "
Hello, World!
")))
+ (dream-get "/hello/:name"
+ (fn (req) (dream-text (str "Hi, " (dream-param req "name"))))))))
+```
+
+`dream-make-app` wraps the router in the default stack (error catch + content-type).
+`dream-run` installs the root handler on the existing SX HTTP server — it does **not**
+open its own socket.
+
+## Public surface
+
+- **types** — `dream-request`/`dream-response`/`dream-route`, accessors
+ (`dream-method`/`-path`/`-body`/`-header`/`-query-param`/`-param`), smart
+ constructors (`dream-html`/`-text`/`-json`/`-empty`/`-not-found`/`-redirect`),
+ convenience (`dream-queries`, `*-or` defaults, `dream-accepts?`/`dream-wants-json?`).
+- **router** — `dream-get`/`-post`/`-put`/`-delete`/`-patch`/`-head`/`-options`/`-any`,
+ `dream-router`, `dream-scope` (prefix + middleware), `:name` params + `**` catch-all,
+ 405 + `Allow`, automatic HEAD.
+- **middleware** — `dream-pipeline`, `dream-no-middleware`, `dream-logger`,
+ `dream-content-type`, `dream-set-header`, `dream-tap-request`.
+- **session** — `dream-sessions` / `dream-sessions-signed`, `dream-session-field` /
+ `dream-set-session-field` / `dream-session-all` / `dream-invalidate-session`; cookie
+ helpers (`dream-cookie`, `dream-set-cookie`, `dream-cookie-sign`/`-unsign`).
+- **flash** — `dream-flash`, `dream-add-flash-message`, `dream-flash-messages`.
+- **form** — `dream-form` (Ok/Err), `dream-form-fields`, `dream-multipart`, CSRF
+ (`dream-csrf` / `dream-csrf-protect` / `dream-csrf-token` / `dream-csrf-tag`).
+- **websocket** — `dream-websocket`, `dream-send`/`-receive`/`-close`/`-broadcast`.
+- **static** — `dream-static` (mime, ETags, 304, ranges, traversal guard).
+- **error** — `dream-catch`, `dream-status-text`/`-line`, `dream-status-page`.
+- **cors** — `dream-cors`, `dream-cors-origin`, `dream-cors-with`.
+- **json** — `dream-json-encode`/`-parse`, `dream-json-value`, `dream-json-body`.
+- **run / api** — `dream-run`/`-port`/`-opts`, `dream-app`, `dream-make-app`,
+ `dream-serve`.
+
+## Testing story
+
+Every effectful concern is **dependency-injected**, so the whole framework is testable
+without a running host:
+
+- sessions take a backend `(fn (op) …)` — `dream-memory-sessions` for tests,
+ `dream-perform-sessions` in production;
+- static files take an fs — `dream-memory-fs` vs `dream-static-perform-fs`;
+- websockets take an io — `dream-mock-ws` vs `dream-ws-perform-io`;
+- `dream-run` takes a listen transport (`dream-run-with`).
+
+Run the suite: `bash lib/dream/conformance.sh` (367 tests, 14 suites).
+
+## Notes & caveats
+
+- Headers are dicts with **lowercased string keys** (in SX keywords *are* strings, so
+ `:content-type` == `"content-type"`).
+- Outgoing cookies accumulate in a `:set-cookies` list on the response so multiple
+ `Set-Cookie` headers don't collide.
+- The CSRF/cookie/ETag signing uses a pure-SX keyed hash — **not cryptographic**.
+ Production should inject a host HMAC (`dream-csrf-with`, and the signed-session
+ secret path).
+- JSON and multipart are in-memory (not streaming).
diff --git a/lib/dream/api.sx b/lib/dream/api.sx
new file mode 100644
index 00000000..63b1850b
--- /dev/null
+++ b/lib/dream/api.sx
@@ -0,0 +1,33 @@
+;; lib/dream/api.sx — Dream-on-SX public facade.
+;; Loaded last; bundles the modules into a batteries-included surface. The full
+;; public API is the `dream-*` functions across types/router/middleware/session/
+;; flash/form/websocket/static/error/cors/json/run; this file adds convenience
+;; app builders. Depends on all other dream modules.
+
+(define dream-version "0.1.0")
+
+;; standard middleware stack (pure — no IO): error catch outermost, then
+;; content-type sniffing. Logger is opt-in since it performs host IO.
+(define
+ dream-defaults
+ (fn
+ (handler)
+ (dream-pipeline (list dream-catch dream-content-type) handler)))
+
+;; build a complete app handler from a route list with the default stack
+(define
+ dream-make-app
+ (fn (routes) (dream-defaults (dream-router routes))))
+
+;; build an app and wrap it with extra middleware (outermost first)
+(define
+ dream-make-app-with
+ (fn
+ (middlewares routes)
+ (dream-pipeline middlewares (dream-make-app routes))))
+
+;; one-call serve: routes + opts -> installed on the host
+(define
+ dream-serve
+ (fn (routes opts) (dream-run-opts (dream-make-app routes) opts)))
+(define dream-serve-port (fn (routes port) (dream-serve routes {:port port})))
diff --git a/lib/dream/conformance.sh b/lib/dream/conformance.sh
index a66cfc35..0d634986 100644
--- a/lib/dream/conformance.sh
+++ b/lib/dream/conformance.sh
@@ -34,6 +34,7 @@ MODULES=(
"lib/dream/cors.sx"
"lib/dream/json.sx"
"lib/dream/run.sx"
+ "lib/dream/api.sx"
"lib/dream/demos/hello.sx"
"lib/dream/demos/counter.sx"
"lib/dream/demos/chat.sx"
@@ -54,6 +55,7 @@ SUITES=(
"cors dream-co-tests-run! lib/dream/tests/cors.sx"
"json dream-js-tests-run! lib/dream/tests/json.sx"
"run dream-rn-tests-run! lib/dream/tests/run.sx"
+ "api dream-ap-tests-run! lib/dream/tests/api.sx"
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
)
diff --git a/lib/dream/tests/api.sx b/lib/dream/tests/api.sx
new file mode 100644
index 00000000..add9b71f
--- /dev/null
+++ b/lib/dream/tests/api.sx
@@ -0,0 +1,77 @@
+;; lib/dream/tests/api.sx — facade: app builders + default stack.
+
+(define dream-ap-pass 0)
+(define dream-ap-fail 0)
+(define dream-ap-fails (list))
+
+(define
+ dream-ap-test
+ (fn
+ (name actual expected)
+ (if
+ (= actual expected)
+ (set! dream-ap-pass (+ dream-ap-pass 1))
+ (begin
+ (set! dream-ap-fail (+ dream-ap-fail 1))
+ (append! dream-ap-fails {:name name :actual actual :expected expected})))))
+
+(dream-ap-test "version is a string" (string? dream-version) true)
+
+;; ── dream-make-app: routes -> handler with default stack ───────────
+(define
+ dream-ap-routes
+ (list
+ (dream-get "/" (fn (req) (dream-html "hi
")))
+ (dream-get "/boom" (fn (req) (error "kaboom")))
+ (dream-get
+ "/raw"
+ (fn (req) (dream-response 200 {} "plain words")))))
+(define dream-ap-app (dream-make-app dream-ap-routes))
+
+(dream-ap-test
+ "app serves"
+ (dream-resp-body (dream-ap-app (dream-request "GET" "/" {} "")))
+ "hi
")
+(dream-ap-test
+ "app catches errors -> 500"
+ (dream-status (dream-ap-app (dream-request "GET" "/boom" {} "")))
+ 500)
+(dream-ap-test
+ "app 404 for unknown"
+ (dream-status (dream-ap-app (dream-request "GET" "/nope" {} "")))
+ 404)
+(dream-ap-test
+ "app sniffs content-type"
+ (dream-resp-header
+ (dream-ap-app (dream-request "GET" "/raw" {} ""))
+ "content-type")
+ "text/plain; charset=utf-8")
+
+;; ── dream-make-app-with: extra outer middleware ────────────────────
+(define
+ dream-ap-tag
+ (fn (next) (fn (req) (dream-add-header (next req) "X-App" "1"))))
+(define
+ dream-ap-app2
+ (dream-make-app-with (list dream-ap-tag) dream-ap-routes))
+(dream-ap-test
+ "extra middleware header"
+ (dream-resp-header
+ (dream-ap-app2 (dream-request "GET" "/" {} ""))
+ "x-app")
+ "1")
+
+;; ── dream-serve wires through dream-run ────────────────────────────
+(define dream-ap-captured nil)
+(define dream-ap-listen (fn (op) (begin (set! dream-ap-captured op) :ok)))
+(define
+ dream-ap-served
+ (dream-run-with dream-ap-listen (dream-make-app dream-ap-routes) {:port 7000}))
+(dream-ap-test "serve listens" dream-ap-served :ok)
+(dream-ap-test "serve port" (get dream-ap-captured :port) 7000)
+(dream-ap-test
+ "served app runs"
+ (get ((get dream-ap-captured :app) {:method "GET" :target "/"}) :body)
+ "hi
")
+
+(define dream-ap-tests-run! (fn () {:total (+ dream-ap-pass dream-ap-fail) :passed dream-ap-pass :failed dream-ap-fail :fails dream-ap-fails}))
diff --git a/plans/dream-on-sx.md b/plans/dream-on-sx.md
index 89344d77..913d4cb7 100644
--- a/plans/dream-on-sx.md
+++ b/plans/dream-on-sx.md
@@ -124,6 +124,12 @@ with extensions + hardening below.
`dream-param-or` (defaults), `dream-has-query?` / `-header?` / `-param?`,
`dream-content-type-of`, `dream-method-is?`, `dream-accepts?` / `dream-wants-json?`
(Accept-header content negotiation).
+- **2026-06-07 — Ext: api.sx facade + README** (`lib/dream/api.sx`, 9 tests, 367 total).
+ `dream-version`, `dream-defaults` (pure stack: error-catch + content-type; logger is
+ opt-in since it performs IO), `dream-make-app routes`, `dream-make-app-with`,
+ `dream-serve`/`dream-serve-port`. `lib/dream/README.md` documents the full public
+ surface, quickstart, the dependency-injection testing story, and caveats. **All
+ planned extensions complete — 367/367 across 14 suites.**
## Extensions (post-roadmap)
@@ -137,7 +143,7 @@ The five-types core is complete; these harden it toward a production HTTP front
- [x] **Signed session cookies** (`dream-sessions-signed` — tamper-evident sid).
- [x] **JSON helpers** (encode + recursive-descent parse, pure SX).
- [x] **Query/header convenience** (`dream-queries`, `*-or` defaults, `dream-accepts?`).
-- [ ] **`api.sx` facade + README** — single load point listing the public surface.
+- [x] **`api.sx` facade + README** — `dream-make-app` / `dream-serve` + `README.md`.
## Stdlib additions Dream will need