Skip to main content

Brand kit

The brand kit holds what every document of your organisation shares: the company name, the legal footer, colours, fonts and logos. Templates read it instead of repeating it, so a new logo or a changed registered address reaches every document at once, published templates included, without saving a single new version.

Every organisation has exactly one brand kit, on every plan. It applies to all workspaces of the organisation. A new organisation's kit starts with the organisation's name as the company name and nothing else.

Set it up in the app​

Open Brand in the organisation navigation. The page has one card per part of the kit:

CardWhat you set
IdentityCompany name (up to 120 characters) and Legal footer (company, address, register number, VAT ID; up to 2,000 characters). Templates read them as brand.name and brand.legal_footer.
LogosThree variants: Primary (the full logo on a light background), Inverse (for dark backgrounds) and Mark (only the symbol, for headers and small spaces). PNG, JPEG, WebP or SVG up to 512 KB. A logo is saved as soon as you drop or upload it, and removed with its own button, without Save brand kit.
ColoursUp to 24 named tokens, each with a colour: primary, accent, text, muted… A token is 1 to 32 lowercase letters, digits and dashes and starts with a letter; a colour is #rgb, #rrggbb or #rrggbbaa. Each token becomes brand.colors.<token> and the CSS variable --brand-color-<token>.
TypographyHeading font and Body font (a family name, up to 100 characters) and Base size, a number with pt or px (10pt, 14px). The font lists offer your organisation fonts first, then the fonts every render has built in (Noto Sans, Noto Serif, Liberation Sans, Liberation Serif, DejaVu Sans, DejaVu Serif); you can also type any other family name.
PreviewA sample letterhead with the unsaved values, rendered the way a template reads the kit.
Defaults for new templatesPaper format (A3, A4, A5, A6, Letter, Legal or Tabloid), Orientation (portrait or landscape) and the four margins as CSS lengths in mm, cm, in, px or pt (20mm). Each can stay Not set.

Save brand kit stores everything except the logos, and the page warns you before you leave with unsaved changes.

Owners and admins edit the brand kit, logos included; editors and viewers see it read-only. Every change is recorded in the organisation's audit log.

Defaults for new templates

Paper and margins are only a starting point: a new PDF template created with New template in the app begins with them. They are never applied when a template renders. A template stores its complete settings, so existing templates keep their own paper and margins when you change the defaults, and so do templates made from an example, an import or the CLI. GET /brand returns them as page_defaults.

The brand variable​

Every template, in every language, sees the kit as brand:

{
"version": 7,
"name": "Fennlor Studio GmbH",
"colors": { "primary": "#0f766e", "accent": "#f59e0b", "text": "#111827" },
"fonts": { "heading": "Inter", "body": "Source Serif 4" },
"font_size": "10pt",
"logo": {
"primary": "https://cdn.formfeed.dev/a/brand/…/primary-3f9a1c2b7d4e.svg",
"inverse": null,
"mark": null
},
"legal_footer": "Fennlor Studio GmbH · Musterstraße 1 · 12345 Musterstadt · HRB 00000 · USt-IdNr. DE000000000"
}
FieldTypeMeaning
versionintegerGoes up by one with every change of the kit (logos included). A kit starts at 1; 0 means no kit at all, as in a local render without a pulled kit.
namestring or nullCompany name
colorsobjectOne entry per colour token, { "primary": "#0f766e" }. Always an object, empty when there are no tokens.
fonts.heading, fonts.bodystring or nullThe family names from Typography
font_sizestring or nullBase size, such as 10pt
logo.primary, logo.inverse, logo.markURL or nullPublic address of each logo on the CDN. A new file gets a new address, so no cache shows an old logo.
legal_footerstring or nullLegal footer

Values you have not set are null.

In Jinja2 and Liquid, brand is a global variable; in Handlebars it is added to the root of the data. The examples below read the same kit in each language:

<header style="border-bottom: 2px solid {{ brand.colors.primary }}">
{% if brand.logo.primary %}<img
src="{{ brand.logo.primary }}"
alt="{{ brand.name }}"
height="48"
/>{% endif %}
</header>
<h1 style="color: {{ brand.colors.primary | default('#111827') }}">
Invoice {{ invoice.number }}
</h1>

The legal footer belongs in the Footer tab, so Chromium prints it on every page:

<!-- Footer tab -->
<div style="width: 100%; font-size: 7pt; color: #666; text-align: center;">
{{ brand.legal_footer }} · Page <span class="pageNumber"></span> of
<span class="totalPages"></span>
</div>

({{brand.legal_footer}} in Handlebars.)

Your data wins. If the request data has a top-level field called brand, the template sees that field and not the kit. Templates that already read a brand from their data keep working; rename the field when you want the kit.

The editor offers brand.* in autocomplete, and brand is never reported as a variable missing from the sample data.

CSS variables​

Formfeed also injects the kit as CSS custom properties, after the organisation fonts and before the print reset, so your CSS can use them and still override them:

:root {
--brand-color-primary: #0f766e; /* one per colour token: --brand-color-<token> */
--brand-color-accent: #f59e0b;
--brand-font-heading: 'Inter';
--brand-font-body: 'Source Serif 4';
--brand-font-size: 10pt;
}

Only values you have set produce a variable, so give a fallback where one may be missing:

body {
font-family: var(--brand-font-body, 'Helvetica Neue', Arial, sans-serif);
font-size: var(--brand-font-size, 10pt);
}
h1,
h2 {
font-family: var(--brand-font-heading, inherit);
color: var(--brand-color-primary, #111827);
}
.total {
background: var(--brand-color-accent);
}

With Tailwind use arbitrary values: class="bg-[var(--brand-color-primary)] text-white". They work in the editor preview and in the render alike.

Fonts. An organisation font named in the kit is embedded in every render that uses the kit, even when the template only says font-family: var(--brand-font-heading). A Google font or any other web font still needs its <link> in the Head tab; a family name alone does not load it.

Headers and footers​

The CSS variables are also available in the header and footer templates, so inline styles such as style="color: var(--brand-color-primary)" work there.

Chromium does not load external images in headers and footers. Formfeed therefore embeds the brand logos for you: <img src="{{ brand.logo.mark }}"> in the Header tab prints the logo on every page. This applies to brand logo URLs only; any other image URL in a header stays as it is.

How changes reach your documents​

  • Live. The next render uses the current kit, for draft and published templates alike. There is no republishing.
  • Within a minute. API renders read the kit through a cache of up to 60 seconds, so a change made in the app reaches them within a minute. The editor loads the current kit when you open a template, for the preview and for True render.
  • Recorded. Every render records the brand kit version it used as brand_version in its reproducibility record, so you can tell later which kit a document was printed with. The render's details on the Renders page of the app show it under Reproducibility; the API's render object does not carry it (it has engine_version, template_checksum and output_sha256, see Reproducibility).
  • Deduplication knows. A render is only answered from the deduplication cache when the brand kit version (and the shared partials) are the same as before; after a change the next request renders again.
  • Never without it. If the kit cannot be read, the request fails with 500 worker_exhausted before a render is recorded, so it costs nothing; retry it. A document is never printed without its legal footer.

Ad-hoc html renders that name an engine get the kit too, and so do Word and PowerPoint templates (which get no shared partials); plain HTML and url renders do not. How renders use them sums this up for the API.

Deleting a logo takes it out of the next render: brand.logo.<variant> becomes null. Guard <img> tags with an if as above so no broken image appears.

Read it from code​

GET /brand returns the kit as templates see it, plus page_defaults and updated_at. The kit is edited in the app only; the API reads it.

Use it locally with the CLI​

Local previews and renders do not call the API for the kit. Download it once into the project:

npx formfeed brand pull
# brand v7 (Fennlor Studio GmbH) -> .formfeed/brand.json

The file holds what GET /brand returns. formfeed dev, formfeed render (from local files), formfeed test, formfeed ci preview, formfeed renders pull --run and the @formfeed/testing helpers render with .formfeed/brand.json as brand; a running formfeed dev reloads when you pull again. formfeed validate checks that the file is valid JSON. Without the file, templates render with an empty kit: version 0, no colours and every other value null. That differs from your organisation's kit, which starts with the company name, so pull before you compare local output with the API's.

Pull again after changing the kit in the app. Whether you commit .formfeed/brand.json is up to you; committing it keeps snapshot tests stable in CI.