Accepted
ADR 0032 — Design-token overlay and render-time resolution
How an editor's overrides of a theme's design tokens are stored, resolved against the theme's defaults, and applied at render
A theme ships design-token defaults in tokens.json and a static stylesheet that consumes them as CSS custom properties. The customise screen lets an editor override those tokens — a brand colour, a type scale, a spacing rhythm. This record settles where those overrides are stored, how they resolve against the theme's defaults at render, and how they are applied without breaking the stylesheet's immutable cache.
Two override namespaces
Overrides are stored as typed settings in two key families, both versioned like every other setting:
- theme.tokens.core.{path} — applied regardless of which theme is active. Core overrides survive a theme switch; editors use them for cross-theme adjustments such as the brand colour.
- theme.tokens.{themeId}.{path} — scoped to one theme. Preserved across switches but only applied when their owning theme is active; editors use them for tokens that only make sense in that theme.
The {path} is the dot-joined token path under the tokens.json root, for example color.accent or text.h1.size. Each path segment is lowercase ASCII alphanumeric with no hyphens (a numeric segment such as space.0 is allowed). Forbidding hyphens keeps the path-to-name transform injective, so two distinct paths can never alias to the same CSS variable. The validator is structural, not enumerative: any structurally valid path is accepted, so a power user can override a path through the typed-settings API even before the customise UI exposes a control for it.
Resolution order
For each token path the resolved value is the first non-null in this chain:
- the theme-specific override for the active theme, then
- the core override, then
- the theme's own default from tokens.json.
The resolver reads tokens.json from R2 (cached per isolate, safe because the theme and version are immutable) and the two settings namespaces in a single D1 query, returning a flat map keyed by token path. It runs once per render, memoised per request.
Render-time emission
The render path emits a small inline style block in the head, before the theme's static stylesheet link, declaring the resolved tokens as CSS custom properties under a --cusp- prefix. The naming is mechanical: the dot-joined path has its dots turned into hyphens, so color.accent becomes --cusp-color-accent and text.h1.size becomes --cusp-text-h1-size. A theme's stylesheet that references var(--cusp-color-accent) automatically picks up overrides; the --cusp- prefix was checked against Tailwind, Bootstrap, shadcn, and MUI variable prefixes for collisions and is clear.
Writing or deleting a token override drains the prerendered HTML under the rendered/ prefix in R2, purges the theme surrogate key, and bumps the global cache epoch so other colocations miss prior-token Cache API entries too. The next request re-renders with the new resolved values. The stylesheet's independent, immutable cache key is untouched.
Open schema with well-known keys
The tokens schema is structural rather than exhaustive: it constrains the shape under each top-level group but does not require particular leaf keys. A theme that introduces a new leaf key is valid, the resolver passes it through, and the customise UI lists it as an additional token the theme defines without offering a control until a well-known-keys vocabulary covers it. This keeps community themes from forcing a schema release for a single new token.
Consequences
- Editor-first: an override is a single typed-settings write that takes effect on the next render via cache purge — no stylesheet regeneration, no workflow, no rebuild.
- The customise screen's Core panel is identical across themes; theme-specific tokens hide and re-show with theme switches; the storage layer preserves both kinds across switches.
- All overrides go through the versioned-write helper, so site-wide time travel reconstructs token state at any historical timestamp.
- Community themes opt in by shipping tokens.json and referencing var(--cusp-*) in their stylesheets — no Worker code change and no registry entry.
- Regenerating the stylesheet on every override was considered and rejected: it would break the immutable cache and run a workflow per slider drag. The inline token block is dramatically cheaper.
Read the theme packaging record