ADR 0008: Production Upload Storage¶
- Status: Accepted
- Date: 2026-07-10
- Amended: 2026-08-02
Context¶
The original decision made production Disk storage fail closed. A real durable
mount at /app/storage, a single web replica, a ReadWriteOnce claim, and a
Recreate rollout prevented uploads from silently landing in an ephemeral
container filesystem. Those constraints remain valid for Disk deployments.
MedTracker must also support customers that select S3-compatible object storage and must not make S3 a requirement for customers that continue to use Disk. Operators need a verified migration in either direction without rewriting logical blob keys, weakening attachment authorization, or assuming that a retired source is still current.
Decision¶
Production explicitly selects one of four Active Storage services:
persistent: Disk-only steady state.persistent_with_s3_mirror: Disk-readable migration state.s3_with_persistent_mirror: S3-readable migration or rollback state.s3: S3-only steady state.
Disk-inclusive modes retain the original fail-closed ACTIVE_STORAGE_ROOT
checks. S3-inclusive modes require endpoint, bucket, region, access key, and
secret key configuration. persistent remains the default, so a customer can
run or upgrade MedTracker without S3 settings.
Topology constraints¶
A Disk-only deployment may use the existing ReadWriteOnce and Recreate
single-pod topology. Every process that performs storage work must see the same
durable root. Horizontal web scaling is not supported by that topology unless
the operator separately proves suitable shared-filesystem semantics.
S3-only deployments do not mount /app/storage; independently scheduled web
and worker pods are permitted because the durable object service is shared.
Selecting S3 does not itself require a web/worker split.
Symmetric migration¶
Disk to S3 uses persistent → persistent_with_s3_mirror →
s3_with_persistent_mirror → s3. S3 to Disk uses the same states in reverse.
Backfill copies by logical key, verifies recorded checksums, tolerates valid
existing destinations, and resumes safely. Cutover requires quiesced storage
mutations, drained mirror work, and a stable fully verified blob set. Blob
service identities change in one database transaction.
During the rollback window, writes continue to both services. An in-window rollback returns to the source-primary mirror state. After finalization, returning to the former backend is a new migration with a newly provisioned and fully reconciled destination; it is not rollback.
Source retirement is optional and is never performed by the application. It is eligible only after the rollback window, acceptance, recovery proof, final reconciliation, and explicit operator sign-off.
Recovery¶
Every recovery point coordinates PostgreSQL with the backends required by live blob service identities and the active migration phase:
- Disk-only: database plus Disk recovery reference.
- S3-only: database plus S3 recovery reference.
- Mirror or rollback window: database plus both storage references.
The existing 35-daily and 12-monthly retention objectives continue to apply. Every accepted recovery point must pass an isolated restore, integrity check, authorized retrieval, and cross-household denial check.
Rejected alternatives¶
Making object storage mandatory would add infrastructure and credential requirements for customers whose durable Disk topology is sufficient. Keeping separate one-way migration implementations would duplicate verification, rollback, and privacy behavior. Copying the Disk directory directly would copy physical sharding rather than the Active Storage logical service contract.
Consequences¶
Operators choose and provision a supported backend explicitly. Disk remains a supported steady state. S3 enables storage-independent scheduling but adds provider-owned durability, access-policy, lifecycle, and recovery duties. Migration phases protect both backends and require maintenance gates. No deployment may delete a source merely because application finalization passed.
Related documents¶
docs/operations/upload-storage-backup-and-restore.mddocs/operations/home-ops-portable-storage-handoff.mdconfig/storage.ymlconfig/environments/production.rb- GitHub issues #1551 and #1774