Supabase Storage uses PostgreSQL Row Level Security (RLS) to control access to files. This provides fine-grained control over who can upload, download, and delete files.
-- Allow users to upload to their own foldercreate policy "Users can upload to own folder"on storage.objectsfor insertwith check ( bucket_id = 'user-files' and (storage.foldername(name))[1] = (select auth.uid()::text));-- Allow users to read their own filescreate policy "Users can read own files"on storage.objectsfor selectusing ( bucket_id = 'user-files' and (storage.foldername(name))[1] = (select auth.uid()::text));-- Allow users to delete their own filescreate policy "Users can delete own files"on storage.objectsfor deleteusing ( bucket_id = 'user-files' and (storage.foldername(name))[1] = (select auth.uid()::text));
-- Create team_members tablecreate table team_members ( team_id uuid references teams(id), user_id uuid references auth.users(id), role text, primary key (team_id, user_id));-- Folder structure: team-files/{team-id}/document.pdfcreate policy "Team members can access team files"on storage.objectsfor selectusing ( bucket_id = 'team-files' and (storage.foldername(name))[1]::uuid in ( select team_id::text from team_members where user_id = (select auth.uid()) ));-- Only team admins can uploadcreate policy "Team admins can upload"on storage.objectsfor insertwith check ( bucket_id = 'team-files' and (storage.foldername(name))[1]::uuid in ( select team_id::text from team_members where user_id = (select auth.uid()) and role = 'admin' ));
-- Only allow uploads during business hourscreate policy "Business hours uploads only"on storage.objectsfor insertwith check ( bucket_id = 'documents' and extract(hour from now()) between 9 and 17 and extract(dow from now()) between 1 and 5);
-- Users can upload/update their avatarcreate policy "Users can manage own avatar"on storage.objectsfor allusing ( bucket_id = 'avatars' and name = concat((select auth.uid()::text), '.png'));
// Create signed URL that expires in 1 hourconst { data, error } = await supabase .storage .from('private-documents') .createSignedUrl('confidential.pdf', 3600)if (error) { console.error('Error creating signed URL:', error)} else { console.log('Signed URL:', data.signedUrl) console.log('Expires at:', data.expiresAt) // Share this URL - it works even if user doesn't have direct access}
-- Users table (already exists in auth.users)-- Photo metadata tablecreate table photos ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users(id), title text, description text, file_path text, is_public boolean default false, created_at timestamp with time zone default now());alter table photos enable row level security;-- Users can view their own photoscreate policy "Users can view own photos"on photos for selectusing ( user_id = (select auth.uid()) );-- Users can view public photoscreate policy "Anyone can view public photos"on photos for selectusing ( is_public = true );-- Storage policies for photos bucket-- Users can upload to their foldercreate policy "Users can upload photos"on storage.objectsfor insertwith check ( bucket_id = 'photos' and (storage.foldername(name))[1] = (select auth.uid()::text));-- Users can read their own photoscreate policy "Users can read own photos"on storage.objectsfor selectusing ( bucket_id = 'photos' and (storage.foldername(name))[1] = (select auth.uid()::text));-- Users can read public photoscreate policy "Anyone can read public photos"on storage.objectsfor selectusing ( bucket_id = 'photos' and name in ( select file_path from photos where is_public = true ));-- Users can delete their own photoscreate policy "Users can delete own photos"on storage.objectsfor deleteusing ( bucket_id = 'photos' and (storage.foldername(name))[1] = (select auth.uid()::text));