# ADR 0002: Deliver themes at run time rather than as a package - **Status:** accepted - **Date:** 2026-10-04 ## Context The conventional way to ship a design system to applications is a package: an npm package, a Go package, a Maven artifact. The consuming application installs it, imports from it, and rebuilds when it changes. That model is good, and it is the right answer for most design-system artefacts. The question here is whether it is the right answer for *design tokens specifically*. ## Decision Themes are served over HTTP at run time as CSS custom properties and JSON. The runtime SDKs are ES modules loaded by URL through an import map. Nothing is installed by the consumer. ## Rationale ### The cost of the package model, for tokens specifically Design tokens are unusual among design-system artefacts. They are: - **entirely data.** No behaviour to tree-shake, no components to code-split; - **consumed at a single moment.** At first paint, by a stylesheet link; - **changing on a different cadence from code.** A designer adjusts a colour; no application code needs to change. The package model imposes costs that fit code perfectly and fit data badly: | Cost | Fits code? | Fits tokens? | | --- | --- | --- | | Version resolution and peer conflicts | Yes | No. Nothing here has a peer. | | A rebuild of every consumer | Necessary | Pure overhead. The CSS output is byte-identical. | | Coordinated deploys on version skew | Necessary | Avoidable: a runtime fetch has no skew. | | A published version to track | Necessary | A database row, which an operator already tracks. | | Install friction | Usually fine | Often the blocker. A Go service with a templated frontend cannot `npm install` at all. | | Tree-shaking and chunk sizing | Necessary | Nothing to shake. | The specific motivating case: a service that serves server-rendered HTML cannot consume a design-system npm package without introducing a Node build into its release pipeline. That is a large change made to deliver a CSS file. Serving the CSS directly removes it. ### What runtime delivery buys **Republishing is a database write.** A designer adjusts a colour, clicks publish, and every application picks it up on its next request. There is no version to cut, no build to trigger, no consumer to upgrade. **No skew.** An application never holds a stale copy of a theme in its `node_modules`, because it never held one. Rolling back a theme is a database write too, and is atomic across every consumer. **No supply chain.** A token file is not code. It cannot execute anything, it has no transitive dependencies, and it cannot be compromised in the way an npm package can. This is a genuinely large reduction in attack surface. **Language neutrality.** Any environment that can make an HTTP request and read CSS can consume a theme: Go, Rust, PHP, Jinja, Django templates, a static site generator, a server-rendered email. ### What it costs Honesty requires stating the costs: **A network hop at first paint.** A stylesheet link is a blocking request. Mitigated by immutable-by-URL caching: a CDN serves it from an edge, a publish changes the version and therefore the URL, and a revalidation costs a few milliseconds and no body. This is the same cost model as a web font, which every application already pays. **A runtime dependency.** The service must be up for a consuming application to look right. Mitigated by a hard failure being a *fallback to the application's own styles*, not an error page: a failed theme load leaves the page unstyled rather than broken, which is exactly the right degradation. **CSS custom properties as the only interface.** A token cannot be compiled into a Tailwind class, a Sass mixin or a styled-component prop. Anything that needs compilation is not served by this system. That is the boundary of the approach, and it is a real boundary rather than a detail. **A version-pinning story is needed.** Runtime delivery means a consumer can be mid-request while a theme is republished. The answer is that a published theme's version never changes — a publish creates version N+1 — so a URL pinned to version N is immutable forever, and `/rt/v1/themes/{slug}` resolves to the latest. This is the same model as an immutable container tag. ## Alternatives considered **An npm package of design tokens.** Rejected for the reasons above. It remains the right answer for anything requiring compilation, and the two models compose: an application can build a Tailwind preset from a theme at its own build time, using `/rt/v1/theme.json`, and then use the runtime for the parts that must be live. **A CSS file on a CDN, uploaded by hand.** The delivery mechanism without the management. It removes the coupling but gives up atomic publish, rollback, history and per-theme authorisation. This is what the service automates. **Server-side rendering of inline styles.** Each application renders the variables into its own HTML. Zero latency, but every application must implement a fetcher, a cache, a fallback and a revalidation policy, and they will all get it subtly different. Centralising it means one correct implementation. **Web Components for the tokens.** Not applicable. Tokens are not components. **A GraphQL API.** Rejected: the payload is one object, so a single GET is simpler, cacheable at the HTTP layer, and readable with `curl`. GraphQL would add a schema, a server and a client library to deliver a JSON document. ## Consequences **Good** - Publishing is immediate, atomic and reversible. - No consumer rebuild, no version skew, no package to track. - Consumers in any language, including ones that cannot use npm at all. - A theme cannot execute code. - Caching is a solved problem rather than a design decision, because URLs are immutable. **Bad** - A network hop on the critical rendering path, mitigated by aggressive caching. - Only expressible as CSS custom properties. Anything needing compilation is out of scope. - The service is a runtime dependency for every consumer's first paint. - Debugging "why does this consumer look wrong" requires knowing which theme and version it resolved. Mitigated by `X-Theme-Version` and `X-Theme-Updated-At` response headers, and by the `sourceURL` comment in the generated CSS that makes browser devtools navigate back to the endpoint. **Neutral** - The admin UI is itself a consumer, which means the delivery path is exercised on every interaction. A change that breaks delivery breaks the tool that maintains themes, which is a useful continuous check. - The runtime SDKs are built and served rather than published, so they are versioned by the service's release rather than by a registry.