Mockly

templates

Make a bucket private + serve files with signed URLs

A template to make Supabase Storage buckets private, prevent listing/enumeration, and deliver downloads using short-lived signed URLs from your backend. This template is designed to be applied safely: you’ll get copy‑paste steps, variations, and a verification checklist so you know the fix is real.

What “Make a bucket private + serve files with signed URLs” solves (and when it matters)

Stops public downloads and filename enumeration by making buckets private and controlling access through server-side signing.

Use this whenever user uploads include anything sensitive (invoices, documents, exports) or when filenames are predictable.

Before you apply “Make a bucket private + serve files with signed URLs” (safety prerequisites)

  • List every place your frontend currently calls Supabase directly for this surface (table, Storage, or RPC).
  • Decide the backend endpoint(s) you will add so the app still works after you revoke client access.
  • Confirm which environment you’re changing (dev/staging/prod) and plan to repeat the same verification in each.
  • If this is production: plan a staged rollout (feature flag, canary) so you can roll back app behavior quickly without reopening exposure.

Download / copy‑paste instructions

Apply the bucket privacy SQL in Supabase SQL Editor (or toggle in Dashboard if available). Then implement a server route that validates ownership and returns a signed URL.

Template variations (choose the safest one that fits)

Private bucket + short expiration

When to use: General case for downloads that should not remain accessible for long.

-- Ensure bucket is private
UPDATE storage.buckets SET public = false WHERE id = 'invoices';
-- Revoke any public listing policies (review policies separately)

One-time share links

When to use: When you want sharing but still need expiration and auditability.

-- Keep bucket private; implement server-side signed URLs with a 5–10 minute TTL
-- Store access logs for share events in your app

Implementation steps (practical)

  1. Identify buckets that store user or business-sensitive data.
  2. Set buckets to private and remove public/list permissions.
  3. Standardize filenames to non-guessable identifiers (UUIDs).
  4. Implement a backend endpoint that checks user ownership for the requested object path.
  5. Generate signed URLs with short expiration (minutes).
  6. Add rate limiting to prevent signing abuse and enumeration attempts.
  7. Re-test downloads from anon/auth; direct access should fail without a signed URL.

Verification checklist for “Make a bucket private + serve files with signed URLs” (make sure the fix is real)

  1. Attempt to list objects without a backend: it should fail.
  2. Attempt to download an object without a signed URL: it should fail.
  3. Verify signed URLs work only until expiration.
  4. Confirm object names are not predictable (no incremental IDs).
  5. Confirm your backend endpoint refuses access for non-owners.
  6. Re-scan and ensure storage exposure issues disappear.
  7. Monitor for repeated signing attempts and block abusive clients.

Common pitfalls (what breaks in real projects)

  • Applying SQL but leaving the frontend calling Supabase directly (the exposure remains).
  • Locking down one surface (tables) but forgetting Storage/RPC exposure.
  • Fixing in dev but not re-checking staging/prod (config drift).
  • Not adding verification steps, so regressions ship unnoticed.
  • Treating service_role like a convenience key instead of a server-only secret.

Rollback plan for “Make a bucket private + serve files with signed URLs” (without reopening exposure)

If the app breaks after you lock down direct access, the safest rollback is to restore application behavior (backend endpoints) — not to re‑grant direct client access.

A practical sequence:

  1. Restore or hotfix the backend endpoint to return the needed data for authorized users.
  2. Add temporary logging for denied direct access so you can spot missing code paths.
  3. Only if absolutely necessary: create a narrowly scoped, time-limited policy for a specific table/bucket/function and remove it after the backend path is fixed.

Related glossary terms

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

Related integrations

  • Supabase Storage signed URLs → /integrations/supabase-storage-signed-urls

FAQ

Will this template break my app?

It can, because it removes direct client access. That’s intentional: you’re moving reads/writes to a backend layer. Use the implementation steps to keep user-facing features working through secure endpoints.

Why does Mockly recommend a zero-policy (deny-by-default) posture?

It’s the simplest posture to reason about: if the browser can’t access tables/RPC/storage directly, a whole class of policy mistakes disappears. You can still add carefully scoped policies later if you intentionally want client access.

How do I verify I didn’t just hide the problem in the UI?

Run direct queries using the same client credentials your app ships. If the database denies access and only your backend endpoints work, your fix is effective.

Next step

Need help finding which tables/buckets/functions are exposed in your project? Run a Mockly scan and jump straight to the templates linked from each issue.

Explore related pages

parent

Storage Safety templates

/templates/storage-safety

sibling

Lock down a public table (backend-only access)

/templates/access-control/lock-down-public-table

sibling

Remove over-permissive RLS policies (adopt deny-by-default)

/templates/access-control/remove-over-permissive-policies

cross

Supabase Storage Bucket Privacy

/glossary/supabase-storage-bucket-privacy

cross

Signed URLs

/glossary/signed-urls

cross

Pricing

/pricing