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
adminorpartnerrole - 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.py—POST /firm/{id}/logo(UploadFile) +DELETE /firm/{id}/logofrontend/src/api/auditforge.ts—uploadFirmLogo(FormData) +deleteFirmLogofrontend/src/components/FirmManagement.tsx—LogoUploaderwidgettests/test_auditforge_endpoints.py— 7 tests (auth, 404, 415, 413, 422 empty, persists, clear)