Linked Notes stores attachment metadata in PostgreSQL and bytes in the
attachment_data Docker volume mounted at /data/attachments. Back up and
restore them together; either one alone is incomplete.
- Final filenames are random UUIDs generated by the server. They have no client path or extension.
- In-progress files live under
.staging/<uuid>.parton the same volume so the final rename is atomic. - The database records the display name, detected safe MIME type, byte count, SHA-256, optional image dimensions, note ID, and timestamps.
ATTACHMENTS_DIRmust be absolute.MAX_UPLOAD_BYTESis configurable and defaults to 104,857,600 bytes (100 MiB).- PNG, JPEG, GIF, and WebP may render inline. All other content downloads as an
attachment; unknown or misleading content uses
application/octet-stream.
The current schema needed no Phase 4 migration: the versioned foundational
migration already created the Attachment table, indexes, and cascading note
relationship. Phase 4 activates that reviewed schema and adds filesystem
integrity behavior around it.
Oversized, empty, length-mismatched, interrupted, or invalid requests remove
their staged bytes and do not create metadata. If the note changes before commit,
the optimistic conflict rolls back metadata/version changes and removes the final
byte. Removal commits metadata first and deletes bytes afterward; an unlink
failure is logged as attachment_orphan_cleanup_failed with an opaque storage
name and is repaired by reconciliation.
If a referenced file is absent or its size differs, the attachment card remains visible with an unavailable state and download returns HTTP 410. Metadata is not silently deleted. Workspace Settings can run a checksum reconciliation and remove only unreferenced final bytes and staging files older than 24 hours.
Measured on 19 July 2026 using the loopback Compose installation, Docker Engine 29.6.1, and the non-root read-only app container:
- 96 MiB deterministic upload in 64 KiB chunks
- 4.079 seconds, 23.53 MiB/s
- app memory: 69.43 MiB baseline, 79.98 MiB peak, 10.55 MiB increase
- server SHA-256:
49542702b710c0b50cd7bc83caee0e8e0fe03c8863a052b52d3f30f9101350ad - the client-generated and server-generated SHA-256 values matched; cleanup removed the measurement fixture
After an app-container restart, health remained green and a fixture re-downloaded with the same pre-restart SHA-256. Post-restart reconciliation reported equal metadata and stored-file counts with zero missing, corrupt, orphaned, or stale files. These local numbers demonstrate bounded behavior for the agreed fixture; they are not a universal throughput guarantee.
Workspace Settings now creates one portable archive containing the relational workspace and checksum-verified attachment bytes. Restore stages and verifies the complete archive before changing live data; replacement creates a retained safety backup first. See backup, restore, and recovery for the format, limits, merge semantics, and step-by-step recovery procedure.
Host-level PostgreSQL and attachment_data volume snapshots remain useful as a
second independent recovery layer, but must be captured together while writes
are stopped. Do not delete either volume with docker compose down --volumes
unless permanent data loss is intended.