file-storage-design · git:20260720.1dc980e · 2026-07-20 · sha256 b6e6ca5a89a22da9
file-storage-design git:20260720.1dc980eA
Immutable. This exact content is served forever at /api/v1/blob/b6e6ca5a89a22da9.
---
name: file-storage-design
description: Store user files in object storage with presigned transfers, validation, and lifecycle rules. Use when building upload/download features or moving file handling off application servers.
---
# File storage design
Files go to object storage; your servers handle authorization and
metadata, never the bytes. Every design decision follows from keeping
payloads off your request path.
## Method
1. **Presign uploads, direct to the bucket.** Client asks your API for
an upload URL; you authorize, generate a presigned PUT (or POST
policy) scoped to one key, content-type, size limit, and short
expiry; client uploads directly. Your API never proxies bytes, so
uploads cannot exhaust your workers.
2. **Own the keys, never trust filenames.** Server generates the object
key (`tenant/{id}/uploads/{uuid}`); the user's filename is metadata,
stored and returned as Content-Disposition. User-controlled paths are
traversal and overwrite attacks waiting (see path-traversal-defense).
3. **Verify after upload, before use.** A confirm endpoint (or bucket
event) checks the object exists, size matches, magic bytes match the
declared type (not the extension), then flips DB status to `ready`
and enqueues scanning/thumbnailing as jobs. Files never referenced by
a confirm get lifecycle-deleted (abandoned uploads are storage cost).
4. **Serve through presigned GETs or signed CDN URLs.** Short-lived
(minutes) per-object URLs after your authz check; long-cache public
assets go behind the CDN with immutable keys. Buckets stay private;
a public bucket is an incident report with a delay timer.
5. **Model metadata in your database.** Owner, tenant, size, type,
checksum, status, created_at; the bucket is a blob heap, your DB is
the truth about whose file is whose. Deletes are DB-first (soft),
object cleanup async, so a failed delete never orphans authorization.
6. **Set lifecycle rules in code.** Abandoned multipart uploads aborted
after a day; temp/exports expire in days; cold originals transition
to infrequent-access tiers; versioning plus a deletion grace window
where compliance demands undelete.
7. **Bound everything.** Max size per type, allowed content types,
per-user quotas and rate limits on URL issuance; large files go
multipart with the same per-part signing.
## Boundaries
- User-uploaded HTML/SVG served from your domain is stored XSS; serve
from a sandboxed domain with Content-Disposition: attachment or strict
CSP (see file-upload-safety for the security-side detail).
- Files that must join transactions (tiny configs, thumbnails under a
few KB) can live in the DB; anything bigger pays for itself in
operational pain.
- Direct-to-bucket requires CORS configuration on the bucket; scope it
to your origins and the PUT/POST methods only.