Mockly

conversions

Convert a public Storage bucket to private + signed URLs

A conversion playbook to stop public downloads and move to private buckets with backend-generated signed URLs. This page is written as a utility: clear from/to states, step-by-step logic, and examples so you can apply it without guessing.

From → To (what you’re converting)

From: Bucket is public or listable; objects can be discovered or downloaded directly.

To: Bucket is private; downloads only via short-lived signed URLs after authorization checks.

Safety notes for Convert a public Storage bucket to private + signed URLs (order of operations)

Conversions are safest when you change application call paths before you revoke access.

  1. Add the backend endpoint that will replace the direct Supabase call.
  2. Switch the frontend to call your backend endpoint (feature flag if needed).
  3. Revoke direct client privileges (grants, bucket access, or RPC execute).
  4. Verify: direct access fails; backend endpoint succeeds.

Conversion logic (step-by-step)

  1. List buckets and identify which contain sensitive content.
  2. Make the bucket private (public=false).
  3. Remove any policies that allow object listing or broad reads.
  4. Rotate predictable filenames to UUID-based keys (for legacy objects too).
  5. Implement a backend endpoint that validates ownership before signing URLs.
  6. Generate signed URLs with short expiration and return only the URL to clients.
  7. Verify direct listing/download fails without backend signing.

Example conversions

Example 1

Input: Invoices bucket is public and files are named invoice_1234.pdf.

Output: Bucket is private; files are UUID named; users download via /api/invoices/{id}/download.

Notes: Signed URLs expire quickly and access is audited.

Example 2

Input: Users can list objects in uploads bucket.

Output: Listing fails; backend signs only owned objects.

Notes: Rate limiting blocks brute force signing attempts.

Verification checklist for Convert a public Storage bucket to private + signed URLs

  • Reproduce the risky behavior once before changing anything (so you can prove the fix).
  • After the conversion, the same direct access test fails (401/403).
  • The user-facing feature still works through backend endpoints.
  • A scan/checklist re-run no longer flags the exposure.
  • You recorded the rule so future migrations don’t reintroduce direct access.

Related converters (what to do next)

  • Convert a public table to backend-only access → /conversions/public-table-to-backend-only — A conversion playbook to remove direct client table access and route everything through a secure backend layer.
  • Convert a public RPC to service_role-only → /conversions/public-rpc-to-service-role-only — A conversion playbook to revoke EXECUTE grants from public roles and expose RPC functionality only through backend endpoints.

Related templates

  • Make a bucket private + serve files with signed URLs → /templates/storage-safety/make-bucket-private-signed-urls

Related glossary terms

  • Supabase Storage Bucket Privacy → /glossary/supabase-storage-bucket-privacy
  • Signed URLs → /glossary/signed-urls

Common pitfalls during conversions

  • Revoking access before the backend endpoint is deployed (causes outages and rollback pressure).
  • Leaving one legacy client call path in place (exposure still exists).
  • Assuming “authenticated users” are authorized without ownership/tenancy checks.
  • Fixing only one environment and forgetting staging/prod (drift).

If you must keep some client access during Convert a public Storage bucket to private + signed URLs

Sometimes you can’t move everything server-side immediately. If you keep any client access temporarily, make it an explicit, reviewed exception.

  • Keep the exception narrow: one operation, one resource, one documented reason.
  • Add an expiration date and a follow-up task to remove the exception once the backend path is ready.
  • Add extra verification: tests for cross-user/tenant access and direct access attempts that must fail outside the exception.
  • Prefer least privilege (specific grants/policies) over broad authenticated access.

This keeps “temporary” from becoming permanent drift.

What to document after the conversion (prevents regressions)

  • Which resource you converted (table/bucket/function) and the exact change in access model.
  • Which direct access test proves the conversion worked.
  • Which backend endpoint now owns the privileged operation and what authorization it enforces.
  • Which drift guard you will re-run after migrations.

FAQ

Why is backend-only access recommended in this conversion?

Because it removes direct client access to sensitive resources. That prevents scraping, reduces policy complexity, and makes authorization easier to test and monitor.

Can I keep some client access and still be safe?

Sometimes. But you should only do that intentionally, with carefully scoped policies and strong verification. If you’re unsure, backend-only is the safer default.

What’s the best verification step after this conversion?

Try direct access using client credentials and confirm it fails, then confirm your backend endpoints still work for authorized users.

Next step

Want to know if your current state is actually exposed? Run a Mockly scan first, then apply the conversion that matches the finding.

Explore related pages

parent

Conversions

/conversions

sibling

Convert a public RPC to service_role-only

/conversions/public-rpc-to-service-role-only

sibling

Convert a public table to backend-only access

/conversions/public-table-to-backend-only

cross

Supabase Storage Bucket Privacy

/glossary/supabase-storage-bucket-privacy

cross

Signed URLs

/glossary/signed-urls

cross

Pricing

/pricing