/storage/v1/* on your project URL. It manages files in buckets backed by S3, with object metadata mirrored into the storage.buckets and storage.objects tables in Postgres so RLS can gate access.
For the conceptual model, see Storage model. For uploading, see Storage uploads. For RLS, see Storage policies.
Common headers
Authorization is the right pattern for browser-direct uploads. Service Role in both is for server-side admin operations.
The Storage API uses bearer auth for most endpoints. Public-URL reads (/object/public/*) work without auth; signed-URL reads pass auth via a ?token= query parameter.
Buckets
A bucket is a top-level container with a name, public flag, optional MIME allowlist, and optional file-size override.POST /storage/v1/bucket
Create a new bucket.Bucket id (used in URLs). Must be unique. Lowercase alphanumeric, dashes, and dots.
Human-readable name. Defaults to
id.When
true, the bucket’s public URL (/object/public/{bucket}/{path}) returns files without auth. Default false.Per-bucket override for the project’s default file size limit. In bytes.
Restrict uploads to specific Content-Types. Default null = anything.
{ "name": "avatars" } on 200.
GET /storage/v1/bucket
List all buckets. Returns the bucket metadata rows fromstorage.buckets, filtered by SELECT policies.
{ id, name, public, owner, created_at, updated_at, file_size_limit, allowed_mime_types }.
GET /storage/v1/bucket/
Get a specific bucket’s metadata.PUT /storage/v1/bucket/
Update bucket metadata. Body fields are the same as create (public, file_size_limit, allowed_mime_types).
DELETE /storage/v1/bucket/
Delete an empty bucket. Returns409 not_empty if the bucket contains any objects — empty it first via POST /bucket/{id}/empty or DELETE the objects individually.
POST /storage/v1/bucket//empty
Delete all objects in a bucket without removing the bucket itself. Returns200 OK.
Objects
The core file operations. Object endpoints are routed by bucket + path.POST /storage/v1/object//
Upload a new file. The path can include slashes — they’re stored verbatim, not interpreted as folders by Storage. Returns409 Duplicate if a file already exists at that path; use PUT or set the x-upsert: true header to overwrite.
Headers:
Content-Type— the MIME type of the file. Checked against the bucket’sallowed_mime_typesif set.x-upsert: true(optional) — overwrite if exists.
{ "Id": "...", "Key": "avatars/user-123/avatar.png" }.
PUT /storage/v1/object//
Overwrite an existing file at the same path. Same body shape as POST. Returns200 OK.
GET /storage/v1/object/public//
Fetch a file from a public bucket. No auth required. Returns the file bytes with the storedContent-Type. Returns 400 if the bucket is not public.
GET /storage/v1/object/authenticated//
Fetch a file from any bucket. RequiresAuthorization: Bearer <access token>. RLS on storage.objects decides whether the request succeeds.
DELETE /storage/v1/object//
Delete a single file. Requires DELETE policy match onstorage.objects for the calling role.
POST /storage/v1/object/list/
List files in a bucket. Filtered by SELECT policies onstorage.objects.
Only return files whose
name starts with this prefix.Max files to return. Default 100.
Pagination offset.
{ column: "name" | "created_at" | "updated_at", order: "asc" | "desc" }.Substring search on
name. Case-insensitive.{ id, name, bucket_id, owner, created_at, updated_at, last_accessed_at, metadata }.
POST /storage/v1/object/copy
Copy a file to a new location (potentially in a different bucket).Source bucket.
Source path.
Destination bucket. Defaults to source bucket.
Destination path.
POST /storage/v1/object/move
Move a file to a new location. Same parameters as copy; the source is deleted on success.Signed URLs
For sharing files via TTL-bounded URLs that work without auth.POST /storage/v1/object/sign//
Mint a download signed URL.URL TTL in seconds. Default 60. Max 604800 (7 days).
{ "signedURL": "/object/sign/documents/report.pdf?token=..." }. The URL is path-relative — prepend your BASE_URL to get the full URL.
GET /storage/v1/object/sign//?token=…
Fetch a file via a signed URL. The token is the signature; no other auth required.POST /storage/v1/object/upload/sign//
Mint an upload signed URL — your server creates the URL, the client PUTs the file directly to it. Useful for high-bandwidth flows or offline clients.URL TTL in seconds. Default 7200 (2 hours).
{ "url": "/object/upload/sign/documents/report.pdf?token=..." }. The client then PUTs to <BASE_URL>{url} with the file bytes and Content-Type header.
POST /storage/v1/object/sign
Sign multiple URLs in one request. Body:{ "paths": ["bucket1/path1", "bucket2/path2"], "expiresIn": 3600 }. Returns an array of signed URLs.
Image transformations
Therender/image endpoints route through imgproxy. The path shape mirrors the object endpoints — public, authenticated, sign — with the same auth requirements.
GET /storage/v1/render/image/public//
Fetch a transformed image from a public bucket. Pass transformations as query parameters.Output width in pixels.
Output height in pixels.
cover, contain, or fill. Default cover.JPEG/WebP quality, 0-100. Default 80.
webp, png, jpeg, or origin. Default origin (preserves the original format).GET /storage/v1/render/image/authenticated//
Same as above but auth-gated.GET /storage/v1/render/image/sign//?token=…
Same as above via signed URL. To mint a signed transformation URL, hitPOST /storage/v1/object/sign/{bucket}/{path} with a transform parameter in the body:
TUS resumable uploads
For files larger than 50MB (the per-request limit). Implements the TUS 1.0 protocol. Use a TUS client library rather than hand-rolling.POST /storage/v1/upload/resumable
Initiate a resumable upload. SubsequentPATCH requests with Upload-Offset headers stream the chunks. HEAD requests query progress.
Required headers:
apikeyAuthorization: Bearer <token>Upload-Length: <total bytes>Upload-Metadata: bucketName <base64>,objectName <base64>,contentType <base64>Tus-Resumable: 1.0.0
tus-js-client.
Error responses
Storage returns errors as{"statusCode": "<code>", "error": "<machine-readable>", "message": "<human-readable>"}. Common cases:
| Status | Error | When |
|---|---|---|
| 400 | invalid_mime_type | Content-Type not in bucket’s allowed_mime_types |
| 400 | invalid_signature | Signed URL token doesn’t validate (tampered, expired, or wrong key) |
| 401 | Invalid JWT | Missing or expired access token on an authenticated endpoint |
| 403 | not_authorized | RLS denied the operation |
| 404 | not_found | Bucket or object doesn’t exist |
| 409 | Duplicate | POSTing to an existing path (use PUT or x-upsert: true) |
| 409 | not_empty | DELETE bucket with objects still in it |
| 413 | Payload too large | File exceeds the 50MB per-request limit (or bucket override) |
Next steps
Storage uploads
The four upload flows with worked examples.
Storage policies
RLS patterns on storage.objects.
Storage model
Buckets, public vs private, the underlying tables.
Auth Reference
The auth surface that mints the tokens you’ll use here.