Files
web_sport/docs/adr/0009-media-in-s3-compatible-storage-metadata-in-postgresql.md

38 lines
1.5 KiB
Markdown

# ADR-0009: Media in S3-compatible storage, metadata in PostgreSQL
- **Status:** Accepted
- **Date:** 2026-08-11
## Context
Product photography is the bulk of an apparel store's bytes. Storing binaries in
PostgreSQL bloats backups, makes replication slow, and forces image delivery through the
application tier.
## Decision
Bytes live in Cloudflare R2 (MinIO locally). PostgreSQL stores only metadata: object
key, MIME type, dimensions, size, alt text and a small base64 blur placeholder.
Browsers upload directly to the bucket using a short-lived presigned URL, so files never stream
through the API. Public URLs are composed at read time from `STORAGE_PUBLIC_URL + storageKey`,
never stored — so changing bucket, CDN domain or provider is a config change, not a data
migration.
Uploads are restricted by a MIME allow-list, and generated keys are date-partitioned UUIDs that
never echo the user's filename.
## Consequences
Database backups stay small and fast. Images are served by a CDN at the edge. The
API scales on CPU, not bandwidth.
The cost is eventual-consistency between the two stores: a failed upload can leave an orphaned
row, and a deleted row can leave an orphaned object. A periodic reconciliation job is the
accepted mitigation; two-phase commit across a database and object storage is not worth it.
## Alternatives considered
`bytea` columns — rejected for the reasons above. Serving uploads through the API
— rejected: it makes the API a bandwidth bottleneck and a timeout risk on large files.