Skip to content

Phase 19 — Firm logo upload

Last updated: 2026-05-08

Partners shouldn't need their own CDN to white-label deliverables. Phase 19 lets a firm admin upload a logo file directly from the Firms tab. The image persists as a base64 data URL on the firm record — no external hosting, no separate static-file route, no auth-on-img concerns.

Endpoints

POST   /auditforge/firm/{firm_id}/logo   # multipart/form-data
DELETE /auditforge/firm/{firm_id}/logo   # clear back to ""

POST request: multipart/form-data with a single file field. Allowed content types: image/png, image/jpeg, image/svg+xml, image/webp. Max size: 200 KB — sized for cover-page logos, well under reasonable limits to keep engagements.json lean.

POST response:

{
  "ok": true,
  "firm_id": "firm-abc123",
  "logo_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAE…",
  "size_bytes": 12480,
  "content_type": "image/png"
}

The firm's logo_url is overwritten in place. Engagement deliverables for this firm now render with the new logo, including DOCX export (the data URL embeds without a fetch).

DELETE clears logo_url back to empty; deliverables fall back to the colored-square initials.

Why data URLs instead of S3

Three reasons: 1. No browser auth on <img src=...>. A bare HTTPS URL pointing at a private S3 bucket would 403 in the browser unless we built a presigned-URL flow. Data URLs sidestep that entirely. 2. DOCX-friendly. The python-docx rendering stack reads data URLs directly. A separate URL would need a synchronous fetch from inside the rendering process. 3. Small footprint. A typical PNG/SVG logo is 5–50 KB. Even at 100 firms × 50 KB, the firm store is 5 MB — totally fine in JSON, hits S3 in a single round-trip.

The 200 KB cap exists so a careless paste of a 4K hero image doesn't bloat the store. SVG (which is text and compresses well) is preferred when possible.

Auth + role gating

  • Admin token works (x-admin-token: <ADMIN_TOKEN>)
  • Session token works for users with admin or partner role
  • Associates rejected with 403 (Phase 15 read-only enforcement)

A future iteration could allow each firm's partners to manage only their own firm's logo — for now any partner-or-above can update any firm. The existing per-engagement firm scoping (Phase 9) covers data isolation; logo curation is a low-stakes branding decision.

UX

In the Firms tab, the firm editor now includes a LogoUploader widget below the "Logo URL" field:

  • Shows the current logo as a 48×48 thumbnail (or "no logo" placeholder)
  • File picker accepts only the supported image types
  • On select, posts the file → updates logo_url → refreshes the list
  • "Clear logo" button calls DELETE

The plain-text URL field is still there for partners who'd rather host their own logo on a CDN. Either path produces a renderable deliverable.

Files

  • app/auditforge_endpoints.pyPOST /firm/{id}/logo (UploadFile) + DELETE /firm/{id}/logo
  • frontend/src/api/auditforge.tsuploadFirmLogo (FormData) + deleteFirmLogo
  • frontend/src/components/FirmManagement.tsxLogoUploader widget
  • tests/test_auditforge_endpoints.py — 7 tests (auth, 404, 415, 413, 422 empty, persists, clear)