ADR-0011: Server-Side Rendering with HTMX¶
Status: Accepted (decision); partially superseded by ADR-0023 on the template-engine choice Date: 2026-01-01
Status note (2026-05-25): The core decision (server-side rendering + HTMX, no SPA framework) is unchanged and load-bearing. The specific template engine — Jinja2 in the original ADR — was retired in #1042 (v0.67.92) and replaced by the typed Fragment substrate documented in ADR-0023. Surfaces declare
render: fragmentin DSL; the runtime emits HTML from a frozen-dataclass primitive tree. Read the rest of this ADR as the rationale for SSR + HTMX; read ADR-0023 for the rationale on how SSR is implemented today.htmx version note (2026-06-17): Migrated htmx 2.0.9 → htmx 4.0.0-beta4 (branch
htmx4-eval, evaluation indocs/evaluation/htmx4-evaluation.md+htmx4-browser-baseline.md). SSR+HTMX decision unchanged; this is a runtime-library upgrade. Key shifts absorbed: colon-namespaced events (htmx:before:requestetc.); fetch replaces XHR (detail.ctx.response.status/.headers.get(), CSRF viadetail.ctx.request.headers); native morph (hx-swap="innerMorph", replaces the idiomorph extension); explicit attribute inheritance (bridged withhtmx.config.implicitInheritance=truepending a markup audit); all htmx-2 extension files dropped (native morph/hx-status/hx-sse+ server-sideHX-Retargetcover their roles; forms send url-encoded which the server parser already accepts, so json-enc is unnecessary). Two htmx-4 behavioural traps, both the same root cause — implicit default triggers now re-fire on swap/reprocess (filter-select self-swap loop; body-drill<tr>auto-navigation). Fix pattern: make triggers explicit (hx-trigger="change changed"/hx-trigger="click"). Verified regression-free viadazzle ux verify --interactionson the 3 workspace apps.
Context¶
Dazzle generates live applications directly from DSL specifications. The UI layer must be produced programmatically from DSL constructs (surfaces, fields, modes) without requiring a separate frontend build pipeline or framework expertise.
Early prototypes used a React SPA approach, which introduced:
- Build complexity — Node.js, bundlers, and package management alongside Python
- Two-repo tension — Frontend and backend as separate concerns with separate deployment cycles
- DSL impedance mismatch — DSL-generated UI required serialising full app state to JSON for the client
- Security surface — Client-side routing and token handling added XSS and CSRF exposure
Decision¶
Adopt FastAPI for the API layer, Jinja2 for server-side HTML rendering, and HTMX for interactivity. No JavaScript SPA framework in the main application.
Why SSR with Jinja2?¶
| Criterion | SSR + Jinja2 | React SPA | Vue SPA |
|---|---|---|---|
| DSL → UI generation | Direct template render | JSON serialisation step | JSON serialisation step |
| Build step required | No | Yes | Yes |
| Python-native | Yes | No | No |
| Security defaults | Strong (CSRF built-in) | Manual | Manual |
| Time to first byte | Fast | Slow (JS parse + hydrate) | Slow |
| LLM-generated code quality | High | Variable | Variable |
Why HTMX?¶
HTMX replaces the interactivity layer without introducing a JavaScript framework:
- Partial page updates via
hx-get,hx-post— server returns HTML fragments - No client state — all state lives on the server, consistent with DSL-first design
- Progressive enhancement — pages work without JS, enhanced with it
- Minimal JS — no bundler, no
node_modules, no transpilation
Architecture¶
HTMX triggers return rendered HTML partials from the same Jinja2 layer. No JSON API needed for UI interactions.
Consequences¶
Positive¶
- No frontend build step —
dazzle servestarts immediately - DSL constructs map directly to template variables and partials
- CSRF and XSS protections apply uniformly at the FastAPI layer
- Single language (Python) for all application logic
- LLM-generated templates are straightforward Jinja2, not JSX or component trees
Negative¶
- Rich client interactions (drag-and-drop, offline) require custom JS beyond HTMX
- Team members with SPA backgrounds face a learning curve
- Browser history and deep-linking require explicit HTMX push-url configuration
Neutral¶
- Static assets (CSS, icons) served from
dazzle_page/static/without a bundler - JavaScript islands remain available for complex widgets via
<script>tags
Alternatives Considered¶
1. React SPA¶
Separate React frontend consuming a JSON API from FastAPI.
Rejected: Adds Node.js build toolchain, complicates DSL → UI code generation, increases security surface.
2. Vue SPA¶
Same architecture as React with Vue instead.
Rejected: Same drawbacks as React. No meaningful advantage for a DSL-generated UI.
3. Separate Frontend and Backend Repositories¶
Independent deployment of UI and API with a CDN in front.
Rejected: Operational overhead without benefit at current scale. DSL spec ties UI and API together by design.
Implementation¶
Templates live in src/dazzle/page/templates/. HTMX is loaded from CDN or vendored into src/dazzle/page/static/. FastAPI route handlers in src/dazzle/http/ return TemplateResponse objects backed by Jinja2.