storage.objects table is a regular Postgres table with Row Level Security enabled. Every file operation through /storage/v1/object/authenticated/* runs RLS policies on this table before letting the request through, just like queries against your own public.* tables.
This page is a recipe collection for the three storage-specific patterns that come up most. For the conceptual model, see Storage model. For the general RLS framing, see RLS Model. For uploads, see Storage uploads.
The shape of storage.objects
Before the patterns, what you’re writing policies against:storage.foldername(name)— splits the path by/and returns the segments as atext[]. So for an object atdocuments/2026/q1/report.pdf,storage.foldernamereturns{documents, 2026, q1}(excluding the filename).storage.filename(name)— returns just the filename part. For the same path, returnsreport.pdf.
bucket_id and auth.uid() to express “files in this bucket, in this folder, owned by this user.”
Pattern 1 — Users access only their own files
Most common pattern. Files are keyed by user id as the first path segment (e.g.,avatars/<user-id>/photo.png). Each user can upload, read, update, and delete only their own files.
auth.uid() returns uuid; storage.foldername(name)[1] returns text. The cast auth.uid()::text makes the comparison work — without it, Postgres errors with “operator does not exist.”
Why path-prefix not owner? You could match on owner = auth.uid() instead. The difference is what happens when a user uploads to a path that doesn’t start with their id — owner = auth.uid() blocks it because the owner is set at upload time (so the policy passes by construction), but (storage.foldername(name))[1] = auth.uid()::text blocks it because the path itself is wrong. Path-prefix is stricter; it prevents a user from uploading to <some-other-user-id>/file.png even if they somehow get the owner column set to their own id.
For belt-and-suspenders, check both:
Pattern 2 — Public read, auth-only write
Think of the bucket as a content site: anyone can read any file (the bucket itself ispublic = true, so the read side is free — see the warning below), but only signed-in users can upload, and only to paths they own.
Pattern 3 — Role-based access (admins manage, users read their own)
A team document-sharing app: every user can read and upload their own files (Pattern 1), but admins can read and delete anyone’s files. Using the JWT claim approach (admin is set onapp_metadata.role via PUT /auth/v1/admin/users/{id}):
app_metadata.role = "admin" matches both the per-user policy (for their own files) and the admin policy (for everyone’s). Postgres ORs them; the admin sees everything in the bucket.
Using a database table (public.team_members(user_id, role)) instead — useful when roles change dynamically without re-issuing JWTs:
Listing files in a bucket
The Storage API’sPOST /storage/v1/object/list/{bucket} endpoint runs against storage.objects and applies SELECT policies. Users see only the files they have a SELECT policy for. If you want to surface a “browse all files in this bucket” UI for admins, the admin policy above is what makes it work.
GET /storage/v1/bucket/{id} (list bucket metadata) is gated separately by policies on storage.buckets. By default, signed-in users can see all bucket metadata in their project. If you want to hide bucket existence from non-admins, add policies on storage.buckets:
Folder structure as authorization
If your bucket is organized as<org-id>/<user-id>/<filename>, you can express “users in org X can read anything in their org’s folder” with a multi-segment check:
Testing storage policies
The same techniques from RLS Testing apply —SET LOCAL ROLE authenticated; SET LOCAL request.jwt.claims = '{"sub": "...", "role": "authenticated"}'; and then run your INSERT/SELECT against storage.objects to confirm the policy accepts the right things and rejects the wrong things.
A small detail: when testing storage policies, the path you pass to storage.foldername() matters. To simulate an upload at avatars/<uid>/photo.png, your test row’s name column should be that exact string — the policy reads name, not path or any other column.
Pitfalls
-
Forgetting to enable RLS.
storage.objectshas RLS enabled by default. If you’re testing locally with a fresh schema and policies aren’t applying, check thatALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;ran. Without RLS, every signed-in user sees everything. -
The public URL bypasses RLS. Worth repeating because it confuses people. If your bucket is
public = true, no amount of policy work onstorage.objectswill gateGET /storage/v1/object/public/.... Use private buckets + signed URLs if you need RLS-gated reads. -
storage.foldername()is zero-indexed in some examples online but one-indexed here. PostgreSQL arrays are one-indexed:(storage.foldername('a/b/c.png'))[1]is'a', not'b'. If you copy-paste from older Supabase docs, double-check the index. -
Update policies need both USING and WITH CHECK. Without
WITH CHECK, a user could changename(orowner) to a path they don’t own mid-update. Always pair them. -
auth.uid()returns NULL foranon. A policy likeowner = auth.uid()will never match for anon — which is usually what you want, but means an anon-public-write bucket needs a separate policy targetingTO anonwith whatever conditions you want.
Next steps
Storage uploads
The upload flows these policies gate.
Storage model
Buckets, public vs private, the underlying tables.
RLS Cookbook
The general RLS patterns this page builds on.
RLS Testing
How to test storage policies in psql or the Studio.