forma-server
Axum middleware for the Forma Stack — server-side rendering, content-hashed asset serving, and CSP headers. Renders pages from FMIR binary IR without a JavaScript runtime.
Install
[]
= "0.1"
This pulls in forma-ir automatically.
What It Does
render_page(config)— generates a full HTML page from an IR module + slot dataload_ir_modules(manifest)— loads.irfiles from embedded assets at startup (load_ir_modules_strictreturns the failures instead of swallowing them)serve_asset(filename)— serves assets with Brotli/gzip negotiation;Cache-Control: immutablefor content-hashed names, revalidation + ETag for the restgenerate_csp_nonce()— 256-bit cryptographically random nonce (ring CSPRNG), returned as a validatingCspNoncebuild_csp_header(nonce)— strict CSP: nonce +'strict-dynamic'scripts, nonce-locked style elements, nounsafe-eval
Quick Start
use ;
use Embed;
;
// At startup: load manifest and IR modules
let manifest = ;
let = ;
// Render a page. PageConfig has no Default impl — every field is explicit.
let page = render_page;
// The HTML and the CSP header are one unit — serve them together, no-store.
let headers = page.headers;
Two Rendering Modes
| Mode | What happens | When to use |
|---|---|---|
| Phase 1: Client Mount | Renders <div id="app"></div> — FormaJS mounts from scratch |
No IR available, or fallback |
| Phase 2: SSR Reconcile | Renders full HTML from IR walker — FormaJS hydrates | IR + slots available, fast first paint |
Security
- CSP headers: Strict policy with cryptographic nonces —
'strict-dynamic'scripts, nonce-locked style elements, nounsafe-eval. Style attributes are permitted viastyle-src-attr 'unsafe-inline', because compiledstyle:bindings emit them and a nonce cannot be attached to an attribute - Nonce generation: 256-bit entropy via ring CSPRNG, wrapped in a validating
CspNonce - Serve HTML and CSP together: the HTML embeds a per-request nonce, so it must be sent
Cache-Control: no-storeand never cached across requests —PageOutput::headers()does this, andPageOutput::nonceis exposed so you never have to parse the policy string - Asset serving:
rust-embedprevents path traversal, and requested names are checked for exact membership in the embedded list so debug and release builds behave identically;manifest.jsonand direct.br/.gzrequests are refused - Template escaping: Interpolated values (URLs, class names, CSS, title) are HTML-escaped. Two fields are documented raw sinks:
config_script(raw JS, terminator-neutralised but trusted) andbody_prefix(raw HTML). Useconfig_jsonfor user-derived data - No unsafe code:
#![forbid(unsafe_code)]on the crate root — compiler-enforced, not asserted
When You Need This
Use it when: Your backend is Rust + Axum and you want the full Forma SSR pipeline with asset serving and CSP.
You don't need it if: You're only using FormaJS client-side (no Rust server), or you're using a non-Axum framework (use forma-ir directly).
Part of the Forma Stack
Frontend (TypeScript)
| Package | Description |
|---|---|
| @getforma/core | Reactive DOM library — signals, h(), islands, SSR hydration |
| @getforma/compiler | Vite plugin — h() optimization, server transforms, FMIR emission |
| @getforma/build | Production pipeline — bundling, hashing, compression, manifest |
Backend (Rust)
| Package | Description |
|---|---|
| forma-ir | FMIR parser, walker, WASM exports |
| forma-server | This crate — Axum middleware: SSR, asset serving, CSP |
Full Framework
| Package | Description |
|---|---|
| @getforma/create-app | npx @getforma/create-app — scaffolds Rust server + TypeScript frontend |
License
MIT