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. Part of
it is assetsInlineLimit, a rule that keeps font files out of the stylesheet:
Vite inlines assets under 4 KB as data URIs, so without it the smallest font
subsets end up in render-blocking CSS. Every theme's vite.config.js passes
production.assetsInlineLimit to build.assetsInlineLimit; other small assets
keep Vite's default.
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.
Standalone stylesheet manifest entries (file ending in .css) participate in
the same deduplicated hash/raw/gzip checks as JS-associated css arrays; no
budget exemption is created. node --test scripts/check-theme-asset-budget.test.mjs
qualifies bounded synthetic files/manifests without Vite or repository builds.
CC1/PC1 deliver pinned local presentation fonts through vendor-fonts.css, a
CSS-only entry resolved with manifestAsset() before native styles. This keeps
font CSS separate from the app's per-file ceiling. Vendor licenses ship through
existing theme media copying. Owner builds must qualify actual sizes, hashed font
URLs, manifest/metadata and glyph rendering. Rebuild/upload complete theme trees
before publishing the changed heads: old manifests lack this entry and its
fallback URL is unbuilt. No external fallback masks missing delivery.
Sober reuses Default's canonical backoffice Storage runtime with a narrowly
scoped dynamic_css_budgets allowance: only the manifest source
../../default/asset/js/admin/storage.js may emit up to 230,000 raw / 60,000
gzip bytes of CSS, matching Default's limits. The checker requires that source
to be a direct dynamic import and refuses CSS shared with the eager shell.
Other Sober CSS retains the 100,000 raw / 20,000 gzip limits. Storage and Blog
controllers are required dynamic sources and forbidden in Sober's eager graph.
Sober's menu, module and Klaro settings controllers have the same required-
dynamic/forbidden-eager boundary: only matching inherited admin x-data roots
request them, and registration settles before the single Alpine startup.
Failed panels expose localized native reload recovery without blocking the shell
or other page capabilities. Entry ceilings remain 90,000 raw / 32,000 gzip
bytes. The owner-run artifact observed on 2026-10-13 measures 85,135 raw /
28,935 gzip bytes and passes the official checker (26 JS, five CSS assets).
The agent ran no theme build; deployed browser acceptance is separate. GrapesJS and
elFinder remain monolithic lazy vendor modules; their 500 kB advisory is retained
alongside the unchanged 1,250,000 raw / 400,000 gzip chunk ceilings.
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.
The metadata generator uses a final (order: 'post') Rollup generateBundle
hook, after Vite's CSS, preload and manifest transformations. It records the
final bytes and manifest, omits removed CSS-only JavaScript chunks and excludes
its own file. node --test scripts/frontend-build-policy.test.mjs checks that
contract using temporary synthetic sources and in-memory Vite output; it needs
the Default theme's installed development dependencies but does not build a
theme or write public assets.
The initial 2026-09-28 audit found that Default/Sober/LTS1 metadata had captured intermediate bytes and removed chunks. The owner subsequently rebuilt those themes with the corrected generator. A fresh audit verifies every recorded file size and SHA-256 digest (140 Default, 8 Sober and 42 LTS1 outputs), including the manifests, against the emitted files; all three lockfile digests also match. Installed PhotoSwipe assets remain present. These checks establish output and lockfile integrity; the metadata does not record a source-tree digest. The generator fix does not retroactively change deployed files.
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
Sober budget correction — 2026-09-27
The owner-built Sober CSS is 93,136 bytes raw / 16,508 gzip (Node gzip level 9),
exceeding its former 90,000-byte raw ceiling by 3,136 bytes. Since the September
16 source baseline (8de0e463), the foundation gained 20,048 source bytes for
shared admin controls, forms, data workspaces and a small topbar correction.
The emitted component layer occupies 12,829 bytes; the growth supports the
delivered backoffice contract. The raw CSS ceiling is now 100,000 bytes,
leaving approximately 7.4% headroom over the measured artifact. The gzip
ceiling remains 20,000 bytes; no styles or template scan roots were removed.
The main app remains 79,612 bytes raw / 26,739 gzip. The 1,115,620-byte GrapesJS core (300,764 gzip) is a dynamic dependency of the separate page-builder entry; the app manifest has no eager imports. It exceeds Vite's generic 500 kB advisory threshold but fits the existing 1,250,000 raw / 400,000 gzip chunk budget. The warning is retained; another manual split is not needed to fix this CSS budget failure. Measurements use the owner's existing build; no theme compilation was performed for this correction.
Verification: the existing Sober artifact passes check-theme-asset-budget.mjs
with Node 22.22.3 (five JS and two CSS assets), and the isolated
FrontendBuildPolicyContractTest passes all 13 tests / 298 assertions.
Build and browser checks
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.