Cloud File Storage
Secure resumable uploads, hierarchy, sharing, versioning, and background scanning.
Product overview
Store large file bytes durably while providing tenant-safe folders, resumable uploads, versions, sharing, quota enforcement, scanning, and synchronized metadata.
Main actors
User, Workspace admin, External recipient, Security operator.
Assumptions
- Object storage holds bytes; PostgreSQL owns namespace and permissions
- Workspace is a tenant boundary
- Files may have many immutable versions
- Clients upload directly with constrained grants
Non-goals
- Collaborative document editing
- Filesystem POSIX semantics
- Backup of arbitrary machines
- Content inspection beyond safety/policy
Requirements
Must-have
- Folders and file metadata
- Resumable upload/download
- Version history
- Member and link sharing
- Quota and malware scan
Should-have
- Trash/restore
- Thumbnails
- Desktop sync change cursor
- Audit export
Future scope
- Block-level delta sync
- End-user encryption
- Document collaboration
- Cross-workspace transfer
Non-functional decisions
| Concern | Decision |
|---|---|
| Durability | Published versions use durable versioned storage and checked metadata; deletion is delayed. |
| Consistency | Namespace, quota reservation, and ACL are transactional; scanning/preview and sync notifications converge. |
| Security | No raw bucket access; grants are narrow and files stay quarantined until checks complete. |
| Scalability | Large bytes bypass application servers; metadata pages use keyset cursors. |
| Privacy | Tenant keys, access logging, retention, and regional placement are explicit. |
User roles
| Role | Access boundary |
|---|---|
| Member | Workspace files granted by inherited ACL |
| Workspace admin | Membership, policy, quota, audit; content access is separately governed |
| External recipient | Only token-scoped shared object |
| Security operator | Quarantine metadata and controlled sample access |
Core workflows
Domain model
| Entities | Relationship |
|---|---|
Workspace → Node | Node is file or folder in one tenant namespace |
File Node → FileVersion | Immutable object metadata; one current published version |
Node → ACL | Direct grant with documented inheritance resolution |
ShareLink → Node | Hashed, expiring, revocable capability |
UploadSession → QuotaReservation | Bounds unfinished bytes |
Workspace → Change | Ordered metadata log drives sync |
Database design
PostgreSQL remains the transactional source of truth. Add specialized stores only for the workloads called out below.
nodes — Tenant namespace
file_versions — Immutable byte object reference
acl_entries — Explicit grants
workspace_changes — Monotonic sync feed
Transaction and retention boundaries
- Reserve quota and create upload session together
- Finalize creates quarantined version; scan publishes current_version and change record atomically
- Move checks destination name uniqueness and prevents descendant cycles
- Trash is soft delete; object purge follows retention and legal hold asynchronously
API design
| Endpoint | Purpose and permission | Idempotency / status |
|---|---|---|
GET /v1/nodes/:id/children?cursor=… | List authorized child nodes Node viewer | Not required 200, 403 |
POST /v1/uploads | Reserve quota and create multipart grant Parent folder editor | Required 201, 403, 413 |
POST /v1/uploads/:id/complete | Verify object and queue scan Session owner | Parts/checksum bound 202, 409, 422 |
POST /v1/nodes/:id/downloads | Issue short-lived download Current ACL/share authorization | Not required 201, 403, 423 |
PATCH /v1/nodes/:id | Rename/move with expected version Node editor and destination editor | Not required 200, 409, 412 |
GET /v1/changes?cursor=… | Ordered tenant sync changes Member; results authorization-filtered | Not required 200, 410 |
List endpoints use opaque keyset cursors unless a small bounded collection is explicit. Unknown objects return 404 where 403 would leak existence.
Architecture
System context
Main application architecture
Critical workflow sequence
Event model
| Event | Producer → consumers | Delivery |
|---|---|---|
UploadFinalized | Upload API → Scanner/type detector | Outbox; unique version stage |
FileVersionPublished | Scanner coordinator → Preview, search, sync | Versioned; consumers dedupe version id |
NodeDeleted | Metadata API → Sync, search removal, lifecycle | Tombstone retained through cursor window |
Failure modes
Object without metadata
Failure
Upload bytes finish but finalize never runs.
Mitigation
Session/object prefix expiry job deletes unclaimed objects and releases quota.
Metadata without object
Failure
Finalize races object-store visibility/failure.
Mitigation
Verify server-side object size/checksum before version transaction; quarantined state never downloads.
Quota race
Failure
Concurrent uploads exceed workspace allowance.
Mitigation
Lock quota counter and reserve expected bytes before grants.
Unsafe file
Failure
Malware scan fails or times out.
Mitigation
Remain quarantined; bounded retries then explicit blocked/manual state.
ACL stale cache
Failure
Revoked user still downloads.
Mitigation
Authorization epoch in cache/token; download grant is short-lived and issued after current check.
Folder cycle
Failure
Concurrent moves make a node its descendant.
Mitigation
Lock affected ancestry or use materialized path/version check; reject ambiguous concurrent move.
Consistency and concurrency
Strong consistency / transactions
- Namespace uniqueness
- quota reservation
- current published version
- ACL/share revocation
Eventual convergence
- Preview
- search index
- device sync delivery
- physical object purge
Concurrency controls
- Unique active sibling name constraint
- Quota counter locked per workspace
- Node optimistic version for move/rename
- Upload complete keyed by session/checksum
- Avoid distributed locks; route tenant metadata writes to its database owner
Caching and background work
Safe to cache
- Immutable published versions through CDN
- Thumbnails
- Folder pages briefly with node version
Never a cache authority
- Quarantined bytes
- Long-lived ACL decision
- Quota available during reservation
Content-addressed/versioned bytes use long TTL. Metadata cache keys include tenant/node version and ACL epoch; revocation bumps epoch.
Background jobs
- Scan and type-detect
- Thumbnail/preview
- Orphan multipart cleanup
- Search indexing
- Trash retention purge
- Quota reconciliation
Security boundaries
Security claims here are design controls, not certification. Threat modeling and jurisdiction-specific review remain required.
- Presigned operations bind bucket key, method, size/checksum, and expiry
- Detect type from bytes; scan/re-encode risky formats
- ACL checks include ancestors and current tenant membership
- Share tokens have high entropy, hashes at rest, expiry, and revocation
- Archive extraction guards path traversal, zip bombs, and file count
- Remote import blocks SSRF and metadata addresses
- Audit shares, downloads, admin actions, and export
Observability
Logs
- Upload session, quota reservation, object verification, scan verdict, and publish transitions
- ACL/share evaluation result with tenant, node, principal type, and policy epoch—never token value
- Move/rename/delete change sequence and optimistic version conflict
Metrics
- Upload completion, checksum rejection, and orphan-session rate
- Scan queue age, verdict distribution, and preview failure rate
- Reserved-versus-stored quota drift and reconciliation corrections
- Sync cursor lag, change-log retention misses, and download authorization denials
Traces
- Trace upload from quota reservation through direct-object finalize, scan, publish, and sync event
- Trace shared download through token/ACL resolution and short-lived storage grant
Alerts
- Quarantined files remain unresolved beyond policy window
- Quota invariant or metadata/object reconciliation drifts
- ACL revocation propagation or sync change feed stalls
Testing strategy
| Layer | What it proves |
|---|---|
| Unit | ACL inheritance, path/name normalization, quota arithmetic, cursor |
| Integration | Tenant FKs, name race, quota lock, object finalize |
| Contract | Object store multipart and scanner verdict |
| Concurrency | Same-name upload, folder move, quota reservation |
| Failure | Orphan object, scanner crash, stale ACL cache |
| End-to-end/load | Upload → publish → share → revoke → restore; large multipart metadata pressure |
Do not mock PostgreSQL constraints, transaction isolation, object-store semantics, or provider contract fixtures in the tests intended to prove those boundaries.
Deployment evolution
Scale evolution
These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.
| Scenario | Architecture may evolve toward | Decision rule |
|---|---|---|
| 1K users | One API, PostgreSQL for the folder tree, ACLs, and quotas, object storage with presigned uploads, a scan worker | Keep bytes out of the API with direct uploads; commit file metadata only after the stored object is verified |
| 100K users | Keep bytes direct-to-storage; scale scan/preview independently; add metadata read replicas and search | Scale scanning and previews independently when their backlog delays file availability; metadata keeps one primary |
| 1M+ users | Partition workspaces into regional cells with one metadata writer; replicate immutable bytes under residency policy | Move workspaces into regional cells for residency or metadata write load; immutable objects replicate safely |