DocumentationBrand profiles

Reporting

Brand profiles

White-labelling for reports: a logo, two colours, a client name, and the two lines of text that appear on the cover and in the footer.


What a profile holds#

Field Where it appears
name Nowhere on the report. It is how you find the profile in the list.
company_name "Prepared for {name}" on the cover, and in the document title.
primary_color The rule under the cover title and the accent bar beside each figure.
accent_color Reserved. Stored and round-tripped; not yet drawn.
cover_text A paragraph under the cover, before the methodology note.
footer_text The footer of every page.
hide_powered_by Removes "Generated by Uptime Cairn" from the footer.
is_default The profile a template with no explicit choice falls back to.

cover_text and footer_text are plain text, and nothing downstream interprets markup in them. That is a rendering constraint rather than a storage one: the PDF writer has no rich-text pipeline, and a field that renders in HTML and not in PDF is worse than one that renders nowhere.


Colours are stored exactly as you write them#

#0B5FFF comes back as #0B5FFF, not #0b5fff. A brand colour is a string somebody pasted out of a brand guide, and handing it back in a different case is what makes a white-label feature feel like somebody else's product.

Only #rrggbb is accepted. Three-digit shorthand and named colours are refused rather than guessed at.

Where the colour actually appears#

Two places, and deliberately only two: the rule under the cover title, and the thin accent bar beside each figure block. Both are decorative and neither carries meaning, so a hard-to-read choice costs nothing but taste.

The charts keep their own colours. Green, red and grey on the uptime strip are the legend — the caption under it says "Green: no downtime observed" — so a brand that recoloured the bars would produce a figure that contradicts its own caption. That is a correctness rule rather than a matter of restraint.


Logos must be raster#

PNG or JPEG. SVG is refused at upload, with the reason and the fix:

export the logo as PNG — at about 480 pixels wide it will be sharp on both the page and the print

The refusal is inconvenient and it is the right way round. The PDF writer embeds image data directly; there is no SVG rasteriser in the binary and adding one would be a rendering engine. A logo accepted at upload and dropped silently at render time is discovered by a client receiving an unbranded document.

The format is decided from the bytes, not from the declared Content-Type. That is the half that makes the refusal reliable: an SVG labelled image/png is exactly how one reaches the renderer, and browsers and curl both mislabel often enough that trusting the header would fail in the case this exists for.

Other limits:

  • 1 MB. Over it is a 413. A logo is drawn into a 240 × 56 box; a three-megabyte PNG is a scan of a letterhead.
  • Nothing is stored on a refusal, so a profile is never left claiming a logo it has no bytes for.
PUT /api/v1/brand-profiles/{id}/logo
Content-Type: image/png
<the bytes>

logo_url on the profile is always null. The field is defined in the API and no operation serves the bytes back, so a URL there would name an endpoint that answers 405. The rendered HTML and PDF embed the logo directly, which is what actually makes those files standalone.


The default, and what an unconfigured install looks like#

At most one profile is the default, enforced by the database rather than by convention. Marking a second one default demotes the first in the same transaction — "there is already a default" is not something you can act on when making this one the default is exactly what you asked for.

An install with no profile at all is not unbranded. The report falls back to the instance name from settings.general and the primary colour from settings.appearance, so the first report a new install produces looks configured rather than anonymous.

Two fields and no more. There is no logo, footer or client name in that settings section, and inventing a footer would put words on a client's document that nobody wrote.

A profile that exists but leaves primary_color empty still gets the instance's colour. Creating a profile to set a client name should not silently undo the appearance you already chose.


Deleting one#

A profile a live template names cannot be deleted. The request is a 409 that counts what is in the way:

3 report templates still use this brand profile

The foreign key would allow the delete and let those templates fall back to the default. The refusal is better: the fallback is invisible until an agency's client receives an unbranded document, whereas the refusal happens while somebody is looking at the screen.

A soft-deleted template does not hold a profile hostage. It renders nothing, so it has no claim on branding.


Branding is copied onto each report when it runs#

This is the part worth understanding before you rebrand.

Every generated document carries the profile as it stood at the moment that run executed — the client name, the colour, the footer. Rebrand in June and every January report you ever sent still says what it said when it was sent.

That is what an artifact is for. A report referencing a live profile would let "who was this prepared for?" change under a client's feet, and the question gets asked precisely because somebody tidied the profile up.


Reports only#

Status pages keep their own inline branding, and the resulting three-way duplication — brand profile, status page, settings.appearance — is a stated cost rather than an oversight.

Making profiles replace status-page branding would have turned StatusPage.theme, logo_url, primary_color and footer_text into a brand_profile_id: a breaking change to a shipped schema. A status page and a report for the same client are therefore configured separately and can drift. Unifying them is a later change, and profiles are shaped so status pages can adopt them without reshaping anything.

Edit this page on GitHubdocs/guides/brand-profiles.md · synced from b5f5d14