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