Skip to content

Company branding

Each company in Trimus has a square avatar that appears in the sidebar rail and the preferences switcher. By default that avatar is a generated letter tile — the first character of the company name on a colour background derived from the name itself. You can replace it with an uploaded logo.

Where logos appear

When a logo is uploaded, it replaces the letter avatar in two places:

  • The company rail down the left edge of every page.
  • The company switcher on the Preferences page.

The Branding panel on the per-company Settings page also shows a 96×96 preview of the current logo (or the letter-avatar fallback if none is configured).

  1. Open the company you want to brand.
  2. Click Settings in the sidebar.
  3. In the Branding card at the top of the page, click Choose File under "Logo file", pick an image from your machine, and click Upload logo.

The page reloads with the new logo live across the UI.

On the same Branding card, click Remove logo. The button only appears if a logo is currently configured. After removal the company reverts to its letter avatar.

Supported formats and limits

The upload endpoint accepts the four image formats with broad browser support:

  • image/png
  • image/jpeg
  • image/gif
  • image/webp

Any other content type returns 415 Unsupported Media Type. SVG is deliberately excluded because an SVG can carry embedded <script> that would run in the browser when the image is rendered.

Other guardrails:

Constraint Limit Failure response
File size 1,000,000 bytes (~1 MB) 413 Request Entity Too Large
Image dimensions 2000 × 2000 max 415 Unsupported Media Type
File content matches MIME required (verified by header decode) 415 Unsupported Media Type

The file-size cap is also enforced client-side via the file input's accept attribute, but the server check is authoritative — a crafted multipart submission that bypasses the browser still fails with 413.

How the bytes are stored

Uploaded images are persisted under the Trimus storage directory:

<storage_dir>/<company-id>/logos/<uuid>

Each upload also writes an assets row (filename, content type, size, storage key, uploading user) and upserts a company_logos row pointing at the new asset.

Replacing an existing logo prunes the previous file from disk and the previous assets row from the database as a best-effort post-step. If that prune fails, the new logo is still live and the orphan will be picked up by the storage GC; an slog.Warn line records the failure so accumulating orphans can be spotted.

Image-serving endpoint

The image is served by GET /api/companies/{companyId}/logo/image. The response is the raw bytes with:

  • Content-Type set from the asset's stored MIME.
  • Cache-Control: public, max-age=3600 so browsers cache the avatar for an hour without a roundtrip on every page navigation.
  • 404 Not Found when the company has no company_logos row, the asset_id is NULL, the asset row was deleted, or the bytes are missing from disk.

Access is gated by the standard RequireCompanyAccess middleware — any actor with read access to the company can fetch the logo bytes; no extra checks are layered on top.

JSON metadata endpoint

GET /api/companies/{companyId}/logo returns the company_logos row itself (asset ID, theme, updated-at) as JSON, without the image bytes. Useful for scripted callers that just need to know whether a logo is configured.

Theme

The upload form has a Light / Dark theme dropdown that maps to the theme column on company_logos. It is currently unused by the serving path — both light and dark sites of the UI render the same image. The column is reserved for a future theme-aware variant that serves a different file when the user is in dark mode.