Attachment storage
Keep attachment bytes in Postgres, or move them to S3-compatible storage, with garbage collection and a verified migration.
Attachment bytes are stored separately from the notes themselves. BLOB_STORAGE decides where: postgres, which needs no configuration at all, or s3 for any S3-compatible bucket.
Choosing
| Postgres | S3 | |
|---|---|---|
| Configuration | None | Bucket, credentials, region or endpoint, and a CORS policy |
| Per-file ceiling | 25 MB by default, a memory bound | 500 MB by default, a product decision |
| Where bytes flow | Through the server process, buffered | Direct between client and bucket |
| Backups | Covered by your database backup | Separate from the database, so back both up |
Turning S3 on
Point the server at the bucket and restart it. An incomplete configuration is a fatal startup error rather than a silent fallback to the database, so a half-configured server will not quietly accept uploads it cannot keep.
Garbage collection
Collecting attachments no note references any more is off by default, behind BLOB_GC_ENABLED. When it is on:
- An attachment must have existed for
BLOB_GC_ORPHAN_DAYS, 30 by default, before it can be collected. An attachment is uploaded before the note embedding it is written, and that note may arrive days later from a device that was offline. - The orphan pass runs at most every six hours, on top of a sweep every fifteen minutes.
- At most 200 deletions happen per run, which bounds the blast radius if the reference table is ever wrong.
The deletion queue is a different thing and is always on: those are objects whose row a vault or organization delete already removed, and the flag does not affect them.
Moving existing attachments to S3
The migration runs in four phases and is reversible until the last one.
- 1Dry run. It reports what would move and changes nothing.
- 2Write the objects. Every blob's bytes are re-hashed, any row whose content disagrees with its recorded hash is refused rather than moved, each object is verified with a HEAD request, and only the storage key is recorded. Reads still come from the database, so this phase is undone by clearing one column.
- 3Confirm downloads still work, while they are still being served from the database.
- 4Flip the verified rows and release their bytes, after re-checking that each object is still present at the right size. After this the database copy is gone.
This page is written from docs/DEPLOY.md in the repository. Those documents go further, and they are the ones that change first.
Read it on GitHubWas this page helpful?
