Storage
Overview
Section titled “Overview”Storage provides blob storage for files, images, documents, and large payloads. The HTTP surface is S3-shaped and familiar if you have used Supabase Storage or AWS S3.
Every organization gets named buckets. Objects support path-like keys (uploads/2026/report.pdf), multipart uploads for large files, and time-limited signed URLs for secure public access. Authorization combines Identity permissions with a shared policy DSL—the same engine Data uses, evaluated against object metadata instead of SQL rows.
Backends are swappable: local filesystem for development and self-hosted deployments, S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) for production.
Key features
Section titled “Key features”- Bucket management — Create, list, update, and delete buckets per org.
- Simple PUT/GET/HEAD/DELETE — Stream objects with correct content types and ETags.
- Multipart uploads — S3-compatible protocol for files up to 5 TiB.
- Signed URLs — HMAC signing on filesystem backend; native presign on S3.
- Public buckets — Optional anonymous read for CDN-friendly assets.
- Per-bucket policies — Fine-grained rules using
object.*andauth.*builtins. - Range requests — Partial content downloads with
Range: bytes=0-1023.
Quickstart
Section titled “Quickstart”Create a bucket, upload an avatar, and generate a signed download URL.
reactor auth login user@example.com --password '...'reactor storage buckets create avatarsreactor storage upload avatars user-123.png ./avatar.png --content-type image/pngreactor storage sign avatars user-123.png --ttl 3600import { createClient } from '@reactor/client';
const reactor = createClient({ url: process.env.REACTOR_URL! });await reactor.auth.signInWithPassword({ email, password });reactor.auth.setOrg('acme');
await reactor.storage.createBucket('avatars', { public: false });
await reactor.storage.from('avatars').upload('user-123.png', file, { contentType: 'image/png',});
const { signedUrl } = await reactor.storage .from('avatars') .createSignedUrl('user-123.png', 3600);# Create bucketcurl -s -X POST "$REACTOR_URL/storage/v1/buckets" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"name":"avatars","public":false}'
# Upload objectcurl -s -X PUT "$REACTOR_URL/storage/v1/buckets/avatars/objects/user-123.png" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: image/png" \ --data-binary @avatar.png
# Generate signed URLcurl -s -X POST "$REACTOR_URL/storage/v1/buckets/avatars/objects/user-123.png/sign" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"ttl_secs":3600,"action":"read"}'How-to guides
Section titled “How-to guides”List objects with a prefix
Section titled “List objects with a prefix”reactor storage list avatars --prefix uploads/ --limit 100const { objects } = await reactor.storage .from('avatars') .list('uploads/', { limit: 100 });curl -s "$REACTOR_URL/storage/v1/buckets/avatars/objects?prefix=uploads/&limit=100" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme"Upload a large file with multipart
Section titled “Upload a large file with multipart”For files over 5 MiB, use multipart upload. Minimum part size is 5 MiB except the last part.
reactor storage multipart start backups archive.tar.gzreactor storage multipart upload backups archive.tar.gz --part 1 --file part1.binreactor storage multipart upload backups archive.tar.gz --part 2 --file part2.binreactor storage multipart complete backups archive.tar.gzawait reactor.storage.from('backups').uploadMultipart('archive.tar.gz', file, { partSize: 5 * 1024 * 1024, onProgress: (pct) => console.log(`${pct}%`),});# Initiatecurl -s -X POST "$REACTOR_URL/storage/v1/buckets/backups/objects/archive.tar.gz?uploads" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme"
# Upload parts (repeat for each partNumber)curl -s -X PUT "$REACTOR_URL/storage/v1/buckets/backups/objects/archive.tar.gz?partNumber=1&uploadId=UPLOAD_ID" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ --data-binary @part1.bin
# Completecurl -s -X POST "$REACTOR_URL/storage/v1/buckets/backups/objects/archive.tar.gz?uploadId=UPLOAD_ID" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"parts":[{"part_number":1,"etag":"..."},{"part_number":2,"etag":"..."}]}'Add a bucket policy for tenant isolation
Section titled “Add a bucket policy for tenant isolation”Policies are defined per bucket and scope (read, write, delete).
policy tenant_isolation on bucket "avatars" for read, write, delete using (object.metadata->>'org_id' = auth.org_id()::text);reactor storage policies apply avatars --file policies/avatars.sqlawait reactor.storage.from('avatars').upload('user.png', file, { metadata: { org_id: orgId },});Configuration
Section titled “Configuration”[storage]backend = "fs" # "fs" or "s3"signed_url_ttl_secs = 3600max_object_size = 5368709120 # 5 GiB
# Filesystem backend (local dev, sizes 1–2)[storage.fs]root = "./.reactor/blobs"signed_url_hmac_key = "base64-32-byte-key"
# S3 backend (production, sizes 5–6)[storage.s3]endpoint = "https://s3.amazonaws.com"region = "us-east-1"bucket = "my-reactor-storage"access_key_id = "..."secret_access_key = "..."layout = "single_bucket" # prefix: {org_id}/{bucket}/{key}Environment variables:
| Variable | Default | Description |
|---|---|---|
REACTOR_STORAGE_BIND | 0.0.0.0:8003 | HTTP bind address |
REACTOR_STORAGE_BACKEND | fs | fs or s3 |
REACTOR_STORAGE_FS_ROOT | — | Required for filesystem backend |
REACTOR_STORAGE_S3_BUCKET | — | Required for S3 backend |
REACTOR_STORAGE_MAX_OBJECT_SIZE | 5 GiB | Per-object upload cap |
Limits and quotas
Section titled “Limits and quotas”| Limit | Value | Notes |
|---|---|---|
| Max object size | 5 GiB (default) | Configurable via max_object_size |
| Max object size (absolute) | 5 TiB | With multipart upload |
| Min multipart part size | 5 MiB | Except last part |
| Max parts per upload | 10,000 | S3-compatible limit |
| Max key length | 1,024 characters | No .., null bytes, or control chars |
| Bucket name length | 3–63 characters | Lowercase alphanumeric + hyphens |
| Signed URL default TTL | 1 hour | Configurable per request |
| Public bucket reads | Anonymous allowed | No JWT required for GET |
Permission scheme:
| Permission | Scope |
|---|---|
storage:bucket:create | Create buckets |
storage:{bucket}:read | Read objects |
storage:{bucket}:write | Upload objects |
storage:{bucket}:delete | Delete objects |
storage:{bucket}:admin | Update/delete bucket |
storage:*:* | Full storage access |
API and SDK links
Section titled “API and SDK links”- HTTP base path:
/storage/v1/ - OpenAPI reference: Storage API
- JavaScript SDK:
reactor.storage - CLI:
reactor storage - Guide: File uploads
| Method | Path | Description |
|---|---|---|
POST | /storage/v1/buckets | Create bucket |
PUT | /storage/v1/buckets/{bucket}/objects/{*key} | Upload object |
GET | /storage/v1/buckets/{bucket}/objects/{*key} | Download object |
HEAD | /storage/v1/buckets/{bucket}/objects/{*key} | Object metadata |
DELETE | /storage/v1/buckets/{bucket}/objects/{*key} | Delete object |
POST | /storage/v1/buckets/{bucket}/objects/{*key}/sign | Generate signed URL |
Troubleshooting
Section titled “Troubleshooting”bucket_not_found or object_not_found
Section titled “bucket_not_found or object_not_found”Verify bucket name spelling and org context. Buckets are scoped to the active org—switch with X-Reactor-Org if needed.
policy_denied on upload
Section titled “policy_denied on upload”The caller lacks storage:{bucket}:write or a bucket policy blocked the operation. Check object metadata against policy expressions.
signature_invalid or signature_expired
Section titled “signature_invalid or signature_expired”Signed URLs include exp and sig query parameters. Regenerate if expired. During key rotation, both current and previous HMAC keys are accepted for a transition window.
409 bucket_not_empty
Section titled “409 bucket_not_empty”Deleting a bucket with objects requires ?cascade=true or delete all objects first.
Multipart upload stuck
Section titled “Multipart upload stuck”Abandoned multipart uploads accumulate until cleaned up (v0.2 sweeper). Abort explicitly:
curl -s -X DELETE "$REACTOR_URL/storage/v1/buckets/backups/objects/archive.tar.gz?uploadId=UPLOAD_ID" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme"