Django Portfolio Journal

Version 2: Debugging Supabase S3 integration: errors I hit


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.

Error 1: SignatureDoesNotMatch

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.

Error 2: AccessDenied / Missing signature

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.

Error 3: https://https// — the server restart problem

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.


© 2026 Dorothea Reher  ·  Impressum  ·  Privacy Policy
Designed by BootstrapMade and modified by Dorothea Reher