The previous article covered the django-storages configuration for Supabase. What it did not cover is the three separate errors I hit on the way there. Each one had a non-obvious root cause.
The first error appeared when running the management command to seed project images:
An error occurred (SignatureDoesNotMatch) when calling the PutObject operation: The request signature we calculated does not match the signature you provided. Check your key and signing method.
SignatureDoesNotMatch with S3-compatible storage usually comes down to three things: wrong credentials, wrong endpoint URL, or wrong region.
Credentials: Supabase has multiple key types. The anon and service_role keys are in Project Settings → API. The S3 access keys are completely separate — they live in Project Settings → Storage → S3 Access Keys. These are not interchangeable.
Endpoint URL: The correct format is https://<project-ref>.supabase.co/storage/v1/s3. There is no .storage. subdomain.
Region: This was the actual cause. The region name must match exactly what Supabase shows in the S3 Access Keys section — not what you might assume from your project's geographic location. One character off and the signature fails.
With uploads working, the images were still not visible in the browser. Clicking an image link in Django admin returned an XML response instead of an image:
<Error>
<Resource>images/uploads/project/image.jpg</Resource>
<Code>AccessDenied</Code>
<Message>Missing signature</Message>
</Error>
This is a Supabase-specific issue with how django-storages constructs file URLs. Without additional configuration, it builds URLs pointing at the S3 API endpoint:
https://<project-ref>.supabase.co/storage/v1/s3/images/uploads/project/image.jpg
The S3 API endpoint always requires authentication — even for a public bucket. Supabase serves public files through a different path:
https://<project-ref>.supabase.co/storage/v1/object/public/images/uploads/project/image.jpg
The fix is the custom_domain option in the S3Boto3Storage configuration. When set, django-storages uses it as the URL base instead of deriving it from the endpoint:
"custom_domain": config("SUPABASE_S3_CUSTOM_DOMAIN"),
The value in .env must be:
SUPABASE_S3_CUSTOM_DOMAIN=<project-ref>.supabase.co/storage/v1/object/public/images
No https:// prefix — django-storages adds that automatically.
After adding custom_domain, image URLs still rendered incorrectly — this time with a doubled protocol:
https://https//<project-ref>.supabase.co/storage/v1/object/public/images/...
My first assumption was that the .env value still contained https://. I checked the file — it did not. I opened a new terminal and tried again. Same result.
The cause: Django's development server does not reload when .env files change. It watches Python files and restarts automatically, but .env is not a Python file. python-decouple reads it once at startup and caches the values in memory. Changing .env while the server is running has no effect until the process is restarted.
The fix: stop the server, restart it. After restart, django-storages picked up the corrected value and generated the right URL.
The lesson is worth keeping: any tool that reads configuration at startup — python-decouple, Django settings, a connection pool — requires a process restart to pick up changes, regardless of how many new terminals you open.