Skip to main content

Recipes

This page provides common Postgres-Native Cloud Storage recipes. Each recipe includes a bucket contract, path design, RLS policies, and SDK examples.

User avatars

Bucket contract

Bucket: avatars
Purpose: user avatars
Path template: <uid>/avatar.png
Read: signed-in users can read, or public-read depending on business needs
Upload: only the owner can upload to their <uid>/ folder
Overwrite: only the owner can overwrite their avatar
Delete: only the owner can delete their avatar
Limits: 5 MB; image/png, image/jpeg, image/webp

Create bucket

INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
false,
5 * 1024 * 1024,
ARRAY['image/png', 'image/jpeg', 'image/webp']
);

RLS policies

CREATE POLICY avatars_select ON storage.objects
FOR SELECT TO authenticated
USING (bucket_id = 'avatars');

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

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

CREATE POLICY avatars_delete_own ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

SDK

const bucket = app.storage.from('avatars');
const uid = loginState.user.id;

const { error } = await bucket.upload(`${uid}/avatar.png`, file, {
contentType: file.type || 'image/png',
upsert: true,
});

if (error) {
throw error;
}

const { data } = await bucket.createSignedUrl(`${uid}/avatar.png`, 600);
console.log(data.fullSignedURL);

Private user files

Bucket contract

Bucket: private-files
Purpose: private user attachments
Path template: <uid>/<filename>
Read: only owner
Upload: only owner to their <uid>/ folder
Delete: only owner
Public access: no

RLS policies

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

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

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

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

SDK

const bucket = app.storage.from('private-files');
const uid = loginState.user.id;

await bucket.upload(`${uid}/resume.pdf`, file, {
contentType: 'application/pdf',
});

const list = await bucket.list(uid, { limit: 20, withDelimiter: true });
console.log(list.data?.objects);

const signed = await bucket.createSignedUrl(`${uid}/resume.pdf`, 300);
console.log(signed.data?.fullSignedURL);

Team files

Bucket contract

Bucket: team-files
Purpose: team collaboration attachments
Path template: <team_id>/<filename>
Read: team members
Upload: team members
Delete: team admins
Business table: public.team_members(team_id, user_id, role)

Business table

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

GRANT SELECT ON public.team_members TO authenticated;

RLS policies

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()
)
);

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()
)
);

CREATE POLICY team_files_delete_admin 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'
)
);

SDK

const bucket = app.storage.from('team-files');
const teamId = 'team_001';

await bucket.upload(`${teamId}/design.pdf`, file, {
contentType: 'application/pdf',
});

const { data } = await bucket.list(teamId, {
limit: 50,
withDelimiter: true,
});

console.log(data.objects);

Article cover image

Bucket contract

Bucket: article-assets
Purpose: article covers and inline images
Path template: articles/<article_id>/<filename>
Read: everyone can read public articles; only author can read private articles
Upload: article author
Business table: public.articles(id, owner_id, is_public, cover_key)

RLS policy

CREATE POLICY article_assets_select ON storage.objects
FOR SELECT TO anon, authenticated
USING (
bucket_id = 'article-assets'
AND EXISTS (
SELECT 1 FROM public.articles a
WHERE a.cover_key = storage.objects.name
AND (a.is_public OR a.owner_id = auth.uid())
)
);

For uploads, choose a flow based on your business model: upload through a backend after the article exists, or upload to a temporary folder first and clean it up later.

How to describe a scenario to AI

I want to implement: team files
Bucket: team-files
Path template: <team_id>/<filename>
Auth: Postgres-Native auth; user ID is auth.uid()
Business table: public.team_members(team_id, user_id, role)
Permissions: team members can upload and read; only role = admin can delete
SDK: Web app uses app.storage.from('team-files')
Please generate: bucket SQL, RLS policies, upload/list/delete examples, and authorization test cases