# portal A content-driven question engine: one Rust binary that turns a Git repo of YAML into a live site — pages, forms, review desks, and state machines, with a durable event-sourced record of every answer underneath. The engine special-cases nothing: everything a site serves is declared in its content repo, and the same build serves any number of sites. Point an instance at a content repo and that repo *is* the site: pages, copy, state graphs, and branding (`site.yaml`: title, wordmark, and which hero the landing page opens on — the YES canvas, a gesture-drawing canvas wired to a relay, or plain copy). Content pushes hot-reload every running instance; instances differ only by env vars. [uhhm.no](https://uhhm.no) ([uhhm/questions](https://project.uhhm.no/uhhm/questions)) and [redoal.com](https://redoal.com) ([redoal/questions](https://project.uhhm.no/redoal/questions)) are two faces of the same binary, deployed from the same release artifact. It composes with the surrounding stack rather than bundling it: Gitea hosts and serves the content, NATS JetStream stores events and projections, Kanidm provides identity, and anything downstream (automations, newsletters, onboarding) subscribes to the answer stream. ## What the engine provides - **Content-driven pages** (`src/content.rs`, `src/app.rs`): YAML loaded from Gitea at boot and hot-swapped on a NATS reload signal; a bad push keeps the last-good content serving. A page's `id` is its URL; `qualifies` gates it to a Kanidm group. - **State machines as content** (`src/aggregates/`): `aggregates.yaml` declares each bucket's states and legal transitions; the engine replays a record's event history and refuses undeclared moves, with optimistic concurrency (CAS on the event log's sequence) against racing decisions. An empty history reseeds from the KV projection, so wiping the event stream strands nothing. - **Durable events + projections** (`src/events/`, `src/answers.rs`): every submission and decision appends to a JetStream event log and projects into a NATS KV bucket pages read back; every one also publishes on `portal.answers.submitted` for automations (n8n) to react to. - **Answer chains** (`src/chain.rs`): each submission hashes its parent(s), so a visitor's path through the questions is a verifiable lineage; `?chain=` links carry it, and self-service transitions (unsubscribe) authorize by holding one. - **Live resources** (`src/resource.rs`): content can pull a KV bucket, Gitea starred/org repos/releases, or any public JSON URL (SSRF fail-closed), reshaped by a content-declared `jq` filter. - **Input kinds as content**: a requirement's `type:` picks the widget — plain fields, a rich-text editor, or a `gesture` drawing canvas that can connect to a redoal relay so similar strokes echo between visitors live. Submitted values are arbitrary JSON; the engine records what the widget produced. - **Review desks**: any Kv resource with `transitions` renders rows with per-state action buttons and one shared confirm per alternative — owners walk records through their graphs without bespoke UI per bucket. ## Binaries - `portal` — the server (Leptos SSR + hydrate, Axum underneath). - `question_lint` — headless content validation, shipped inside every release artifact so each content repo's CI lints with the exact portal version its instance runs; also works offline: `question_lint --path