Local storage by default, media unrouted, the logo excepted (2026-08-03)¶
Uploaded files (documents, invoice PDFs, the firm logo) had been served
two ways: in production from an S3-compatible bucket with signed URLs,
and under runserver from MEDIA_ROOT through Django's static()
helper. A checkout therefore needed cloud credentials to behave like
production, and a server running local storage would have served every
confidential upload at a guessable media/... address. The
containerization work of August 2026 made the storage backend a setting
and had to decide what a local-storage server exposes.
Decision¶
STORAGE_BACKEND selects the default storage: local (the default) is
FileSystemStorage under MEDIA_ROOT; s3 is django-storages with
query-string signing on. Local is the default on purpose so a checkout
runs without cloud credentials. Any other value refuses to start.
MEDIA_ROOT is never exposed through Django's URL configuration in
production. development_media_urlpatterns() routes media/ only when
DEBUG is on and the backend is local. Everything confidential is
reached through a view that checks access and streams the file through
the storage API, so the two backends are interchangeable.
One exception: media/company/, the upload_to of Firm.logo, stays
routed under local storage in every mode
(public_branding_media_urlpatterns()). The logo is public branding
and is loaded by URL from the settings page, the public intake form
pages and the invoice PDF renderer. Only that subdirectory is exposed;
under S3 the bucket's own URLs apply and the route is not installed.
Alternatives¶
- Keeping S3 as the only backend. Rejected so that "a checkout can run without cloud credentials"; the test suite likewise points storage at a temporary directory for every test.
- Routing all of
media/under local storage, asrunserverdoes. Rejected: "confidential uploads must go through the authenticated download views". - Unrouting the logo along with everything else. Tried, for part of a
day on 2026-08-03: the logo 404ed on the settings page, the intake
forms and in every invoice PDF generated in that window, which had to
be regenerated afterwards (
generate_invoice_pdfs --sent-since). - Embedding the logo from disk in PDFs instead of fetching it by URL.
Done as well for the PDF renderer's
file://fetcher, so the PDF no longer depends on the HTTP route; the route remains for the pages.
Consequences¶
- Code reads and writes files only through a
FieldFileordefault_storage. A path on disk is wrong under either backend. - A template that builds a
media/...URL for anything but the firm logo works underrunserverand 404s on a server. Serve the file through a view. - A new public asset (a second logo, a letterhead image) is not public
by putting it in
media/; it needs eithercompany/as itsupload_toor a route of its own, and a decision that it is public. config/tests/test_media_security.pypins all of this: unrouted in production, routed underrunserver, never mapped under S3, the logo exception's scope, and a login on the document endpoints.- Changing
STORAGE_BACKENDon a live install moves nothing: the files must be copied across with the paths kept, as the operator page describes.
Evidence¶
config/settings.py, the storage block: "Local is deliberately the default so a checkout can run without cloud credentials. Production never exposes MEDIA_ROOT through Django's public URL configuration; protected download views continue to stream confidential files through Django's storage API."config/urls.py,public_branding_media_urlpatterns(): "Unrouting MEDIA_ROOT protects confidential uploads, but the logo is deliberately public branding ... Only this one subdirectory - Firm.logo's upload_to- is exposed."
- Commit "feat: containerization optimizations (#482)" (2026-08-03):
STORAGE_BACKEND, the media routing functions andtest_media_security.py. - Commit "fix: firm logo 404s after media unrouting - route media/company/ publicly" (2026-08-03): "PR #482 rightly stopped routing MEDIA_ROOT in production (confidential uploads must go through the authenticated download views), but the firm logo lives there too and is deliberately public branding."
- Commit "fix(invoicing): heal logo-less invoice PDFs from the media unrouting window" (2026-08-03).
Related¶
- Platform and config, "Storage" and "URLs"
- File storage