# ADR 0011: Filesystem volume for uploads, DB-tracked quotas - Status: accepted - Date: 2026-07-04 ## Context Pages contain pasted/uploaded images; kickoff added non-image attachments (PDF, office files, …) with a type allowlist and size limits. Self-hosting must not require object storage. Quotas exist on three levels (instance default → per user → per pond, most specific wins) per kickoff decision; storage volume is one of the quota dimensions (kickoff assumption #1, confirmed). ## Decision - **Uploads live on a dedicated Docker volume**, laid out as `uploads//` (opaque ids; original filename and metadata in the database). No S3 dependency; the storage access goes through a thin `FileStorage` service so an S3 binding stays possible later. - **Serving**: files are streamed by the API with permission checks (an attachment inherits the permissions of its pond/page); no direct static serving of user uploads. `Content-Disposition` and strict `Content-Type` handling prevent inline execution (see `security.md`). - **Validation on upload**: configurable MIME/extension allowlist (instance setting; images always allowed), size limit per file, magic-byte sniffing for images. SVG uploads are sanitized (script stripping) or rejected per instance setting. - **Quota dimensions** (each on the three-level override ladder): - storage bytes per pond (default: 1 GiB), - max file size (default: 25 MiB), - editors per pond (default 5) and readers per pond (default 50) for self-signup personal ponds, - additional ponds a user may create (default 0). Current usage is tracked in the database (`pond_usage`), updated transactionally with upload/delete; quota exceedance yields a clear, i18n-ed error. - **Orphan cleanup**: deleting a page moves it to trash (ADR 0013); its files are removed when the trash entry is purged. A nightly job reconciles volume contents against the database and reports drift. ## Consequences - Backup must cover the uploads volume in addition to `pg_dump` (ADR 0015). - Multi-node scaling would need shared storage or the S3 binding — out of scope at target size, but not blocked. ## Alternatives considered - **Files as bytea in PostgreSQL**: single backup artifact, but bloats the database, slows dumps/restores, and complicates streaming; rejected. - **MinIO/S3 required**: another stateful service for every self-host; rejected for v1, kept possible via the `FileStorage` interface.