pnpm workspace with apps/web, apps/api, apps/collab, and packages/shared; strict TypeScript base config, repo-wide ESLint (flat) + Prettier, Vitest per package, and root scripts lint/typecheck/test/ build. @dorfteich/shared ships a first health-response helper consumed by apps/api to prove workspace linking. Existing markdown docs are reformatted once by the new Prettier setup. Closes #1 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2.4 KiB
2.4 KiB
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/<pondId>/<fileId>(opaque ids; original filename and metadata in the database). No S3 dependency; the storage access goes through a thinFileStorageservice 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-Dispositionand strictContent-Typehandling prevent inline execution (seesecurity.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
FileStorageinterface.