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).
Uploading a logo¶
- Open the company you want to brand.
- Click Settings in the sidebar.
- 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.
Removing a logo¶
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/pngimage/jpegimage/gifimage/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:
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-Typeset from the asset's stored MIME.Cache-Control: public, max-age=3600so browsers cache the avatar for an hour without a roundtrip on every page navigation.404 Not Foundwhen the company has nocompany_logosrow, theasset_idis 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.