This error occurs when attempting to access Supabase Storage without proper Row-Level Security (RLS) policies or valid authentication. The fix involves creating appropriate RLS policies on the storage.objects table and ensuring your client sends a valid JWT token.
When you try to upload, download, or manipulate files in Supabase Storage, the request goes through Row-Level Security (RLS) policies on the `storage.objects` and `storage.buckets` tables. By default, Supabase Storage does not allow any operations without explicit RLS policies that grant permission. The "You are not authorized" error means one of two things is happening: 1. No RLS policy exists that allows your user (or anonymous users) to perform the requested operation 2. You're not authenticated, or your JWT token is invalid/expired, and no policy exists for anonymous access This is a security-by-default feature—Supabase requires you to explicitly define who can access what in your storage buckets. Unlike some storage solutions that default to "open," Supabase defaults to "closed" until you create policies.
First, check whether your bucket is public or private. This affects what RLS policies you need:
-- Check bucket privacy setting
SELECT name, public
FROM storage.buckets
WHERE name = 'your-bucket-name';Public buckets: Anyone can read files if a SELECT policy allows it (no authentication required)
Private buckets: Require authentication and SELECT policy to read files
You can change bucket privacy in the Supabase Dashboard under Storage → Buckets → (bucket) → Settings → Public bucket toggle.
View current policies on the storage.objects table to see what's already configured:
-- View all policies on storage.objects
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE tablename = 'objects' AND schemaname = 'storage';If no policies exist for your bucket, or the operations you need aren't covered, you'll need to create them.
To allow authenticated users to upload files to their own folder:
-- Allow authenticated users to upload files
CREATE POLICY "Users can upload their own files"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'your-bucket-name'
AND auth.uid()::text = (storage.foldername(name))[1]
);This policy allows INSERT operations only when:
- The user is authenticated
- The file is going into a folder matching their user ID
- The bucket is 'your-bucket-name'
For allowing uploads to any path:
CREATE POLICY "Authenticated users can upload"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'your-bucket-name');To allow users to read files (download or list):
For public read access:
CREATE POLICY "Public files are readable by anyone"
ON storage.objects
FOR SELECT
TO public
USING (bucket_id = 'your-bucket-name');For authenticated users to read their own files:
CREATE POLICY "Users can read their own files"
ON storage.objects
FOR SELECT
TO authenticated
USING (
bucket_id = 'your-bucket-name'
AND auth.uid()::text = (storage.foldername(name))[1]
);For private buckets with signed URLs:
CREATE POLICY "Authenticated users can read files"
ON storage.objects
FOR SELECT
TO authenticated
USING (bucket_id = 'your-bucket-name');To allow users to update or delete their files:
-- Allow users to update their own files
CREATE POLICY "Users can update their own files"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
bucket_id = 'your-bucket-name'
AND auth.uid()::text = (storage.foldername(name))[1]
)
WITH CHECK (
bucket_id = 'your-bucket-name'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Allow users to delete their own files
CREATE POLICY "Users can delete their own files"
ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'your-bucket-name'
AND auth.uid()::text = (storage.foldername(name))[1]
);Note: The upsert functionality requires SELECT, INSERT, and UPDATE policies all to be present.
Ensure your client code is sending the authentication token:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key' // Use anon key, not service_role
)
// Sign in first
const { data: authData, error: authError } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
})
// Now storage operations will include the JWT automatically
const { data, error } = await supabase.storage
.from('your-bucket-name')
.upload('user-folder/file.jpg', file)
if (error) {
console.error('Upload error:', error)
}The Supabase client automatically attaches the JWT to Storage API requests after authentication.
Verify your policies work by testing in the Supabase Dashboard:
1. Go to Storage → Select your bucket
2. Try uploading a file
3. If it works in the Dashboard but not in your app, the issue is likely with your client authentication
Use the SQL Editor to test policies directly:
-- Test if current user can insert
SELECT * FROM storage.objects
WHERE bucket_id = 'your-bucket-name';
-- Check current user context
SELECT auth.uid(), auth.role();### Using Service Role Key (Backend Only)
The service_role key bypasses ALL RLS policies and should never be exposed to the client. Use it only in server-side code:
// Server-side only (Next.js API route, etc.)
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY, // Server env var
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
// This bypasses RLS entirely
const { data, error } = await supabaseAdmin.storage
.from('bucket')
.upload('path/file.jpg', file)### Custom JWT Claims
If your RLS policies check custom claims:
CREATE POLICY "Users with admin role"
ON storage.objects
FOR SELECT
TO authenticated
USING (
bucket_id = 'admin-files'
AND (auth.jwt() ->> 'user_role') = 'admin'
);Ensure the claim exists in your JWT. You can inspect tokens at [jwt.io](https://jwt.io).
### Bucket-Level Policies
In addition to storage.objects policies, you may need policies on storage.buckets for bucket creation:
CREATE POLICY "Users can create buckets"
ON storage.buckets
FOR INSERT
TO authenticated
WITH CHECK (true);### Testing Policies Locally
When using Supabase locally with Docker:
# Start local Supabase
supabase start
# Apply migrations including RLS policies
supabase db reset### Common Policy Patterns
Allow uploads only to user's folder:
auth.uid()::text = (storage.foldername(name))[1]Allow uploads with specific file extensions:
(storage.extension(name)) IN ('jpg', 'png', 'pdf')Limit file size (requires additional checks in your application):
RLS doesn't support file size checks. Implement this in your application layer.
### Debugging Tips
1. Check the PostgreSQL logs in Supabase Dashboard → Logs → Postgres
2. Use EXPLAIN to see if your policy is being evaluated
3. Temporarily create an overly permissive policy to isolate the issue:
CREATE POLICY "temporary_debug_policy"
ON storage.objects
FOR ALL
TO authenticated
USING (true)
WITH CHECK (true);If this works, your issue is policy logic, not authentication. Remember to delete this policy after debugging.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists