Private Storage Uploads

Configure private S3-compatible uploads with policy, entitlement, quota, checksum, and durable deletion controls.

Synced with starter commit 7d452a6.

Set STORAGE_PROVIDER to s3, r2, or minio, then configure bucket, region, endpoint, and credentials. Objects stay private; browsers receive short-lived presigned URLs, never credentials. Configure bucket CORS for your web origin and required PUT headers, and do not enable ACLs unless the provider requires them.

Upload Contract

  1. POST /api/storage/uploads validates organization membership, storage.upload, the named policy (general, images, documents, or verified), file type, and the smallest of plan, environment, and policy size caps. It atomically reserves organization quota.
  2. The browser PUTs bytes directly to object storage. The uploader uses XHR only to expose progress.
  3. POST /api/storage/uploads/complete verifies object metadata and optional SHA-256, then marks the row active.
  4. GET /api/storage/files/[uuid]?download=1 returns an owner-scoped signed download URL.
  5. DELETE /api/storage/files/[uuid] soft-deletes the row and schedules durable storage_object_delete work.

Abandoned uploads release their reservations after one hour. Deletion retries through the job queue, so the cron runner must be operating. Object keys are generated by the service; never accept a client-selected bucket key.

Before launch, test the complete flow, wrong MIME and checksum, quota races, expired URLs, cross-organization access, deletion retry, and cleanup. See docs/storage-providers.md in the starter for provider-specific CORS and endpoint details.

On this page

Private Storage Uploads · Sushi SaaS