# ADR 0016: Self-hosted Google Fonts, per-pond font configuration - Status: accepted - Date: 2026-07-04 ## Context The vision: Pond Admins choose fonts for headings, body text, and monospace from a set of free Google Fonts; fonts must be served from the instance itself (never from Google's CDN) to avoid GDPR issues. Defaults: Roboto 400 (headings), Roboto 200 (body), Fira Code (monospace). ## Decision - **Curated font catalog**: the repo contains a maintained list (~15–25 families) of OFL/Apache-licensed families with the needed weights. A build step (`deploy/fonts/`) downloads the WOFF2 files **at image build time** from google-webfonts-helper/upstream sources and bakes them into the `web` image under `/fonts//`. No runtime download, no third-party requests from visitors' browsers — CSP allows `font-src 'self'` only. - **Per-pond configuration**: pond settings store three font slots (`heading`, `body`, `mono`), each referencing a catalog entry + weight. The app applies them as CSS custom properties (`--font-heading`, `--font-body`, `--font-mono`) on the pond's root element; `@font-face` rules for the catalog are generated once. - Defaults per vision: Roboto 400 / Roboto 200 / Fira Code. (Roboto 200 is provided via the variable font or the 200 static weight; fallback stack `system-ui` chain.) - Licensing: each catalog entry records its license (OFL/Apache); the catalog page in the app shows attribution. - Exports: the PDF renderer (ADR 0009) inlines the pond's fonts so PDFs match the on-screen look. ## Consequences - Adding a font is a catalog PR + image rebuild — no runtime font management surface (deliberately small attack/complexity surface). - Image size grows by a few MiB per family (WOFF2, subset to latin/latin-ext by default) — negligible. ## Decisions taken in #303 (2026-08-01) This ADR's "Consequences" said adding a font is a catalog PR plus an image rebuild, with **no runtime font management surface**. That is amended here, for the case this ADR already anticipated under "Alternatives considered": _arbitrary font upload … may become a Site-Admin-level feature later_. An operator running a private instance holds a licence for a typeface and wants to use it on screen and in exported PDFs. Baking it into a custom image works but ties every font change to a rebuild, and the file then lives in the image rather than in the backup. - **Site Admins only.** Not Pond Admins — which is what closes the licensing-risk objection above: the operator who holds the licence is the only one who can upload, and the licence is recorded with the font. Uploaded families are additive; they never replace or shadow a catalog family, and a name collision with one is rejected. - **Uploads are data, not code.** The api stores the submitted bytes and serves them back with a pinned content type. It validates the magic number (`wOF2`/`wOFF`) and a size cap, and it does **not** parse the font — family, category and licence come from the form. This is the answer to the file-format attack-surface objection: font parsers are a known memory-safety surface and we gain nothing from entering it. - **WOFF2 required, WOFF optional, OTF not accepted.** WOFF2 covers both consumers we have — the browser and Gotenberg's Chromium — and is the format the PDF path already inlines. Storing OTF would enlarge uploads and backups for no runtime benefit. - **The bytes live under `./data/`, not in `FONTS_DIR`.** `FONTS_DIR` is the catalog directory baked into the image: anything written there is lost on the next deploy and is never backed up. Custom fonts go to `CUSTOM_FONTS_DIR` (default `./data/fonts`), a sibling of the uploads and plugins directories, and are registered with the backup so a restore brings them back. - **The GDPR guarantee is untouched.** Custom fonts are served from the instance itself like the catalog ones; `font-src 'self' data:` stays as it is, and a visitor's browser still makes zero third-party requests. ## Alternatives considered - **Runtime font download by the server on admin selection**: flexible but adds an outbound dependency, cache invalidation, and licensing bookkeeping at runtime; rejected for v1. - **Arbitrary font upload by Pond Admins**: licensing risk and file-format attack surface; may become a Site-Admin-level feature later.