Frontend Delivery And Verification
Architecture, threat boundaries, and the obfuscation/CSP/SRI decisions are in ADR 0004.
Security Invariants
- Browser JavaScript, CSS, source locations, API requests, and rendered values are observable even when minified.
- Never put credentials, private keys, provider tokens, tenant secrets, or privileged-only data in a bundle, manifest, metadata file, HTML bootstrap, or public source map.
- Authorization, tenant isolation, validation, prices, permissions, and every consequential business rule are enforced server-side. Client checks are UX only.
- Minification is a delivery optimization. Obfuscation is disabled and is not a security control.
Profiles
| Profile | Command | Contract |
|---|---|---|
| Development | npm run dev |
Explicit development mode, readable source modules, HMR, browser debugging; not deployable output. |
| Production | npm run build |
Tree-shaken/minified JS and CSS, content hashes, Vite manifest, no public maps, reproducibility metadata, raw/gzip budgets. |
scripts/frontend-build-policy.mjs owns the shared production shape.
config/quality/frontend-asset-budgets.json owns per-theme entry/chunk/CSS
limits and is the canonical inventory of supported themes. Every theme build
runs scripts/check-theme-asset-budget.mjs, which also rejects unhashed
JS/CSS, public .map files, stale/mismatched lockfile metadata, and Default
features accidentally pulled into the eager app graph.
That per-theme check runs inside one theme directory and therefore cannot see
the published tree as a whole. npm run verify:themes
(scripts/check-theme-build-inventory.mjs) owns the inventory question for
public/build: every supported theme is present with .vite/manifest.json and
build-metadata.json, no directory belongs to a retired theme, no unhashed
js/app.js or css/app.css survived a non-empty output directory, no .map
file is published, and each metadata file names its own theme. It is the last
step of npm run deploy:themes and is the check to run before switching
release traffic.
The root aggregates (purge:themes, update:themes, build:themes,
deploy:themes) enumerate every supported theme; test:themes enumerates
exactly the themes that declare a Vitest suite — lts1 deliberately has none,
so it carries no test:lts1 script and its deploy chain stops at the build.
tests/Unit/Theme/FrontendBuildPolicyContractTest.php derives all of that from
the canonical inventory, so adding a theme to the budgets file fails until its
scripts exist.
build-metadata.json is deterministic: it intentionally excludes timestamps,
hostnames, usernames, absolute paths, environment variables, and Git working
tree data. It is provenance, not a secret container.
Cache And Deployment Contract
- Serve filenames containing a content hash with a long-lived immutable cache policy.
- Revalidate HTML and
.vite/manifest.json; do not cache them as immutable. - Upload the complete new
public/build/{theme}tree before switching release traffic. Retain the previous tree while existing HTML can still reference it. entryCss()andentryJs()resolve the manifest and emit hash URLs without mutable?v=values. Their legacy fallback keeps old builds loadable only during the operator-controlled transition.- Never publish source maps. If private symbolication is later approved, upload maps directly to the approved monitoring service during the build and remove them before the public artifact is assembled.
Recorded Policy Evidence
Measured on 2026-09-06 against the owner-compiled public/build tree. Every
supported theme passes scripts/check-theme-asset-budget.mjs; the figures are
the entry chunk and the entry's CSS, raw and gzip, against their budgets.
| Theme | Entry raw / budget | Entry gzip / budget | CSS raw / budget | CSS gzip / budget |
|---|---|---|---|---|
| default | 91,270 / 360,000 | 29,986 / 120,000 | 171,665 / 230,000 | 41,554 / 60,000 |
| sober | 76,762 / 90,000 | 25,922 / 32,000 | 78,221 / 90,000 | 14,064 / 20,000 |
| cc1 | 289,705 / 310,000 | 96,290 / 105,000 | 123,762 / 135,000 | 21,549 / 30,000 |
| dm1 | 417,293 / 450,000 | 136,693 / 150,000 | 531,353 / 550,000 | 110,235 / 125,000 |
| lts1 | 273,622 / 310,000 | 90,703 / 105,000 | 81,092 / 95,000 | 15,903 / 22,000 |
| pc1 | 274,633 / 310,000 | 90,585 / 105,000 | 118,355 / 135,000 | 20,018 / 30,000 |
| pc2 | 277,114 / 310,000 | 91,577 / 105,000 | 74,514 / 75,000 | 13,542 / 18,000 |
| pc3 | 314,007 / 340,000 | 105,803 / 115,000 | 105,735 / 115,000 | 29,891 / 33,000 |
PC3's budgets were re-baselined on the same date. They had been provisioned
while the theme was still a scaffold, and the completed public shell (nested
module navigation, native locale disclosures, progressive mobile menu) exceeded
the entry gzip limit by 803 bytes and the CSS gzip limit by 3,891 bytes. The
new ceilings keep roughly 8% headroom over the delivered artifact, matching the
convention used by the other themes. Two payload reductions are known and
deliberately not taken yet, because both need a rebuild cycle the owner
schedules: animejs is imported eagerly although initReveal() already exits
under prefers-reduced-motion, and the @fontsource imports pull 33
@font-face blocks for subsets the theme does not use.
PC2's CSS raw figure sits at 99% of its budget. It is inside policy, but the next PC2 style change is expected to need a re-baseline or a trim.
Determinism, verified 2026-09-08
The owner rebuilt all eight themes on 2026-09-08. Every figure above is
unchanged, and the content hashes are identical to the 2026-09-06 output —
pc3 still emits js/app-B7HphJv8.js and css/app-CarRSKoV.css — so two
independent builds of unchanged sources produced byte-identical artifacts. That
is checklist item 4 below satisfied with evidence rather than assertion, and it
is what makes the immutable cache policy safe: a rebuild does not invalidate a
client's cached asset unless the source actually changed.
Each build-metadata.json still matches its theme's package-lock.json
digest, declares source_maps: false and obfuscation: none, and
the inventory audit at that date reported only two retired-theme directories.
They were local ignored output, not supported themes.
Local inventory and browser recheck — 2026-09-14
npm run verify:themes now passes for all nine supported output trees,
including dm2. The retired public/build/fhg1 and public/build/solar
directories are already absent; no deletion or theme compilation was needed.
The audit confirms manifests, metadata, hashed paths and no published source
maps for this local tree. An isolated SQLite-backed Default home page returned
HTTP 200; its JS, CSS and the loaded search chunk also returned HTTP 200 during
a headless Chromium desktop capture. This local check does not establish CDN
cache headers, deliberate-error symbolication, visual acceptance across all
themes, or production server behavior. On 2026-09-14 the project owner confirmed
completion of the remaining manual browser/cache/symbolication checks and the
Default/Sober and public-theme visual review. This is owner acceptance; no raw
browser trace or measurement was supplied with that confirmation.
The kiosk surface added no CSS: template/default/tpl/admin/kiosk/** styles
itself with inline --k-* tokens and classes the bundles already carried, so
default and sober came back byte-identical despite the new templates. A
surface that needs no new utilities is also a surface that cannot look broken
between a source change and the next operator-owned rebuild.
Operator Acceptance Checklist
Agents do not compile theme assets. From a clean checkout with the locked Node
dependencies installed, the project owner may compile all nine supported
themes from the repository root with npm run build:themes. The equivalent
per-theme build and test sequence is:
npm --prefix template/default run build && npm --prefix template/default run test
npm --prefix template/sober run build && npm --prefix template/sober run test
npm --prefix template/cc1 run build && npm --prefix template/cc1 run test
npm --prefix template/dm1 run build && npm --prefix template/dm1 run test
npm --prefix template/dm2 run build && npm --prefix template/dm2 run test
npm --prefix template/lts1 run build
npm --prefix template/pc1 run build && npm --prefix template/pc1 run test
npm --prefix template/pc2 run build && npm --prefix template/pc2 run test
npm --prefix template/pc3 run build && npm --prefix template/pc3 run test
Then verify:
- Every build policy check passes and each output has
.vite/manifest.jsonplusbuild-metadata.json. - Manifest JS/CSS filenames include hashes;
public/build/**contains no.mapfiles, no stale unhashedapp.js/app.cssafter the clean build, and no directory belonging to a retired theme. Steps 1 and 2 are scripted: runnpm run verify:themesfrom the repository root and treat a non-zero exit as a release blocker. - A clean browser load returns 200 for every manifest-referenced asset and no dynamic import fails.
- A second load reuses immutable hashed assets; changing an entry source changes its filename while an unchanged clean build keeps its digest. The second half was verified on 2026-09-08 — see "Determinism" above.
- Default and Sober admin sidebar, mobile navigation, active/ancestor state,
tooltips, settings pages, and lazy editor/map/consent features remain usable.
The scripted part of that check is
tools/benchmarks/admin-sidebar-latency.sh; its metric contract and the accepted before/after evidence are in Admin sidebar startup performance. - Each public theme is reviewed at desktop/mobile widths, light/dark mode, keyboard navigation, reduced motion, login, home, and one representative module page.
- A deliberate browser error remains diagnosable by release metadata and minified file/line. No workflow assumes a public source map.
Record the command results and any budget adjustment with the change review. Budgets may move only with a measured bundle-composition explanation.