Decision tree
| Scenario | Use |
|---|---|
| Server-side upload (Node, Python backend, etc.) | Simple POST with the Service Role Key |
| Browser uploads under 50MB, public bucket | Direct POST with the Anon Key |
| Browser uploads under 50MB, private bucket | Same — RLS on storage.objects gates it |
| Files over 50MB | TUS resumable at /storage/v1/upload/resumable |
| Untrusted client, bandwidth on your backend matters | Signed upload URL (server mints, client uploads to S3-style URL) |
Simple upload from a backend
The most basic flow. Server has the Service Role Key, picks a path, sends the bytes.- The URL is
/storage/v1/object/{bucket}/{path}. The bucket must already exist (POST /storage/v1/bucketif not). Content-Typeis what the bucket’s MIME allowlist (if any) checks against.- Use
POSTfor new objects,PUTto overwrite an existing object at the same path.POSTto an existing path returns409 Duplicate. - The path can include slashes — they’re stored verbatim and let you organize “folders” client-side.
Browser-direct upload (with the Anon Key)
For uploads from a signed-in user’s browser, you don’t want to proxy the file through your backend — that doubles your bandwidth and adds latency. Have the browser upload directly to Storage with the Anon Key + the user’s access token. RLS onstorage.objects decides whether the upload is allowed.
storage.objects for this to work (assuming a bucket called avatars and a path convention of <user-id>/...):
storage.foldername() is a helper that splits the path by / and returns the segments — (storage.foldername(name))[1] is the first segment. The policy above lets users upload only to paths starting with their own user id. For more patterns, see Storage policies.
Signed upload URLs
For high-bandwidth cases (large files, many concurrent uploaders) you might not want every upload to go through the Storage API — you can have your server mint a signed URL that lets the client upload directly to S3 (well, to Storage at a URL that Storage forwards to S3 without re-auth’ing). This is a two-step flow: Step 1 (server): mint the signed URL.{ "url": "/object/upload/sign/documents/user-123/big-file.pdf?token=..." }. The token in the URL is single-use and TTL-bounded (default 2 hours, configurable per request via ?expires_in=).
Step 2 (client): PUT the file to the signed URL.
Authorization or apikey — the signature in the URL is the auth. This lets you decouple the upload from your auth layer (e.g., for offline clients that need to upload when reconnected).
TUS resumable uploads (for files larger than 50MB)
For uploads that might fail mid-stream — large files on flaky connections, mobile users, video editors — use the TUS protocol at/storage/v1/upload/resumable. TUS chunks the upload, persists progress server-side, and lets the client pick up where it left off after a disconnect.
The protocol itself is involved (PATCH requests with offset headers, HEAD to query progress, etc.). Use a TUS client library rather than implementing it by hand:
apikey + Authorization) on every chunk request. The TUS client library handles this.
Metadata: the bucketName, objectName, and contentType fields in metadata tell Storage where to put the file. The TUS library base64-encodes them into the Upload-Metadata header.
Downloading
For reads, use one of three URL shapes depending on the bucket’s visibility and your auth model:Common failure modes
413 Payload Too Large— the file (or chunk in TUS) exceeds 50MB. Use TUS with a smaller chunk size.409 Duplicate— POSTing to a path that already exists. UsePUTto overwrite or set thex-upsert: trueheader.401 Unauthorized— missing or wrongAuthorizationheader. Check that you’re sending bothapikeyandAuthorization, and that they match what the operation needs (Anon Key for client-side; Service Role for server-side; user access token for user-scoped operations).400 invalid_mime_type— theContent-Typedoesn’t match the bucket’sallowed_mime_types. Either fix the header or widen the bucket’s allowlist.403 Forbidden— RLS onstorage.objectsdenied the operation. Check your INSERT/UPDATE/SELECT policies match the path pattern your client is using.- TUS uploads stall, no error. The TUS client is retrying invisibly. Check the network tab — if you see repeated PATCH requests with 5xx responses, the issue is server-side (probably an RLS denial on UPDATE, since TUS PATCHes the same row). If you see PATCH requests with 200 responses but no progress, the client isn’t sending the next chunk — check your
onProgresshandler isn’t throwing.
Next steps
Storage policies
RLS patterns on storage.objects — own-files, public-read, role-based access.
Storage model
Buckets vs objects, public vs private, the S3 prefix, MIME constraints.
Storage Reference
Full /storage/v1/* endpoint catalog.
Auth: Signup, signin, magic link
How to get the user access token you’ll use in browser-direct uploads.