TL;DR: Supabase Storage is not covered by your database RLS. Buckets have their own public/private switch and their own policies, and AI builders leave both open. The fix has three parts: private buckets for user files, owner-scoped storage rules, and signed URLs in place of links that never die.

Here is the trap. You locked your tables down with RLS, so the database is safe. Then your app stores ID scans, invoices, and chat uploads in a bucket that anyone with a URL can read. A locked vault next to a wide-open filing cabinet.

If you built with Lovable, Bolt, Cursor, v0, or Replit, there is a real chance your storage sits open right now. Not because you were careless. Because the AI never asked whether that bucket should be private. Check the fast way first: run the free scan on your live URL.

QuestionAnswer
Does database RLS protect my files?
No. Storage has its own switch and its own rules.
What does "public bucket" mean?
Anyone with a file URL can read it. No login. Forever.
Is a private bucket enough?
No. Without owner-scoped rules, any logged-in user may browse all files.
How should private files be shared?
Signed URLs that die in minutes. Never a link that lives for good.
Is this a legal problem too?
Yes. An open bucket with EU user files can trigger GDPR duties.
How do I check mine?
Free scan of your live URL, plus the Supabase RLS checker.

Two systems, one dangerous mix-up

Supabase gives you two access systems. Founders treat them as one. Database RLS decides who can read and write rows in your Postgres tables. Storage rules decide who can read, add, and delete files in your buckets.

They look alike. They are not the same system. Getting the first one right does nothing for the second. That is how an app ends up with strict RLS policies on each table. Next to them: a bucket full of user files that needs no login at all.

Supabase storage security comes down to three checks. Is the bucket private? Do its rules limit each file to its owner? Does the app share files with short-lived signed URLs? Miss any one and you have a leak.

Public vs private buckets: the default that breaks trust

When you create a bucket, you pick public or private. Public means anyone with the URL can read each file. No login. No token. No check. That is fine for a logo. It is a breach for ID scans, invoices, chat files, and all else a user trusted you with.

The problem: public is the fast option. Vibe-coded apps get built fast. A public bucket makes the demo work with one line. So that is what the AI writes, and nobody comes back to flip the switch.

SetupWho can read a fileRight use
Public bucketAnyone with the URL, forever.Logos, marketing images.
Private, no policiesNo client access at all.A starting point, not an end state.
Private + USING (true)Any logged-in user. All files.Nothing. This is a fake lock.
Private + owner-scoped policyOnly the file's owner.User uploads. The correct default.
Signed URLAnyone with the link, until it runs out.Sharing a private file for minutes.

An outsider does not need your login. They need one bucket URL. A public bucket hands that out for free.

Storage policies mirror RLS, and almost nobody checks them

A private bucket is not safe on its own. Private only means "not open to the world." It does not mean each user sees just their own files.

Storage policies live on the storage.objects table. They use the exact same syntax as your table RLS: same USING clauses, same owner checks against auth.uid(). If you can write a policy for a profiles table, you can write one for a bucket.

Here is the part AI builders skip. They set the bucket to private and stop. The policy left behind reads USING (true): a login is required, but any signed-up account can browse every other user's uploads. "Private bucket" and "owner-scoped access" are two separate claims. Most vibe-coded builds only make good on the first.

Check all four layers:

Our Supabase RLS checker tests rule scope in about a minute. The deep-dive walkthrough lives in our Supabase RLS security guide.

Signed URLs: the right way to share private files

If the bucket is private, how does a user see their own photo? Signed URLs. A signed URL is a short-lived link. It dies after a set window, most often minutes. It proves the request was allowed at the time it was made. When the window shuts, the link dies.

Compare that to a public URL, which never dies and never checks a thing. If it lands in a browser cache, a traffic log, or a screenshot, it works for good.

The fix is plain: make signed URLs on the server, scoped to the asking user, with a short life. The AI shortcut is to hardcode a public URL in the frontend, because that is one line instead of a server round-trip. That shortcut stays unseen until someone finds the bucket.

The five mistakes that expose user uploads

We see the same five, over and over. None are exotic. We assume they exist until proven otherwise.

  1. Bucket set to public "just for launch." No one ever comes back.
  2. No storage rule at all. The bucket rides on the public/private toggle alone.
  3. Policies scoped to any logged-in user. One free signup, and a stranger can browse each upload.
  4. Public URLs hardcoded in the frontend. Files that needed a signed URL and a check get a link that never dies.
  5. The service role key in the client bundle. That key skips storage rules by design. If it ships to the browser, nothing else on this list matters. Know the difference: anon key vs service role key.

The scary part is not any single item. These gaps sit next to each other in the same codebase. One exposed key or one lazy rule turns into full bucket access. It is the same family of failure as CVE-2025-48757. There, missing RLS exposed data in 170+ Lovable-built apps. Symbiotic Security scanned 1,072 AI-built apps: 98% had at least one flaw. Storage gaps show up all the time.

The legal side: an open bucket must be reported

A public bucket is not just a technical slip. If it holds personal data — ID scans, medical files, resumes — GDPR may treat it as a breach. The clock starts when the bucket goes live. Not when someone finds it. GDPR follows your users, not your office: one EU user's file in that bucket is enough. Our GDPR guide for vibe-coded apps covers the duties, and the Compliance Wing runs 60 automated checks against your live URL.

The fix checklist (about an hour)

If that list reads like a foreign language, that is fine. It is the whole point of a machine check. Our Supabase security audit page covers what we test. The full path from first scan to launch is in the vibe coding security guide.

Is your bucket public right now?

Find out in about 30 seconds. One free scan of your live URL. No code access.

Run the free scan

What it costs to fix vs what it costs to ignore

The free scan is $0. The full Launch Readiness Audit is $499 one time. It covers storage rules, RLS, secret exposure, headers, and rate limits in one pass. You get a branded PDF and a ranked fix plan with time estimates. You also get a benchmark against 200+ audited apps and a senior review of each finding. Ongoing scans run from $149/mo. A breach letter — legal review, user notice, watchdog mail — costs more than all of it combined.

FAQ

Is Supabase Storage secure by default?

No. A bucket can be created as public, and then anyone with a file URL can read the upload without logging in. Storage only gets safe when you set buckets to private and write owner-scoped policies. Neither happens on its own.

What is the difference between a public and a private Supabase bucket?

A public bucket serves files to anyone with the URL, with no login, for good. A private bucket says no until the request passes your storage rules or carries a valid signed URL.

Do Supabase Storage policies use the same system as database RLS?

Yes. Storage policies live on the storage.objects table and use the same policy syntax as RLS on your own tables. But they are set up separately, so apps with solid database RLS still ship with open storage.

How do signed URLs improve Supabase storage security?

A signed URL grants access to one file for a short window, then it dies. Even if the link leaks into a cache or a screenshot, it dies on schedule. A public bucket URL works for good.

How do I check if my bucket is exposing user uploads?

Run the free Launch Ready Code scan on your live URL, and point the Supabase RLS checker at your project. The two of them check bucket state, rule scope, and whether your service role key is out in the open.

Does an exposed Supabase bucket count as a GDPR problem?

If the bucket holds personal data of any EU resident, yes. It can trigger GDPR duties no matter where your firm is based. The exposure starts when the bucket goes live, not when someone finds it.