chore: adding docs
This commit is contained in:
parent
23816d00f7
commit
81c5112409
39 changed files with 7257 additions and 2 deletions
148
data/docs/go-bananas-0002-runtime-delivery.md
Normal file
148
data/docs/go-bananas-0002-runtime-delivery.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<!-- go-doc:generated:begin source=go-bananas:docs/adr/0002-runtime-delivery.md signature=7d8b696ba971c1cbd2e19854d26de304 generated=2026-10-05 18:14 UTC -->
|
||||
Imported from docs/adr/0002-runtime-delivery.md in the go-bananas repository.
|
||||
|
||||
---
|
||||
|
||||
# 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.
|
||||
<!-- go-doc:generated:end -->
|
||||
Loading…
Add table
Add a link
Reference in a new issue