Appearance
Font sources and licences
Library authors import font families into a library from font sources: Google Fonts, Fontsource, a GitHub repository, a font file or zip by URL, or a company's own font index. The family is declared in the library, so reports that import it can pick it in their font pickers, and it records where it came from, so a newer version can be pulled when someone asks. This page is for whoever manages the sources. Using fonts in libraries and reports is in designer/libraries.md.
Nothing is fetched until someone imports a family, checks for updates, or tests a source. There are no background checks.
Turning a source on
Sources are on the Font sources page in Admin (/admin/font-sources). It needs the fonts.sources permission: only the administrator role has it, a custom role can be given it, and an agent key never has it, whatever its roles, as with settings.manage. Library authors (library.write) see the sources that are on in the Add fonts… dialog; with none on, the library's Assets and fonts tab says that someone with fonts.sources can turn one on.
Two sources come with Reportforge, both off:
| Preset | What it is | Hosts it fetches from |
|---|---|---|
| Google Fonts | The google/fonts repository, which Google Fonts is built from, at main, in its own layout (a METADATA.pb per family folder under ofl/, apache/ and ufl/) | api.github.com, raw.githubusercontent.com |
| Fontsource | Fontsource's API and CDN | api.fontsource.org, cdn.jsdelivr.net |
A preset can be turned on or off and its cap changed, and Google Fonts' ref and token too, but a preset can't be removed. New presets come with releases.
Test fetches the source's catalogue now, on or off, and says how many families it lists. A url source has no catalogue: its Test says which hosts it allows.
Declaring a source
Add a source asks for a name and a kind, and the fields the kind needs:
| Kind | Fields | Hosts |
|---|---|---|
GitHub repository (github) | Owner, repository, ref (a branch, tag or commit; main by default), layout, an optional folder, an optional token | api.github.com, raw.githubusercontent.com |
Font file or zip by URL (url) | The hosts its links may be on: https://fonts.example.com, https://*.foundry.com | Those |
Fontsource (fontsource) | Nothing else | api.fontsource.org, cdn.jsdelivr.net |
Font index (index) | The index's URL, and its hosts (the index's own host when left empty) | Those |
The GitHub layouts:
- Folders: each folder that holds font files is a family. The family's name, and each file's weight and style, are read from the fonts themselves. Its documents are the files whose names start with
LICENSE,LICENCE,OFL,UFL,COPYING,README,DESCRIPTION,FONTLOG,AUTHORS,CONTRIBUTORSorTRADEMARKS, as text, Markdown or HTML. - Google's (metadata): each family folder has a
METADATA.pb, which names the family, its category, licence and files. Its documents areOFL.txt,LICENSE.txtorUFL.txt,DESCRIPTION.en_us.html,ARTICLE.en_us.htmlandFONTLOG.txtwhere present, and theMETADATA.pb.
A token is only ever a secret: reference, such as secret:env:GITHUB_TOKEN, which reportd resolves through the secret providers the config's secrets.providers turns on (env, file). It is never stored or shown as the token. Without one, GitHub allows 60 requests an hour. GitHub tree listings and Fontsource's list are kept for a day per source, so that is plenty for browsing, and a token is for private repositories or many imports.
A source on a company's network can allow internal addresses. Otherwise, as for remote assets and HTTP data sources, the fetcher refuses loopback, private and link-local addresses, after DNS.
The font index format
An index source serves one JSON file, on its hosts:
json
{
"version": "2026.10",
"families": [
{
"name": "Acme Sans",
"category": "sans-serif",
"licence": "OFL",
"licenceUrl": "https://openfontlicense.org",
"files": [
{ "url": "files/AcmeSans-Regular.ttf", "weight": 400, "style": "normal" },
{ "url": "files/AcmeSans-Bold.ttf", "weight": 700, "style": "normal" },
{ "url": "files/AcmeSans-Italic[wght].ttf", "weight": "400 700", "style": "italic" }
],
"documents": [
{ "url": "files/OFL.txt", "kind": "licence" },
{ "url": "files/README.md", "kind": "readme" }
]
}
]
}Links may be relative to the index, and must be on the source's hosts. A variable file's weight is its range, as a string. A document's kind is licence, readme or notes. Bump version when anything changes: "Check for updates" compares it.
What a fetch may do
Every fetch goes through one fetcher, per source:
- only to the source's hosts, over the scheme each host names (
httpsunless it sayshttp); - through the address guard, unless the source allows internal addresses;
- within a timeout, and no redirect off the source's hosts;
- within a cap per import, of the font files and documents together: 20 MB unless the source sets its own (at most 200 MB). Catalogue listings have their own cap of 16 MB. A family's preview in the dialog is one file of at most 4 MB.
Every file an import fetches is checked to be a font (TTF, OTF, WOFF or WOFF2), which internal/fontinfo reads: its family, weight, style, variation axes and licence. A WOFF is stored as the TTF or OTF it wraps, since the engine reads WOFF2 but not WOFF.
Static and variable files
An import takes a static file per style picked, one per weight and italic, where the source has one. A family that a source has only as a variable font comes as its variable file, one for the upright and one for the italic, declared with the weights picked. The dialog says which before Add.
Fontsource splits each family's files by script, such as latin and cyrillic. The engine looks a family's glyphs up in one file per weight and style, and doesn't fall back to a second file of the same family, so an import from Fontsource takes one subset: latin unless another is picked. The dialog says which.
Licences
google/fonts and Fontsource carry the SIL Open Font Licence, Apache 2.0 or the Ubuntu Font Licence, all of which allow embedding fonts in PDFs. The family's licence file goes into the library beside its fonts, and so into its releases and backups. From Fontsource, the documents are the LICENSE and README.md of the family's package on its CDN, which Fontsource's repository publishes.
For url and index sources, reportd reads the licence from the index's licence, else the font's name table (its licence description and URL), else a licence file in the zip. When none names a licence, the dialog warns before Add that it can't tell whether the font may be embedded. It warns too when a font's own embedding permissions (OS/2 fsType) forbid embedding. Whether a font may be used is the library author's call: the warning doesn't stop the import.
Documents are kept as they came, HTML included. The studio opens them as text, and reportd serves library files under a sandboxing Content-Security-Policy, so nothing in them runs.
Where it's kept
state.driver | Where |
|---|---|
file | dataDir/font-sources.json: the declared sources, and what was changed on a preset |
postgres | The schema's font_sources table (schema version 6), one row per source |
Backups carry them (backup.md), and reportd state import and state export move them between the two (state.md). The fonts themselves are library files, on the environment branches with the library.
Each change is audited: fonts.source.add <id>, fonts.source.change <id> and fonts.source.remove <id>. Imports are audited on the library: library.font.add <family>, library.font.update <family>, library.font.declare <family> (fonts uploaded by hand) and library.font.remove <family>.
The API
| Route | Needs |
|---|---|
GET /v1/fonts/sources | library.write or fonts.sources (only the latter sees a token's reference) |
POST /v1/fonts/sources, PUT and DELETE /v1/fonts/sources/{id} | fonts.sources |
POST /v1/fonts/sources/{id}/test | fonts.sources |
GET /v1/fonts/sources/{id}/families?q= (or ?url= for a url source), …/families/{family}, …/preview?family= | library.write, and the source on |
POST /v1/libraries/{name}/fonts, PUT and DELETE /v1/libraries/{name}/fonts/{family} | library.write |
GET /v1/libraries/{name}/fonts/updates, POST /v1/libraries/{name}/fonts/{family}/update | library.write |
They are in the API reference.