Baalda

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

PostgresS3
ConfigurationNoneBucket, credentials, region or endpoint, and a CORS policy
Per-file ceiling25 MB by default, a memory bound500 MB by default, a product decision
Where bytes flowThrough the server process, bufferedDirect between client and bucket
BackupsCovered by your database backupSeparate 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.

A browser-side upload needs the bucket's CORS policy from the deployment document. Without it uploads fail in the client while the server looks perfectly healthy.

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.

  1. 1Dry run. It reports what would move and changes nothing.
  2. 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.
  3. 3Confirm downloads still work, while they are still being served from the database.
  4. 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 GitHub

Was this page helpful?