v2.0.0
Overview
This release introduces file instruments: instruments whose responses are uploaded files (for example single documents or multi-file MRI scan sessions) rather than form data. To support this, Open Data Capture can now use an S3-compatible object storage service. Files are not stored in MongoDB; the browser uploads and downloads them directly via short-lived presigned URLs, and only file metadata is kept in the database.
The default Docker Compose stack now ships a self-hosted RustFS service for this purpose.
This is a non-breaking upgrade. Object storage is opt-in via the new
STORAGE_ENABLEDflag, which defaults tofalse. Existing deployments upgrade and boot unchanged; everything except file instruments works exactly as before. File instruments only become available once you enable storage (see below). WhenSTORAGE_ENABLED=false, attempting to create a file-instrument record returns a clear503 Service Unavailablerather than failing at startup.
What’s new
- File instruments — a new
FILEinstrument kind with one or more file groups, each constrained by an allowed file type and acount(min/max). - Object storage service — a new
rustfsservice in the Compose stack, backed by a./rustfs/datavolume. - Direct browser uploads/downloads — files transfer directly between the browser and the storage service using presigned URLs, keeping large transfers off the API.
- Two demo file instruments (
ARBITRARY_SINGLE_FILE,MRI_SCAN_SESSION) are now seeded by the demo data.
New environment variables
The following variables have been added (see .env.template). “Required” means required only when STORAGE_ENABLED=true; when storage is disabled these are ignored.
| Variable | Required | Description |
|---|---|---|
STORAGE_ENABLED | No | Master switch for object storage / file instruments (default false). When true, the four variables marked “Yes” must all be set or the API refuses to start. |
STORAGE_ENDPOINT | Yes | Internal endpoint the API uses for S3 API calls. In Compose this is http://rustfs:9000. |
STORAGE_PUBLIC_ENDPOINT | No | Externally reachable endpoint embedded in presigned URLs. Defaults to http://localhost:${APP_PORT}/storage, which Caddy proxies to rustfs. Set this to your public storage URL (e.g. https://your-domain.com/storage) for real deployments. |
STORAGE_ACCESS_KEY | Yes | S3 access key. Also used as the RustFS admin access key in the Compose stack. |
STORAGE_SECRET_KEY | Yes | S3 secret key. Also used as the RustFS admin secret key in the Compose stack. |
STORAGE_BUCKET | Yes | Bucket name where files are stored (default open-data-capture). Created automatically on startup if absent. |
STORAGE_REGION | No | Region name. Can be any value for self-hosted S3 (default us-east-1). |
RUSTFS_VERSION | No | RustFS image tag for the Compose stack (default latest). |
Upgrading without enabling storage
If you don’t need file instruments yet, no action is required — pull the new images and restart. STORAGE_ENABLED defaults to false, the bundled rustfs container boots with placeholder credentials but is otherwise unused, and the rest of the application is unchanged.
To enable file instruments later, follow the steps below.
Enabling object storage
1. Add the storage configuration
Set STORAGE_ENABLED=true and provide the storage credentials. If you generate your .env with scripts/generate-env.sh, re-run it — it now generates STORAGE_ACCESS_KEY and STORAGE_SECRET_KEY for you. Back up your existing .env first, since regenerating will also produce new values for other secrets.
Otherwise, copy the new STORAGE_* entries from .env.template into your existing .env and fill them in. At minimum:
STORAGE_ENABLED=trueSTORAGE_ENDPOINT=http://rustfs:9000STORAGE_PUBLIC_ENDPOINT=STORAGE_ACCESS_KEY=$(openssl rand -hex 16)STORAGE_SECRET_KEY=$(openssl rand -hex 32)STORAGE_BUCKET=open-data-captureSTORAGE_REGION=us-east-1When STORAGE_ENABLED=true, all of STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, STORAGE_BUCKET and STORAGE_ENDPOINT must be set or the API will refuse to start. Leave STORAGE_PUBLIC_ENDPOINT blank to use the Caddy-proxied default (http://localhost:${APP_PORT}/storage). Set it explicitly to your externally accessible storage URL for a production deployment behind a custom domain.
2. Update your reverse proxy
The bundled Caddyfile now proxies /storage/* to the storage service so the browser can reach presigned URLs:
handle_path /storage/* { reverse_proxy rustfs:9000 { header_up Host rustfs:9000 }}If you use the bundled Caddy configuration, pull the updated Caddyfile. If you run your own reverse proxy, add an equivalent route forwarding /storage/* to the storage service on port 9000, and ensure the upstream Host header matches the storage host. Otherwise, set STORAGE_PUBLIC_ENDPOINT to a URL your users’ browsers can reach directly.
3. Pull the updated Compose stack and restart
The updated docker-compose.yaml adds the rustfs service (with a ./rustfs/data bind mount) and wires the new variables into the api service. Make sure ./rustfs/data is on persistent, backed-up storage.
docker compose pulldocker compose up -dOn first boot the API creates the STORAGE_BUCKET automatically if it does not already exist.
Backups
File contents now live in object storage, not in MongoDB. Update your backup strategy to include the storage volume (./rustfs/data for the bundled stack, or your S3 bucket) alongside your existing database backups. A database backup alone will no longer capture uploaded files.
Notes
- File uploads and downloads go directly from the browser to the storage service via presigned URLs, so the storage
STORAGE_PUBLIC_ENDPOINTmust be reachable from end users’ browsers, not just from the API container. - If you supply your own managed S3 (e.g. AWS S3, MinIO) instead of the bundled RustFS, point
STORAGE_ENDPOINT/STORAGE_PUBLIC_ENDPOINTat it, set the matching credentials and region, and you can drop therustfsservice.