It’s fast, but you probably shouldn’t use the default public schema in Supabase

Supabase’s automatic API generation from your database schema makes web app development incredibly fast. Instantly, your tables are exposed over REST and GraphQL API, and you can start building rich features without thrashing out any backend code or configuration besides the tables themselves.

But is it wise to launch a product with data sitting in public schemas, when it can be scraped so easily by curious developers or even competitors?

Take a photo‑sharing app: there’s a table with all events, and a table with all photos. With events and photos both in the public schema, someone could write a custom query to SELECT * from events, then loop through each event fetching all photos by event ID — effectively scraping the entire system in minutes. Or, if they don’t care about the relationship, they could just pull every row from the photos table directly.

Ideally, the access restriction should only allow “get all events where author_id = X or get a single event where id = X — effectively making the event ID an unknown token that the host shares only with trusted parties.

RLS alone wouldn’t prevent this kind of data exposure.

Solution, cobble part of what makes Supabase fast to develop

There is a well-recognised limitation to relying only on RLS in the Supabase public schema: while RLS restricts which rows users can fetch, anyone with the anon key can still construct complex queries—such as listing all events and scraping associated photos—subject only to your RLS rules. This means that unless your policies are highly specific (e.g., only allow the author to view their own events and related photos), broad data scraping remains possible.

To fully prevent public users from running unrestricted queries or scraping your data, the recommended best practice is:

  • Move sensitive tables (like events, photos) to a private, non-exposed schema.

    • Move your tables with: ALTER TABLE public.events SET SCHEMA private; and similarly for photos.
  • Do NOT expose the private schema to Supabase Data API clients.

    • In your Supabase Project API settings, ensure private is not listed under “Exposed schemas.” Only the public schema remains accessible directly.

  • Expose only controlled access through database functions or a backend API.

    • Write stored procedures (Postgres functions) in the public schema that internally fetch only authorized rows (e.g., only events/photos where author_id=X or id=X), validating logic inside the function.

    • Grant EXECUTE permission on these functions to anon/authenticated roles, but never SELECT permissions on private tables.

  • RLS is still necessary, but becomes a backup layer.

    • Inside the private schema, use RLS to lock down the tables as usual. But since direct client access is blocked, scraping is impossible without going through your code.

Why is this better than RLS alone?

  • It removes direct table/field visibility and listing from the client API, reducing the attack surface and eliminating schema enumeration.

  • All access must go through a limited interface (function/Edge API/backend route) where you fully control parameters and validate all queries.

Says who

Supabase docs, and expert best practices recommend this schema isolation and function-limited access model for secure production applications. This pattern is widely used to avoid broad fetches and scraping in SaaS, financial, and multi-tenant platforms.

Cost and benefit

Then why not just create a 5GBP DO droplet with MySQL, PHPMyadmin and a single PHP file to return the query results as JSON?

Good question.

First thing that comes to mind is that you probably don’t want your database running in your VM. Managed databases sometimes cost almost as much if not more than Supabase. Then managing deployment of updates to the API, do you install git, SSH into the server and pull updates? FTP the new php file? *shudder*. Or subscribe for 15 USD a month to a deployment service to push the master branch into place every 3 months? Versioned configuration-as-code anyone? No?

And you’ve now spent a day thinking about infra and not shipped anything.

Was this article helpful?
YesNo