# App Server The App Server is Deft's production deployment layer for web apps. A single app file serves in single-process mode during development; in production the same file runs in N worker runtimes, each binding the same port with `SO_REUSEPORT`. The kernel load-balances incoming connections across the worker sockets — there is no application-level proxy hop. ``` ┌──────────────────────────────────┐ │ Parent runtime │ │ (no listener, no traffic) │ │ • file watcher (reload) │ │ • cluster state │ │ • http/cluster-* stdlib functions │ └──────────────┬───────────────────┘ │ createChild + spawnFile ┌──────────────┴───────────┐ │ │ ┌─────┴─────┐ ┌─────┴─────┐ │ Worker 0 │ │ Worker N │ │ SO_REUSEPORT │ SO_REUSEPORT │ own VM │ │ own VM │ │ own heap │ │ own heap │ │ own thread│ │ own thread│ └───────────┘ └───────────┘ ``` Each worker is a fully isolated runtime (own VM, heap, event loop, OS thread) that loads the app file independently. The same app file works in both single-process and cluster mode — in cluster-worker mode, `[vwait]` is a no-op (the loop thread services requests via its tick loop) and `[http/serve]` automatically sets `SO_REUSEPORT`. The underlying HTTP machinery (routers, middleware, templates) is documented separately in [HTTP Server](http-server). ## Usage The app file is unchanged from single-process mode:
 # app.dft
def site %{ routes @{ %{ path "" method :GET handler { "hello" } } } } |> [http/router]
[http/serve $site :3000]
[vwait] 
A separate driver starts the cluster:
 # driver.dft
[http/serve-cluster 3000 "app.dft"]   # n-workers defaults to [os/cpu-count]
[vwait]                                # parent stays alive for reloads + watchers 
`http/serve-cluster` takes: | Arg | Type | Notes | |---|---|---| | `port` | number | TCP port to bind | | `app-path` | string | App file each worker loads | | `n-workers` | number | Optional, defaults to `os/cpu-count` | | `opts` | map | Optional, e.g. `%{ :app-name "api" :watch-dirs @{...} }` | Explicit worker count:
 [http/serve-cluster 3000 "app.dft" 8] 
Watching content directories (the parent watches these for reloads; workers skip their own `--watch` in cluster mode):
 [http/serve-cluster 3000 "app.dft" %{ :watch-dirs @{"packages/help/manual" "www/pages"} }]
[vwait] 
The parent stays alive (via `vwait`) to service the file watcher for reloads. Watch dirs default to the app file's directory; pass `:watch-dirs` to add content trees (e.g. a `docs/*.md` directory the app renders). ## Control stdlib functions | Function | Purpose | |---|---| | `[http/cluster-metrics]` | Full info map: port, app-name, app-path, workers, generation | | `[http/cluster-status]` | Lightweight health: `:status`, `:healthy`, port, workers, generation | | `[http/cluster-reload]` | Trigger a new-generation reload manually; returns info map | | `[http/cluster-stop]` | Destroy all workers and clear cluster state | ## Reload (new-generation swap) The parent watches the app file. On change (or on `[http/cluster-reload]`): 1. N fresh worker runtimes are spawned (each binds `SO_REUSEPORT` — the kernel immediately starts sending them accepts). 2. Old workers' listener fds are closed (kernel stops routing to them). 3. Old worker runtimes are joined and destroyed. 4. `generation` is incremented. There is a brief window during the swap where both old and new workers are bound. In-flight requests on old workers are interrupted when their runtimes are joined; clients retry and hit fresh workers. ## Worker isolation Workers do not share mutable state. Each has its own heap, loaded modules, and caches. State that must be consistent across workers must go through: - **SQLite file (WAL mode)** — render caches, structured state. Each worker opens its own connection; WAL allows concurrent readers that don't block the writer. - **`config/*` registry** — persistent user preferences. - **`pub/sub`** — ephemeral messaging between runtimes. Session state in a worker-local `def` is visible to only a subset of requests and is lost on reload. ## Cross-worker cache convention For a render cache or any structured cross-worker state, open a SQLite file in WAL mode. Cache file convention: `.deft/http_.cache` in the workspace root, where `` derives from the app-path basename (or the `:app-name` opt).
 def db [sqlite/open ".deft/http_app.cache"]
[sqlite/exec $db "PRAGMA journal_mode=WAL"]
[sqlite/exec $db "PRAGMA synchronous=NORMAL"]
[sqlite/exec $db "CREATE TABLE IF NOT EXISTS render_cache (
    path TEXT PRIMARY KEY,
    hash TEXT,
    body TEXT,
    ts INTEGER
)"] 
If cache keys are **content-addressed** (include a source hash), stale entries are never hit and no explicit invalidation is needed on reload. If keys are **path-based**, the parent publishes a `"cache-invalidate"` pubsub message on file change; workers subscribe and delete the matching rows. ## Performance notes - Each worker independently loads the app, parses templates, etc. Memory cost is N× a single-process deployment. - The accept path scales linearly with worker count (kernel-level distribution, no user-space bottleneck). - `os/cpu-count` is the recommended default for `n-workers`. ## Example: deft-lang.org The deft-lang.org site ships in `www/` and is served by the App Server. The app file builds the routers and serves single-process for development:
 # www/app.dft
def site %{
    prefix ""
    statics @{ %{ path "/assets" dir "./public" } }
    routes @{
        %{ path ""            method :GET handler home/home-page }
        %{ path "/docs/:slug" method :GET handler docs/docs-page }
        # ...
    }
} |> [http/router]

[http/serve 3002 $site $partials --watch --watch-dirs "packages/help/manual"]
[vwait] 
The cluster driver is a separate 2-line file:
 # www/cluster.dft
[http/serve-cluster 3000 "www/app.dft" %{ :watch-dirs @{"packages/help/manual" "www/pages"} }]
[vwait] 
## Quick Reference | Form | Purpose | |---|---| | `[http/serve-cluster $port $app-path ?n-workers ?opts]` | Start cluster (multi-worker, SO_REUSEPORT) | | `[http/cluster-metrics]` | Cluster info map | | `[http/cluster-status]` | Cluster health map | | `[http/cluster-reload]` | Trigger cluster reload | | `[http/cluster-stop]` | Stop cluster |