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 old file_uploads_metadata table was renamed to file_versions.

CAS Rules (MANDATORY)

  • Every file goes through CAS. Entities reference files via a fileId FK to files — never via path, URL, blob, or inline bytes. No documentUrl/pdfPath/logoBlob columns in new code.
  • Streams only: service.upload(stream, …) / service.download(fileId) → { stream, contentLength }. The buffer-shaped uploadFile/downloadFile helpers in backend/src/lib/storage.ts are deprecated (quick-lint.sh flags imports from new code).
  • Content is deduplicated on files.contentHash (unique) — the same bytes uploaded twice resolve to one files row.

Source Files

LayerPath
Schemabackend/src/db/schema.ts
Routesbackend/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 Servicebackend/src/services/file-service.ts
Collection Servicesbackend/src/services/file-collections-service.ts, file-collection-members-service.ts, entity-collection-links-service.ts
Bulk / Label / Linkbackend/src/services/bulk-file-service.ts, file-label-service.ts, entity-file-link-service.ts
Chain Synthesisbackend/src/lib/file-operations/ (fulfillment-files.ts, collection-synthesis.ts, user-files.ts, template-files.ts)
Reconciliation Jobbackend/src/lib/jobs/processors/fulfillment-chain-reconciliation.ts
Storage Libbackend/src/lib/storage.ts (buffer helpers deprecated)
Componentsfrontend/src/components/files/ (~32 files)
Queriesfrontend/src/lib/queries/fileQueries.ts, fileCollectionQueries.ts

Database Tables

TablePurpose
filesCAS entity — contentHash (unique), filename, MIME type, size, rendition lineage (originalFileId/renditionType), soft delete (deletedAt)
file_collectionsStructural grouping (no scope of its own); createdBycontacts.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_versionsOne 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_linksPolymorphic join — links a file (and since PR#1752 its collectionId) to any entity type
file_label_associationsLabels 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 to entity_collection_links and drops file_id once all readers/writers have migrated (same phase tightens file_versions.memberId and entity_file_links.collectionId to NOT NULL).
  • Document Obtaining fulfillments synthesize the chain: ensureFulfillmentChain / findUnchainedFulfillments in backend/src/lib/file-operations/fulfillment-files.ts create one collection per request (enforced by a partial unique index on file_collections.documentObtainingRequestId — the discriminator that prevents CAS-deduplicated content from leaking another project’s collection). A BullMQ reconciliation job sweeps unchained fulfillments (PR#2005).
  • createdBy on collections and members targets contacts.id (Human FK Rule); entity_file_links.createdBy and file_versions.userId are grandfathered on users.id until Phase 6.

Storage Architecture

ConcernImplementation
Object storageS3-compatible via Tigris (TIGRIS_* env vars), buckets in fra
Upload / downloadStreams through FileService (creates the CAS row atomically)
Renditionssharp generates thumbnails/previews — rendition rows reference the original via originalFileId + renditionType
Background processingRendition generation via Background Jobs (lib/jobs/processors/rendition.ts)
ValidationMIME type checking and file size limits enforced server-side

Frontend Components

ComponentPurpose
FileUpload / FileUploadDialogDrag-and-drop upload with progress
CollectionCard / CollectionFilesView / CollectionMemberRowCollection-canonical browsing (collections → members → versions)
FilesViewGrid / FilesViewList / CompactFileListGallery and list views
FilePreview / FilePreviewDialog / FileHoverPreviewInline preview for images and PDFs
FileAssociationManager / EntityFilesActionsLink/unlink files to entities
BulkActionsToolbar / BulkLabelAssignDialogBulk 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).

Documents | Document Obtaining | Buildings | Projects | Background Jobs | Database Architecture | Service Layer Pattern | Deployment Pipeline