How Supabase Storage is used as the cloud media backend for species images, QR codes, documents, and other uploaded files.
The Django backend defaults to local filesystem storage (media/). Supabase Storage is the recommended cloud replacement for production, providing a global CDN, Row Level Security (RLS), and a REST API compatible with Django’s Storage interface.
Use the service role key (SUPABASE_SERVICE_KEY) only on the server — never expose it to the browser or commit it to source control. The anon key is safe for public bucket reads.
The management command migrate_to_supabase uploads all local media files and updates database references:
# Dry run — no changes madepython manage.py migrate_to_supabase --dry-run# Migrate all models with a local backuppython manage.py migrate_to_supabase --backup# Migrate a single modelpython manage.py migrate_to_supabase --model Species --backup
The migration process:
Creates a local backup in media_backup/
Scans all models with file fields
Uploads each file to Supabase, preserving directory structure
Verifies each upload
Updates the database reference to the new Supabase path
Apply these RLS policies in the Supabase SQL editor (SQL Editor → New query):
-- Profile pictures: only the owning user can read or uploadCREATE POLICY "Users can view own profile pictures" ON storage.objectsFOR SELECT USING ( bucket_id = 'user-profiles' AND auth.uid()::text = (storage.foldername(name))[1]);CREATE POLICY "Users can upload own profile pictures" ON storage.objectsFOR INSERT WITH CHECK ( bucket_id = 'user-profiles' AND auth.uid()::text = (storage.foldername(name))[1]);-- Public content: species, exhibitions, educational servicesCREATE POLICY "Public content is viewable by everyone" ON storage.objectsFOR SELECT USING ( bucket_id IN ('species', 'exhibitions', 'educational-services'));-- Documents: authenticated users onlyCREATE POLICY "Authenticated users can view documents" ON storage.objectsFOR SELECT USING ( bucket_id = 'documents' AND auth.role() = 'authenticated');-- Private per-user documentsCREATE POLICY "Users can access own documents" ON storage.objectsFOR ALL USING ( bucket_id = 'documents' AND auth.uid()::text = (storage.foldername(name))[1]);
Verify that SUPABASE_API_KEY in .env is correct. For private bucket operations you may need the service role key (SUPABASE_SERVICE_KEY) instead of the anon key.
Bucket not found
Check that the bucket name in SUPABASE_STORAGE_BUCKET matches exactly what you created in the dashboard (case-sensitive).
403 Forbidden when reading files
The bucket is set to private or an RLS policy is blocking access. Either make the bucket public or add a SELECT policy for the relevant role.
Migration is slow
Use --model to migrate one model at a time, or check your network connection. Large MEDIA_ROOT directories with thousands of files will take several minutes.
# Run the full Supabase storage test suitepython manage.py test tests.test_supabase_storage# Run a specific test casepython manage.py test tests.test_supabase_storage.SupabaseStorageTestCase.test_save_file_success
Test coverage includes upload, download, delete, URL generation, file existence checks, path traversal prevention, and bulk upload performance.