File & object storage¶
Initiative stores uploaded files — image attachments, document files, and their versions — through a pluggable storage backend. By default everything lives on local disk, which is perfect for most deployments. If you'd rather keep uploads in S3-compatible object storage, that's a configuration change with no code change.
Choosing a backend¶
The STORAGE_BACKEND environment variable selects where uploads live:
STORAGE_BACKEND |
Where uploads go |
|---|---|
local (default) |
The filesystem, under UPLOADS_DIR. |
s3 |
Any S3-compatible object store you point it at. |
Nothing object-store-related runs unless you opt in with STORAGE_BACKEND=s3. Initiative never runs or bundles an object store of its own — you bring your own (for example, a Garage node, MinIO, or a cloud provider's S3).
How files are organized¶
Both backends namespace files per guild, mirroring the database's per-guild isolation:
- Local:
UPLOADS_DIR/guild_<id>/<file>. - Object storage: objects under a
guild_<id>/key prefix.
In both cases the download URL stays the same (/uploads/{guild_id}/{filename}), and every download is authorized on each request — files are streamed back through the app only after the same guild-membership and access checks as everything else. Storage location never bypasses access control. See How your data is kept separate.
Connecting an S3-compatible store¶
You'll need, from your store: an S3 API endpoint, the region it was configured with, an existing bucket, and an access key (id + secret) with read/write on that bucket. Then set:
STORAGE_BACKEND=s3
S3_BUCKET=initiative # an existing bucket on your store
S3_ENDPOINT_URL=http://garage:3900 # your store's S3 API endpoint
S3_REGION=garage # must match the store's configured region
S3_ACCESS_KEY_ID=GK...
S3_SECRET_ACCESS_KEY=...
S3_USE_PATH_STYLE=true # true for Garage and most non-AWS stores
S3_KMS_KEY_ID= # optional SSE-KMS key id/ARN; blank otherwise
Notes:
S3_USE_PATH_STYLE—truefor Garage and most self-hosted stores (path-style URLs likehttps://host/bucket/key). Setfalseonly for a store that uses virtual-host-style addressing.S3_REGIONmust match the region your store enforces in its request signature.- Credentials — set the access key id/secret, or leave them unset to use the ambient credential chain where your store supports it.
- The bucket must already exist; Initiative reads and writes objects but doesn't create the bucket.
Migrating an existing deployment from local to S3¶
Switching the backend only changes where new uploads go — files already on local disk must be copied across first. The process is designed to be zero-downtime.
1. Backfill while still on local. With the S3_* settings pointed at your store but STORAGE_BACKEND still local, copy existing files into the bucket:
python -m app.db.backfill_uploads_to_s3 --dry-run # preview
python -m app.db.backfill_uploads_to_s3 # copy for real
It uploads each file with its recorded content type and verifies it, and it's idempotent — safe to re-run until it reports failed=0.
2. Cut over with a fallback window. Set:
With the fallback on, a read that misses in S3 falls back to local disk, so nothing 404s during the transition. New uploads now go to S3.
3. Finish. Once everything serves from S3 and a final backfill reports failed=0, set S3_LOCAL_FALLBACK=false and retire the local uploads volume.
Per-guild storage limits¶
Separately from where files are stored, the owner can cap how much each guild may store, from Settings → Platform → Guilds. Lowering a limit below a guild's current usage blocks new uploads but never deletes existing files.
Related¶
- Configuration · Backups & updates
- How your data is kept separate — why downloads stay gated.