Files
File storage and management built on content-addressable storage (CAS). Every file in the system goes through the files table — the single source of truth — with binary content on S3-compatible storage (Tigris) keyed by content hash. Since PR#1752 files are organized in a collection-canonical model: collections → members (versioned slots) → versions.
Pivot (2026-05/06): the flat file model was superseded by the collection-canonical chain. Foundation landed in PR#1752 (spec
2026-05-16-files-collection-canonical-design.md); canonical chain writes for Document Obtaining fulfillments landed in PR#2005. The oldfile_uploads_metadatatable was renamed tofile_versions.
CAS Rules (MANDATORY)
- Every file goes through CAS. Entities reference files via a
fileIdFK tofiles— never via path, URL, blob, or inline bytes. NodocumentUrl/pdfPath/logoBlobcolumns in new code. - Streams only:
service.upload(stream, …)/service.download(fileId) → { stream, contentLength }. The buffer-shapeduploadFile/downloadFilehelpers inbackend/src/lib/storage.tsare deprecated (quick-lint.shflags imports from new code). - Content is deduplicated on
files.contentHash(unique) — the same bytes uploaded twice resolve to onefilesrow.
Source Files
| Layer | Path |
|---|---|
| Schema | backend/src/db/schema.ts |
| Routes | backend/src/routes/files.ts, files-bulk.ts, file-collections.ts, file-collection-members.ts, entity-file-links.ts, entity-collection-links.ts, file-labels.ts |
| File Service | backend/src/services/file-service.ts |
| Collection Services | backend/src/services/file-collections-service.ts, file-collection-members-service.ts, entity-collection-links-service.ts |
| Bulk / Label / Link | backend/src/services/bulk-file-service.ts, file-label-service.ts, entity-file-link-service.ts |
| Chain Synthesis | backend/src/lib/file-operations/ (fulfillment-files.ts, collection-synthesis.ts, user-files.ts, template-files.ts) |
| Reconciliation Job | backend/src/lib/jobs/processors/fulfillment-chain-reconciliation.ts |
| Storage Lib | backend/src/lib/storage.ts (buffer helpers deprecated) |
| Components | frontend/src/components/files/ (~32 files) |
| Queries | frontend/src/lib/queries/fileQueries.ts, fileCollectionQueries.ts |
Database Tables
| Table | Purpose |
|---|---|
files | CAS entity — contentHash (unique), filename, MIME type, size, rendition lineage (originalFileId/renditionType), soft delete (deletedAt) |
file_collections | Structural grouping (no scope of its own); createdBy → contacts.id (Human FK Rule); documentObtainingRequestId discriminator for fulfillment-synthesized collections |
file_collection_members | ”Versioned slot” within a collection — optional label (NULL for bulk slots like Fotodokumentation), sortOrder, soft archive |
file_versions | One row per version of a slot (renamed from file_uploads_metadata, PR#1752); memberId FK, per-version displayName, isArchived, plus legacy entity FKs (building, invoice, …) |
entity_file_links | Polymorphic join — links a file (and since PR#1752 its collectionId) to any entity type |
file_label_associations | Labels applied to files; labelId references document_template_labels |
Collection-Canonical Model
FileCollection 1──* FileCollectionMembers 1──* FileVersions ──1 File (CAS)
FileCollection 1──* EntityFileLinks ──* (any entity)
- A collection has 1..N members; each member is a versioned slot carrying 1..N versions (ordered by
createdAt— newest non-archived row is “current”). - Entity visibility is handled by
entity_file_links.collectionId; new writers populate it, the PR#1752 migration backfilled existing rows. A Phase-6 follow-up renames the table toentity_collection_linksand dropsfile_idonce all readers/writers have migrated (same phase tightensfile_versions.memberIdandentity_file_links.collectionIdto NOT NULL). - Document Obtaining fulfillments synthesize the chain:
ensureFulfillmentChain/findUnchainedFulfillmentsinbackend/src/lib/file-operations/fulfillment-files.tscreate one collection per request (enforced by a partial unique index onfile_collections.documentObtainingRequestId— the discriminator that prevents CAS-deduplicated content from leaking another project’s collection). A BullMQ reconciliation job sweeps unchained fulfillments (PR#2005). createdByon collections and members targetscontacts.id(Human FK Rule);entity_file_links.createdByandfile_versions.userIdare grandfathered onusers.iduntil Phase 6.
Storage Architecture
| Concern | Implementation |
|---|---|
| Object storage | S3-compatible via Tigris (TIGRIS_* env vars), buckets in fra |
| Upload / download | Streams through FileService (creates the CAS row atomically) |
| Renditions | sharp generates thumbnails/previews — rendition rows reference the original via originalFileId + renditionType |
| Background processing | Rendition generation via Background Jobs (lib/jobs/processors/rendition.ts) |
| Validation | MIME type checking and file size limits enforced server-side |
Frontend Components
| Component | Purpose |
|---|---|
FileUpload / FileUploadDialog | Drag-and-drop upload with progress |
CollectionCard / CollectionFilesView / CollectionMemberRow | Collection-canonical browsing (collections → members → versions) |
FilesViewGrid / FilesViewList / CompactFileList | Gallery and list views |
FilePreview / FilePreviewDialog / FileHoverPreview | Inline preview for images and PDFs |
FileAssociationManager / EntityFilesActions | Link/unlink files to entities |
BulkActionsToolbar / BulkLabelAssignDialog | Bulk operations and labeling |
Linkage Semantics
Files are linked to Buildings, Projects, Documents, Document Obtaining fulfillments, Quotes, Invoices and other entities through entity_file_links. Building-level views aggregate across projects (viewer scope); project-level links gate partner access (pricing confidentiality).
Related Pages
Documents | Document Obtaining | Buildings | Projects | Background Jobs | Database Architecture | Service Layer Pattern | Deployment Pipeline