ADR 0017: UI Static Asset Serving Strategy¶
Status: Accepted Date: 2026-05-22 Deciders: Project founder
Context¶
Phase 5 serves the unmodified Apache Airflow 3.2.1 React SPA from the Leoflow
control plane (see docs/ui-compatibility.md). The compiled bundle (HTML, hashed
JS/CSS chunks, fonts, source maps โ roughly 5โ10 MB minified) must be delivered
to the browser, and the SPA's client-side router requires an index.html
fallback for unknown paths.
We need to decide how the leoflow-server binary obtains and serves these
assets.
Decision¶
The Leoflow server embeds the pinned Airflow 3.2.1 SPA bundle via go:embed
and serves it from the root path /.
- The bundle lives under
internal/ui/assets/, committed to the repository, with aVERSIONmarker file containing the exact upstream tag (3.2.1). - It is produced reproducibly by
make fetch-airflow-ui, which extracts thedistdirectory from theapache/airflow:3.2.1image. internal/ui/embed.goembeds the directory;internal/ui/handler.goserves files under their original paths with correct MIME types and cache headers (hashed assetsimmutable,index.htmlno-cache), falling back toindex.htmlfor non-file paths (SPA routing).- The server reserves
/api,/ui,/auth,/healthz,/readyz,/metrics,/docs, and/openapifor their existing handlers; everything else falls to the UI handler.
Rationale¶
- One static-asset pattern. Consistent with embedding the Scalar API docs (ADR 0013) โ the binary already embeds and serves static UI assets.
- Single-binary deployment. No reverse proxy (nginx) to operate; the control plane serves the API and the UI from one process.
- No version drift. The bundle is versioned alongside the binary; a given build always ships the UI it was tested against, pinned to 3.2.1.
- Acceptable size. A 5โ10 MB embedded bundle is fine for a server binary (the agent's tiny-binary constraint in ADR 0004 does not apply to the server).
Consequences¶
- The bundle is a committed binary blob; upgrades are a deliberate event
(re-run
make fetch-airflow-ui, bumpVERSION, retest), matching the pinned compatibility posture. - The build requires the assets directory to exist; a placeholder
index.htmlis committed so the build works beforemake fetch-airflow-uipopulates the real bundle. - Serving the UI and the API from one origin avoids CORS for UI calls.
Alternatives Rejected¶
- Reverse proxy (nginx) serving assets + proxying
/apiand/ui: operational complexity (a second component to deploy and configure) for marginal benefit overgo:embed. - Runtime download from a CDN/registry: adds a runtime network dependency and a failure mode at startup; breaks air-gapped and single-binary deployment.