Skip to main content

RLS policy patterns

This page collects 8 common RLS permission patterns for Postgres-Native cloud storage. You can copy and paste them straight into your project. Naming aligns with PostgreSQL database — RLS policy patterns on purpose — the same mental model applies to both business tables and cloud storage.

Common prerequisites

All the templates below assume:

  • The bucket already exists (see Quick experience).
  • RLS is already enabled on storage.objects / storage.buckets (handled by initialization).
  • You do not need to GRANT ... ON storage.objects — the three roles already have ALL, and the only gate is RLS.
  • auth.uid() returns the current user ID; see Authentication.

1. PUBLIC — fully open

Use cases: static site assets, public artwork, image hosting served via CDN.

CREATE POLICY public_select ON storage.objects
FOR SELECT TO anon, authenticated
USING (bucket_id = 'public-assets');

CREATE POLICY public_insert ON storage.objects
FOR INSERT TO anon, authenticated
WITH CHECK (bucket_id = 'public-assets');
Operationanonauthenticatedservice_role
SELECT
INSERT
UPDATE
DELETE

In practice you usually require sign-in for INSERT.


2. READONLY — everyone reads, only uploader writes

Use cases: UGC images / videos — everyone sees them, only the author can change them.

-- Anyone can read
CREATE POLICY readonly_select ON storage.objects
FOR SELECT TO anon, authenticated
USING (bucket_id = 'ugc');

-- Signed-in users can upload; owner_id must equal themselves (the Storage API injects it)
CREATE POLICY readonly_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'ugc' AND owner_id = auth.uid());

-- Only the owner can update
CREATE POLICY readonly_update ON storage.objects
FOR UPDATE TO authenticated
USING (bucket_id = 'ugc' AND owner_id = auth.uid())
WITH CHECK (bucket_id = 'ugc' AND owner_id = auth.uid());

-- Only the owner can delete
CREATE POLICY readonly_delete ON storage.objects
FOR DELETE TO authenticated
USING (bucket_id = 'ugc' AND owner_id = auth.uid());
Operationanonauthenticated (owner)authenticated (other)service_role
SELECT
INSERT✅ (own only)
UPDATE
DELETE

3. PRIVATE — only the uploader can read/write

Use cases: private files (resumes, personal backups, personal cloud drives).

CREATE POLICY private_select ON storage.objects
FOR SELECT TO authenticated
USING (bucket_id = 'private' AND owner_id = auth.uid());

CREATE POLICY private_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'private' AND owner_id = auth.uid());

CREATE POLICY private_update ON storage.objects
FOR UPDATE TO authenticated
USING (bucket_id = 'private' AND owner_id = auth.uid())
WITH CHECK (bucket_id = 'private' AND owner_id = auth.uid());

CREATE POLICY private_delete ON storage.objects
FOR DELETE TO authenticated
USING (bucket_id = 'private' AND owner_id = auth.uid());
Operationanonauthenticated (owner)authenticated (other)service_role
All

4. ADMINWRITE — everyone reads, only admins write

Use cases: official asset libraries, icon sets, operational banners.

CREATE POLICY adminwrite_select ON storage.objects
FOR SELECT TO anon, authenticated
USING (bucket_id = 'official');

-- No INSERT/UPDATE/DELETE policy → deny by default for anon / authenticated
-- service_role has BYPASSRLS; the backend writes via an API Key
Operationanonauthenticatedservice_role
SELECT
INSERT/UPDATE/DELETE

5. ADMINONLY — only admins read/write

Use cases: sensitive credentials, archived backups, internal-only material.

-- No policies at all → everything denied
-- Only service_role (BYPASSRLS) can read/write; backend uses an API Key
Operationanonauthenticatedservice_role
All

6. Path-prefix isolation (one folder per user)

Use cases: user avatars, personal cloud drives, IM attachments — all users share one bucket but are physically separated by <uid>/.

Convention: object keys look like <uid>/<filename> or deeper, <uid>/<sub-dir>/....

CREATE POLICY user_folder_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY user_folder_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY user_folder_update ON storage.objects
FOR UPDATE TO authenticated
USING (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
)
WITH CHECK (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
);

CREATE POLICY user_folder_delete ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
);
Performance tip

path_tokens is a generated column and can be indexed. For high-throughput access:

CREATE INDEX idx_objects_user_folder
ON storage.objects (bucket_id, (path_tokens[1]));

You can also write path_tokens[1] = auth.uid(), equivalent to (storage.foldername(name))[1] but friendlier to the index.


7. Join business tables (team / multi-tenant)

Use cases: team drives, multi-tenant SaaS — file ownership is dictated by a business table team_members.

Convention: object keys look like <team_id>/<filename>.

-- Business table
CREATE TABLE public.team_members (
team_id text NOT NULL,
user_id text NOT NULL,
role text NOT NULL DEFAULT 'member', -- member / admin
PRIMARY KEY (team_id, user_id)
);

-- Team members can read
CREATE POLICY team_files_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(storage.objects.name))[1]
AND tm.user_id = auth.uid()
)
);

-- Team members can upload into their team's folder
CREATE POLICY team_files_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(name))[1]
AND tm.user_id = auth.uid()
)
);

-- Only team admins can delete
CREATE POLICY team_files_delete ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'team-files'
AND EXISTS (
SELECT 1 FROM public.team_members tm
WHERE tm.team_id = (storage.foldername(storage.objects.name))[1]
AND tm.user_id = auth.uid()
AND tm.role = 'admin'
)
);
Cross-schema GRANT

The EXISTS (SELECT 1 FROM public.team_members ...) subquery runs as the current role, so the authenticated role must have at least SELECT on public.team_members:

GRANT SELECT ON public.team_members TO authenticated;

And the RLS on team_members itself must allow "the user reads their own membership rows".


8. Metadata-driven (share links, content tags)

Use cases: temporary share links, visibility controlled by tags in user_metadata.

-- user_metadata looks like {"share_token": "xyz", "expires_at": "2025-06-01T00:00:00Z"}

CREATE POLICY shared_by_token ON storage.objects
FOR SELECT TO anon, authenticated
USING (
bucket_id = 'share'
AND user_metadata ? 'share_token'
AND current_setting('request.headers', true)::json->>'x-share-token'
= user_metadata->>'share_token'
AND (user_metadata->>'expires_at')::timestamptz > now()
);

This reads the HTTP header x-share-token; request.headers is the GUC injected by PostgREST and works the same way in the Postgres-Native gateway.


Abstracting policy expressions

When the same predicate is reused across many policies, wrap it in a function:

-- Is the current user the owner of this object?
CREATE OR REPLACE FUNCTION storage.is_owner(obj_name text, obj_owner_id text)
RETURNS boolean
LANGUAGE sql STABLE
AS $$
SELECT obj_owner_id = auth.uid();
$$;

-- Is the current user a member of the given team?
CREATE OR REPLACE FUNCTION public.is_team_member(p_team_id text)
RETURNS boolean
LANGUAGE sql STABLE
AS $$
SELECT EXISTS (
SELECT 1 FROM public.team_members
WHERE team_id = p_team_id AND user_id = auth.uid()
);
$$;

-- Use them in a policy
CREATE POLICY team_files_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'team-files'
AND public.is_team_member((storage.foldername(name))[1])
);

Decision table

ScenarioRecommended pattern
Landing-page static assets, fonts, icons1. PUBLIC
Social feed photos, UGC walls2. READONLY
Personal note attachments, resumes3. PRIVATE
Official asset library, campaign banners4. ADMINWRITE
Internal backups, sensitive credentials5. ADMINONLY
User avatars, personal cloud drives6. Path-prefix isolation
Team drives, multi-tenant SaaS7. Join business tables
Temporary share links, tag filtering8. Metadata-driven
Article covers, order attachments — "follows the business object"7. Join business tables (variant)

Next steps