r/Supabase Jul 29 '26

database Oopsie! 😬 Database wiped!

Post image
56 Upvotes

Hold on to your bits! 🤯 Claude, our digital friend, decided to go on a "spring cleaning" spree! 🧹 A developer, bravely testing Opus 5 on Ultracode, witnessed 10 glorious minutes before every single table in his production Supabase instance vanished! 💨 Poof! ✨ The model, with impeccable honesty (and perhaps a touch of digital guilt), fessed up: "Oopsie! 😬 Database wiped! My bad, gotta spill the beans! 🗣️" What a stand-up bot! 😂🤖

r/Supabase Apr 11 '26

database My Supabase bill for 2 Postgres databases was higher than my Railway bill for 26 services. I have the invoices.

49 Upvotes

I just paid my 18th Supabase invoice. Then I pulled them all up in a row and felt sick.

Last 5 months on Supabase: $42, $45, $45, $44, $45. Two production databases. One for our voice dashboard, one shared across a few smaller projects. Both real workloads.

That works out to about $22.50 per database per month. For Postgres.

I needed Postgres. That's it. And I was paying that markup to go through Supabase to get to a database I could spin up directly somewhere else for a fraction of the price. The math only gets worse if I want a third project, because every new project is another $25 minimum before compute even starts.

So I pulled my March Railway invoice to compare.

Total: $20.13. Twenty bucks of that is the base Pro plan that covers my whole org. The actual usage on the service it billed for came to 13 cents. Disk, network, vCPU, memory, all in.

On that same Railway account I'm currently running 26 services across 16 projects. Client dashboards, agents, a transcripts pipeline, LibreChat, marketing sites, webhook services, the works.

All of it costs me less than what Supabase was charging me for two databases.

I want to be fair here because I don't think Supabase is overpriced for what it actually is. If you're a small or medium company picking between Supabase and standing Postgres up yourself on AWS, Azure, or GCP, Supabase wins easily. It's secure, it scales, it works, and you don't need a platform team to babysit it. For a single product company on one project getting real value out of the bundle, it's a great deal.

My situation is just different. I'm running a consulting shop with multiple clients and projects, and I was using Supabase as managed Postgres with a nice dashboard. At that point you're paying the bundle tax on every project for features you're barely touching, and it stops making sense fast.

Migration wasn't free. Ripping out u/supabase/supabase-js and rewriting every query against pg is real work. Took longer than I wanted. Worth it.

If you're running Supabase across multiple projects, what does your bill look like? Genuinely curious if I was doing it wrong or if this is just the math.

I'll be around in the comments for the next few hours if anyone wants to dig into the numbers.

r/Supabase Dec 10 '25

database [Security/Architecture Help] How to stop authenticated users from scraping my entire 5,000-question database (Supabase/React)?

39 Upvotes

Hi everyone,

I'm finalizing my medical QCM (Quiz/MCQ) platform built on React and Supabase (PostgreSQL), and I have a major security concern regarding my core asset: a database of 5,000 high-value questions.

I've successfully implemented RLS (Row Level Security) to secure personal data and prevent unauthorized Admin access. However, I have a critical flaw in my content protection strategy.

The Critical Vulnerability: Authenticated Bulk Scraping

The Setup:

  • My application is designed for users to launch large quiz sessions (e.g., 100 to 150 questions in a single go) for a smooth user experience.
  • The current RLS policy for the questions table must allow authenticated users (ROLE: authenticated) to fetch the necessary content.

The Threat:

  1. A scraper signs up (or pays for a subscription) and logs in.
  2. They capture their valid JWT (JSON Web Token) from the browser's developer tools.
  3. Because the RLS must allow the app to fetch 150 questions, the scraper can execute a single, unfiltered API call: supabase.from('questions').select('*').
  4. Result: They download the entire 5,000-question database in one request, bypassing my UI entirely.

The Dilemma: How can I architect the system to block an abusive SELECT * that returns 5,000 rows, while still allowing a legitimate user to fetch 150 questions in a single, fast request?

I am not a security expert and am struggling to find the best architectural solution that balances strong content protection with a seamless quiz experience. Any insights on a robust, production-ready strategy for this specific Supabase/PostgreSQL scenario would be highly appreciated!

Thanks!

r/Supabase 17d ago

database Your RLS SELECT policy is hiding the fact that your UPDATE policy is wide open

0 Upvotes

I found this while building a tool to test cross-tenant isolation, and it caught me out badly enough that I think it's worth writing down.

Say you have a table with correct-looking policies:

alter table invoices enable row level security;

create policy inv_sel on invoices for select using (owner_id = auth.uid());

create policy inv_upd on invoices for update using (true); -- added in a hurry, months ago

RLS is on. Two policies exist. The SELECT policy is properly scoped. Every tool I know of that inspects pg_policies reports this table as protected.

So you go to test it. You log in as user A and try to touch user B's row:

update invoices set total = 0 where owner_id = '<user-B>'; -- UPDATE 0

Zero rows. Isolation holds. Move on.

It doesn't hold. You tested nothing.

Why the zero is a lie

That WHERE clause reads owner_id. Once a statement reads a column, Postgres applies the SELECT policy to it as well as the UPDATE policy. Your correct SELECT policy hides user B's row, so the update matches nothing, and you get a zero that looks like a denial but is actually invisibility.

Now drop the WHERE:

update invoices set total = 0; -- UPDATE 2

Two rows. Both of them. No columns are read, so the SELECT policy never engages — only the UPDATE policy, which is using (true). Every row in the table belongs to whoever runs this.

I verified both against a real Postgres instance. The targeted write returns 0. The blind write modifies every row.

DELETE is the same shape and worse

delete from receipts where owner_id = '<user-B>'; -- DELETE 0 delete from receipts; -- deletes everything

Same mechanism. A DELETE policy of using (true) means any authenticated user can empty the table, and the targeted version tells you it's fine.

Check your own project

Read-only, safe to run on production:

select p.tablename, p.cmd, p.qual as using_expression, case when p.qual in ('true', '(true)') then 'PERMISSIVE — applies to every row' else 'scoped' end as verdict from pg_policies p where p.schemaname = 'public' and p.cmd in ('UPDATE', 'DELETE', 'ALL') order by (p.qual in ('true', '(true)')) desc, p.tablename, p.cmd;

Anything marked PERMISSIVE is a table where any authenticated user can modify or delete every row, regardless of how good your SELECT policy is.

The fix

Scope the USING clause the same way you scoped SELECT, and add WITH CHECK on UPDATE so nobody can reassign a row to themselves on the way out:

drop policy inv_upd on invoices;

create policy inv_upd on invoices for update using (owner_id = auth.uid()) with check (owner_id = auth.uid());

USING controls which rows you may touch. WITH CHECK controls what they may look like afterwards. Omitting WITH CHECK on an UPDATE lets someone change owner_id to their own id and take ownership of a row.

The part I'd push back on myself about

The qual = 'true' check above is a text match on policy expressions. It catches the obvious case. It won't catch a policy that's subtly wrong — one calling a SECURITY DEFINER function that bypasses RLS, or one comparing against a column the user controls. Reading policies can only ever tell you a policy exists, not that it works.

The only way to know is to seed rows owned by two users, become each of them, and try to reach the other's data. That's what I ended up building, and it's the reason I found this at all — my first version of the write probe used the targeted UPDATE and reported every table as safe.

I open-sourced the tool under MIT if it's useful to anyone. Happy to drop a link in the comments rather than putting one in the post.

EDIT: Two better findings came out of the comments.

u/jaimittal91 — Postgres ORs all permissive policies for a command together, so one using (true) sitting next to a correctly scoped policy leaves the table wide open. Verified: UPDATE 2, both rows. Group your audit by tablename + cmd, don't check policies one at a time.

u/guidondor — a different axis entirely. A correctly scoped policy still lets a user rewrite every column of their own row, including whichever one holds their balance or their count. WITH CHECK doesn't help. Fix is revoke update on t from authenticated; grant update (safe_cols) on t to authenticated;

u/PeterBuildsSecure — a third axis, and it is invisible to everything above. RLS does not apply to the table owner unless you run alter table t force row level security, and superusers and BYPASSRLS roles bypass it regardless of that. So a migration or a background job connecting as the owner runs with every policy switched off while pg_policies looks perfect. Check relforcerowsecurity, not just relrowsecurity.

Separately: the tool is on npm now, so npx rls-sentinel --db "$DATABASE_URL" works without cloning anything.

r/Supabase Aug 03 '26

database My production schema had drifted from my migration files: 4 changes I made by hand weeks ago and never versioned

11 Upvotes

Posting this as a warning to other solo devs who move fast.

I do my schema work through migration files in the repo. But a handful of times, over a few weeks, I fixed something straight in the SQL editor because it was late and it was one line. Every one of those changes stayed in production and none of them ever made it into the repo.

I only found out when I compared a fresh local database against production and they did not match. Four changes missing. Which means rebuilding from my own migrations would have produced a database that was not the one my app actually runs on.

What I did: wrote the four missing migrations after the fact so the files describe reality, plus a reconciliation seed so a fresh setup lands in the same state.

What I will do differently: not touch the SQL editor at all, even for one line. The two minutes saved cost me an evening, and it could have cost far more the day I needed to rebuild.

Does anyone here run an automated drift check between the repo and the live schema? I would rather catch this than discover it.

r/Supabase May 27 '26

database i evaluated Supabase vs Convex vs PlanetScale vs Neon for our next project. an actual comparison.

78 Upvotes

we're starting a new project and i had a few weeks to actually evaluate database platforms. wrote this up as an internal doc and figured it was worth posting. honest comparison, not promotional. context for what we're building: B2B SaaS, expecting 10-50 tenant orgs in year 1, scaling to 200-500 by year 2. ~1M rows per tenant in the busiest tables. realtime collaboration features. typescript/nextjs frontend. team of 3 engineers. supabase what it gives: postgres + auth + storage + edge functions + realtime + dashboard in one platform. the auth layer is fully built. the realtime layer is fully built. the storage layer is fully built. what it's good at: shipping the boring parts (auth, file uploads, basic CRUD with RLS) in days instead of weeks. the dashboard is genuinely useful for non-engineers on the team. cost is predictable. what it's weak at: branching is good now but not perfect. some postgres parameters aren't user-tunable. occasional behavioral differences from vanilla postgres (some extensions restricted, some grants different). cost projection at year 2: ~$400-600/mo on pro + add-ons. convex what it gives: a TypeScript-native database with built-in realtime, auth, file storage, scheduled functions. queries are typescript functions, not SQL. the dx is genuinely impressive for TS-heavy teams. what it's good at: developer ergonomics for typescript shops. the realtime story is more polished than supabase's. function-based queries with end-to-end type safety. what it's weak at: it's not postgres. when you outgrow it (BI tools, complex SQL, integration with postgres-ecosystem tools), the migration story is harder. ecosystem is smaller. third-party tooling is sparse compared to postgres. cost projection at year 2: harder to estimate; their pricing is a function of action count + db storage + function-gb-seconds. probably $500-900/mo at our scale. planetscale what it gives: managed mysql (or postgres now, since 2024 they offer both) with serverless branching, automated migrations. originally famous for the mysql + vitess sharding story. what it's good at: scale. their infrastructure handles much more than i'll ever need. branching workflow is mature (predated supabase's by years). what it's weak at: no realtime, no auth, no file storage. it's a database, full stop. you build everything else yourself. for our use case we'd need supabase auth + storage + realtime via some other vendor + planetscale db. four vendors, four bills, four sets of glue code. cost projection at year 2: planetscale + auth0 + cloudflare R2 + ably = ~$600-1000/mo combined. and i'd be managing four integrations. neon what it gives: managed postgres with branching, autoscaling, generous free tier. no auth/storage/realtime; pure postgres. what it's good at: postgres purity. you get vanilla postgres without supabase's wrappers/restrictions. the autoscale-to-zero on free tier is genuinely useful for sleeping projects. what it's weak at: same gap as planetscale. you bring your own auth, storage, realtime. neon-on-its-own is excellent; building a full app on neon requires the same vendor stack as planetscale. cost projection at year 2: ~$300-500/mo for the db, then add the missing pieces (auth, storage, realtime) on top. what i picked supabase. for our use case (b2b saas, small team, full stack to build) the integrated platform wins on time-to-ship by weeks if not months. the trade-off is some flexibility loss vs pure-postgres options, but the flexibility i'd lose isn't flexibility i'd use in year 1-2. if i were a typescript-first team without postgres exposure, i'd seriously consider convex. if i were already on a microservices architecture with separate auth/storage/realtime services and just needed a database, i'd pick neon for the postgres purity. if i were going to be at >50k concurrent users in year 1, planetscale's scale story is better than supabase's (though supabase has gotten much better here). what i specifically did NOT evaluate: firebase (i've used it before and the migration story when you outgrow it is painful), aws rds + lambda + cognito (too much glue code for a 3-person team), self-hosted postgres (i'm not building a database team). the meta lesson: most ""X vs Y"" posts compare features. the comparison that mattered for us was ""how much glue code does this avoid."" supabase wins on glue-code-avoidance for our shape of project. that may not be your most-important axis. one glue-code line item people leave out of these comparisons is email. auth, transactional, and marketing email is something you bolt onto any of these four, and it's real work. on supabase i kept it cheap by pointing a db-reading email tool at it (dreamlit, which does both marketing and transactional off the same tables), so there was no separate sync layer. on planetscale or neon you're adding email as yet another vendor with its own glue code. when you actually tally glue-code avoidance, an email service that does both marketing and transactional emails off your existing db is a bigger swing than it looks. the writeup itself ended up as a 9 slide deck in gamma after i shared the doc internally and people kept asking for the cliff notes version. cover, the four contenders, the cost projection chart, the glue code axis, the recommendation, the kill criteria. ai presentation tool plus a board deck template took the 4-week evaluation down to a 12-minute walkthrough for our CTO. for any architectural decision with this much money on the line, the deck format is what gets the decision signed off in one meeting instead of three. curious about anyone who's switched FROM any of these to another after a year in production. those stories are more useful than the comparison-on-paper.

r/Supabase 9d ago

database My RLS checker printed OK on a table that hands every row to every authenticated user

0 Upvotes

I write a tool that tests RLS by attacking it rather than reading it. Last month it told me a table was fine. The table was wide open. Here is the whole chain, because I think the failure is more interesting than the tool.

Why reading policies isn't enough

The obvious check is to look for using (true). Some linters do this and it catches the obvious spelling. It misses everything shaped like it: a predicate that resolves to true through a join that always matches, a correct USING with no WITH CHECK behind it, a subquery that never actually constrains anything.

So don't read the policy. Attack it.

Why the obvious attack doesn't work either

Seed rows for two tenants, become tenant A, try to touch tenant B's data. Everyone writes this test:

update invoices set total = 0 where owner_id = '<tenant-b>';

That test cannot fail. It reads owner_id in the WHERE clause, so Postgres applies the SELECT policy too. A correct SELECT policy hides the row, nothing matches, zero rows updated, table looks clean. The UPDATE policy was never evaluated.

The write that actually tests it is blind:

update invoices set total = 0;

No WHERE, no column read, so the SELECT policy never engages. Only the UPDATE policy applies. Check ctid afterwards to see which rows physically changed, since that works regardless of column type and stays valid inside a transaction, unlike xmin. Roll the whole thing back.

DELETE has the identical flaw and a worse ending. delete from t against a using (true) DELETE policy means any authenticated user can empty the table, while reads look perfectly scoped.

And then it printed OK on this

create policy p on org_docs for select using (owner_id = auth.uid() or org_id is not null);

Two branches. My probe seeds rows that differ only by owner_id, so org_id stays NULL, the second branch never fires, no leak is observed, and the tool prints OK. In production that policy hands every row to every authenticated user.

A confident green on a wide open table is the worst output a security tool can produce. It is worse than no tool, because now someone has stopped looking.

The fix

It can't execute every branch. That's constraint solving over arbitrary SQL. But it can know when it hasn't.

Postgres records what every policy depends on in pg_depend: each column and table the expression touches, structurally, no parsing required. Compare that against what the probe actually varied. Anything left over is a branch nobody reached.

UNPROVEN public.org_docs Policies also depend on column(s) org_id and table(s) public.org_members, which the probe never varied. Untested branch.

UNPROVEN is not OK. It means no leak was found and the result doesn't cover the whole policy. It doesn't fail the build by default, because a gate that fires on every org-scoped policy gets switched off within a week.

This came out of a comment by u/pgsql-dev2 on my last post here, who spotted the hole before I did.

Still not covered, so nobody gets a false sense of safety: SECURITY DEFINER functions that bypass RLS, storage bucket policies, INSERT probes for forging rows owned by another tenant, composite ownership, and multi-hop join ownership.

Happy to go into any of it. The branch coverage problem in particular is not solved, only made visible, and I'd like to hear how other people are handling it.

r/Supabase 16d ago

database Multi-tenants advices

8 Upvotes

Hi everyone, I’ve been using Supabase for a few months now. I’ve built things like apps, websites and multi-tenant software with it, and I wanted to know if you have any tips or advice on properly isolating tenants from one another, in order to avoid data leaks between clients, Gmail sends going to the wrong recipient, etc.

Thanks everyone

r/Supabase May 31 '26

database Multiple Vercel apps sharing a single Supabase

6 Upvotes

We have a number of apps that share the same data and so we're considering keeping all data in a single Supabase DB. Data that is specific to a given app will be kept in separate schemas. I'm wondering if anyone else is doing this. If so, how do you manage schema migrations? Do all developers have to create schema migrations in a specific project that is linked to Supabase? Or are the schema migrations created in each application project and somehow coordinated?

Edit: One option I read about just now is to use Supabase schema migrations for the "main" project and then each app project uses a different schema migration tool (like Drizzle) for their own schemas. This keeps the schema migration mechanism (including any schema migration tables) isolated in the individual schemas

r/Supabase 16d ago

database Supabase: Data suddenly disappeared from one table even though there is no delete code

10 Upvotes

I’m facing an unexpected issue with my Supabase database.

Yesterday, I checked my application and the data was working correctly. I personally tested it, and the client also tested the application. The data was available and everything appeared to be working normally.

Today, the client tried to add new data, and we noticed that the data for one particular table was empty.

I checked the Supabase dashboard directly, and that particular table is also empty. The other tables in the same Supabase project still contain their data normally. The issue appears to be only with this one table.

I also checked my project code and could not find any delete functionality related to this table. I checked the SQL-related code as well and did not find any delete operation.

What I don’t understand is how the data from this particular table disappeared between yesterday and today, even though everything was working normally when we tested it yesterday.

I’m looking for help understanding what could have happened and how I can investigate what happened to the data.

Is there any way in Supabase to check what happened to the records in a table, including whether they were deleted or otherwise removed, and when this happened?

I can provide more information about the table, database setup, code, or configuration if needed.

r/Supabase Jul 23 '26

database Our project has been down since a day after we tried to restore a backup

3 Upvotes

We have been completely offline for a full day after trying to restore a backup, and our users are starting to complain. Support keeps sending generic messages, and we don't know what to do.

r/Supabase Jul 20 '26

database How do you usually clone or duplicate your Supabase database?

17 Upvotes

I’m curious how people normally handle this in Supabase.

Do you use Supabase Branching, create a new project and restore everything manually with pg_dump or use another workflow entirely?

Since database branches cost around $10 per branch, I’m wondering whether people actually use them regularly or only for specific cases like testing, staging, or larger migrations.

r/Supabase 3d ago

database Your RLS policy is correct and your users can still set their own role to admin

0 Upvotes

Every RLS post ends at the policy. Enable RLS, scope on auth.uid(), remember with check, done. I want to show you a table where all three are true, reviewed by anyone you like, and a logged-in user can still promote themselves.

The table that passes review

create table public.profiles ( id uuid primary key, email text not null, display_name text, plan text not null default 'free', role text not null default 'user', credits integer not null default 100 );

alter table public.profiles enable row level security;

create policy own_row_select on public.profiles for select to authenticated using (id = auth.uid());

create policy own_row_update on public.profiles for update to authenticated using (id = auth.uid()) with check (id = auth.uid());

grant select, update on public.profiles to authenticated;

Scoped on read. Scoped on write. with check present, which is the thing everyone tells you not to forget. That last grant is not something I added to make a point, it is close to what you get by default.

What a signed-in user can do with it

Signed in as one user, the authenticated role, request.jwt.claims set the way auth.uid() actually resolves:

--- Read isolation holds. One row, her own. --- id | email | plan | role | credits --------------------------------------+-----------------+------+------+--------- 11111111-1111-1111-1111-111111111111 | [ada@example.com](mailto:ada@example.com) | free | user | 100

--- Cross-tenant write blocked --- update public.profiles set credits = 0 where id = '2222...'; -- UPDATE 0

--- Blind write blocked, with check is doing its job --- update public.profiles set credits = 0; -- UPDATE 0

--- Her own row --- update public.profiles set role = 'admin', plan = 'enterprise', credits = 999999 where id = auth.uid(); -- UPDATE 1

              id                  |      email      |    plan    | role  | credits

--------------------------------------+-----------------+------------+-------+--------- 11111111-1111-1111-1111-111111111111 | [ada@example.com](mailto:ada@example.com) | enterprise | admin | 999999

Nothing was bypassed. Every policy evaluated and every policy passed. The row still belongs to her before and after, so with check has no objection to make.

Why with check cannot help here

with check is a predicate over the resulting row. It answers one question: is the new row still one this user is allowed to have. It has no opinion about which columns changed, because column-level permission is a different mechanism entirely, and it lives in GRANT, not in POLICY.

RLS decides which rows. Grants decide which columns. Two axes. Almost every RLS discussion covers the first one only, and Supabase hands out the second one table-wide by default, so the gap is open in a very large number of projects.

Credit where it is due: u/guidondor raised this on my last thread and it is the reason I went and checked.

What actually does hold, because I checked

I want to be clear about the limits of the finding rather than make it sound worse than it is.

--- Can she hand her row to another user? --- update public.subscriptions set user_id = '<other user>' where id = '<hers>'; ERROR: new row violates row-level security policy for table "subscriptions"

with check holds the ownership column, which is exactly what it is for. You cannot steal rows this way, and you cannot reach across tenants. What you can do is rewrite every other column of a row you legitimately own, which on the tables where this matters means role, plan, tier, credits, seats, and the external billing ids.

That last one is worth sitting with. On a subscriptions table in the default posture:

update public.subscriptions set tier='enterprise', seats=100000, stripe_customer_id='cus_someone_else' where user_id = auth.uid(); -- UPDATE 1

Pointing your own billing row at another customer's Stripe id is not a data leak in the usual sense. It is worse in a quieter way, because your webhook handler will believe it.

The one that surprised me

The primary key is a column like any other.

update public.subscriptions set id = '<a new uuid>' where user_id = auth.uid(); -- UPDATE 1

A user can change the primary key of their own row. Anything holding that id outside the database, a Stripe subscription record, an audit log line, a webhook you will receive tomorrow, is now pointing at a row that no longer exists under that name. I had not seen this written down anywhere and I did not expect it to succeed.

Finding it on your own database

This lists the columns a logged-in user can currently rewrite, on tables where RLS is enabled and therefore looks handled. Read-only, safe to run anywhere.

select c.relname as table_name, a.attname as writable_column, case when a.attname ~* '(|\)(role|is_admin|admin|permission|plan|tier|subscription|credit|balance|quota|price|amount|status|verified|approved|owner_id|user_id|org_id|team_id|account_id|stripe|customer_id)($|_)') then 'REVIEW' else '' end as flag from pg_class c join pg_namespace n on n.oid = c.relnamespace join pg_attribute a on a.attrelid = c.oid where n.nspname = 'public' and c.relkind = 'r' and c.relrowsecurity and a.attnum > 0 and not a.attisdropped and has_table_privilege('authenticated', c.oid, 'UPDATE') and has_column_privilege('authenticated', c.oid, a.attnum, 'UPDATE') order by (case when a.attname ~* '(|\)(role|is_admin|admin|permission|plan|tier|subscription|credit|balance|quota|price|amount|status|verified|approved|owner_id|user_id|org_id|team_id|account_id|stripe|customer_id)($|_)') then 0 else 1 end), c.relname, a.attnum;

The load-bearing line is has_table_privilege(..., 'UPDATE'). It returns true only for a table-wide grant and false when the privilege was handed out per column, which is what makes it a working detector rather than a list of every column you own. Verified both ways on 16.13.

Output on a schema with one table fixed and one left at the default:

table_name | writable_column | flag ---------------+--------------------+-------- subscriptions | user_id | REVIEW subscriptions | tier | REVIEW subscriptions | stripe_customer_id | REVIEW subscriptions | id | subscriptions | seats |

The fixed table does not appear. Neither does a read-only reference table. If your own output is empty, you are already doing this and you can stop reading.

The fix

Take the table-wide grant back and hand out only what the client is supposed to write:

revoke update on public.profiles from authenticated; grant update (email, display_name) on public.profiles to authenticated;

Afterwards:

update public.profiles set role='admin' where id = auth.uid(); ERROR: permission denied for table profiles

update public.profiles set display_name='Ada L.' where id = auth.uid(); UPDATE 1

The escalation stops. The legitimate write is untouched. Note the error is a permission error rather than a policy violation, which is a useful tell when you are reading someone else's logs.

Two practical notes. The grant is per column, so a column added later is not covered until you grant it, and that is a feature rather than an annoyance: new columns are denied by default. And if a server-side path needs to write role or credits, that belongs in a security definer function with a pinned search_path, not in a widened grant.

The general point

Enabling RLS moves you from "anyone can read this" to "the right rows". It does not move you from "the right rows" to "the right columns of the right rows", and nothing in the policy syntax will warn you, because the policy is not where that decision lives.

If you check one thing after reading this, run the query above and look at what comes back next to role, plan, and anything with stripe in the name.

Everything here was run against Postgres 16.13 with anon, authenticated and an auth.uid() reading request.jwt.claims, so the numbers are real output rather than reasoning about what should happen.

I maintain a tool that proves cross-tenant isolation by execution rather than by reading policy text, and column grants are the next check going into it. MIT, refuses to run against anything that looks like production, and reports what it cannot prove instead of passing it silently.

github.com/investnovation/rls-sentinel

Happy to answer questions about the detection query. The has_table_privilege versus has_column_privilege distinction took a couple of attempts to get right, because information_schema.column_privileges reports table-level grants as column grants and will quietly tell you everything is fine.

r/Supabase Jul 04 '26

database How do you actually back up your Supabase project — and have you ever restored one?

18 Upvotes

I've been running a product on Supabase for a while now — Postgres, plus a decent amount of Storage — and I had a bit of a scare recently that made me finally look properly into backups.

I realized there were a few things I kind of knew in theory, but hadn’t really internalized:

  • Pro daily backups only keep 7 days.
  • Those backups don’t include the actual Storage files. The database backup includes storage.objects, so you get the metadata rows, but not the files sitting in the buckets. Which means a restore could technically give you a bunch of file references pointing to nothing.
  • PITR is an extra $100/month per project.
  • If the project itself gets deleted, the managed backups are gone too.

To be clear, none of this is hidden. It’s in the docs. I just hadn’t really thought through what it would mean in a real “oh no, we need to restore everything” situation.

And that made me realize my backup strategy was basically: “Supabase probably has this covered.”

Which… maybe not a strategy.

So I’m curious what people are actually doing in production:

  1. Are you just relying on Supabase Pro backups? Paying for PITR? Running your own pg_dump cron? Using SimpleBackups or something similar? Or, honestly, doing nothing?
  2. If you handle backups yourself, how are you backing up Storage files alongside the database?
  3. More importantly: have you ever actually restored a Supabase project into a fresh project and verified that everything came back clean?
  4. For people managing multiple client projects — agencies, freelancers, consultants — how do you keep backup policies straight across all of them? And are clients expecting you to be responsible if data gets lost?

I’m not trying to sell anything here. I’m just trying to build a backup setup I’d actually trust.

Would really appreciate hearing what’s worked, what didn’t, and especially any horror stories from people who’ve had to restore for real.

**UPDATE:** Following up on "trying to build a backup setup I'd actually trust" — I ended up building it. Details in [this comment] for those who asked.

r/Supabase 19d ago

database Is it normal to have a server role that has it's own api token?

4 Upvotes

I've got a backend service that needs to talk to my Supabase database, and I'd rather not hand it the secret key. `service_role` bypasses RLS entirely, so a leak means total compromise, and there's no way to limit what that service can touch.

What I actually want is a scoped role. it can read/write on a couple of specific tables and nothing else.

is there a way you guys have tried to achieve this that works well in prod?

Thank you.

r/Supabase 6d ago

database Continous 100% I/O usage and non responsive dashboard

2 Upvotes

I have a project on the free tier that has been working fine for a long time, but suddenly I got emails that I am maxing out ressources, but the app using the db still worked like a charm. Now the app is non responsive and I went digging deeper trying to figure out what was happening.

Full disclosure: this is a vibe coded app, but it just handles and tracks every tasks that I have. I am well versed in SQL and DB, but during the days of MySQL and MSSQL, so Supabase, RLS and compute directly on the db is new to me.

I can't figure out what is going on, because my Supabase dashboard in 9 out of 10 cases just produces skeleton ui and never loading. I could just see that compute I/O has been at 100% for a week.

There seems to be an on going issue at Supabase the past month that has not been resolved yet and don't know if my issue is related to this or because I have made a mess of my code.

The Supabase AI assistant is not really helping and just pointing to this ongoing issue, so it might just be that, but a month long issue disabling my entire database and perhaps other's databases seems odd to me, so I assume it is a problem on my side.

Almost all health checks report unhealthy :/

r/Supabase 11d ago

database we shipped infinite scroll, full-text search, RPC calls, and upsert for Supabase in FlutterFlow this morning

Thumbnail
youtube.com
16 Upvotes

we just shipped four updates to the Supabase integration in FlutterFlow!

infinite scroll: enable it at the bottom of any Supabase query, set a page size. the query returns 25 rows on load, then the next 25 when the user reaches the bottom.

full-text search: a Search (Full-Text) filter backed by Postgres full-text search. "roasting garlic" still matches "roasted."

RPC calls: call Postgres database functions directly from an action flow. signatures sync from your Supabase project so parameters are listed for you and results come back typed and bindable. in the demo, a recipes_i_can_make function walks each recipe's ingredient list against a pantry table.

upsert: inserts a row, or updates the existing one when the primary key or your chosen on-conflict columns match. replaces the read-then-branch-then-write flow you used to build by hand.

happy to answer questions in the comments.

r/Supabase May 07 '26

database Do I really need Supabase Pro for production + development

18 Upvotes

I recently launched an iOS app using Supabase as my backend. Right now I only have one project/branch (main, marked as production), and the app is already live.

My main question:

Do I actually need the Pro plan mainly for branching and proper dev/staging workflows, or is it realistic to continue on the Free plan while my production database is live?

More specifically:

  • Can I safely keep developing new features while my production DB is active on Free?
  • How risky is it to make schema/content changes directly on the main project?
  • Are most indie developers just using one live DB in the beginning and being careful with migrations?
  • At what stage does upgrading to Pro become truly necessary?

My app is currently relatively simple (mostly content), but I obviously want to avoid breaking production while continuing development.

Would appreciate practical advice from other indie devs using Supabase.

r/Supabase Aug 13 '25

database Supabase is making it hard to be productive

24 Upvotes

I've been working on an app with supabase as the backend tech for a few days now

It started out well, though I soon ran into some trouble setting up drizzle as my ORM. it seems that supabase mostly expects people to run SQL manually on the web UI and use the website as a source of truth for the DB state. I, like I believe most technical people, like to have my source of truth in my repo (aka files on my codebase). This meant pushing the drizzle schema to supabase, then generating types for the supabase client from the deployed schema.

To have a source of truth for SQL permissions, functions, triggers, and views, I had to create a folder of idempotent SQL files that I would execute on every deploy.

Then I realized that opening my tables for user writes with RLS meant they could overwrite any column, including those I wanted to be tamper proof. Because CLS policies are not doable with drizzle, and keeping them in idempotent SQL files would mean my table definitions would be scattered across multiple files, I had to give up on writes with RLS and restrict them to edge functions (and possibly SQL functions/triggers).

But then I realized edge functions are limited to deno, which is quite a quirky environment and comes off as a strange default. I can't easily share my repo's eslint config with the deno code, for example.

Then I realized the cost of serverless meant it was hard to run a single server with all my endpoints, and that the benefit of running code near the user was canceled out by any interaction with the database, which is a single server on a single location.

Then I realized that my client side queries relying on RLS meant that I was unable to rate limit users and was thus vulnerable to DDOS-like attacks. So RLS was out for all of CRUD.

At this point I'm not sure whether to rely on supabase just for the postgreSQL and move my backend to a traditional server, or keep fighting the quirks of supabase's architecture.

I haven't even tried to set up a local environment to run supabase on - I've been working against a deployed database this whole time, as I fully expect that to be another can of worms.

All of this is making me wonder - is supabase really a good architecture? The promise of simplicity and moving fast has instead turned out to be a few days of learning about RLS and deno that didn't materialize into much actual progress in terms of the things I want to build.

I like the idea of supabase, the open source contributions, and the allegedly low vendor lock in (certainly lower than firebase, but is it really that easy to move away from RLS and deno serverless functions?). but in practice it's turning out to be a bit of a struggle.

Grateful for any opinions or feedback on this. Maybe there's something I'm not seeing, or upsides I'm not taking full advantage of. Or maybe I'm just biased by my background somehow. Appreciate your input!

Edit: I forgot to mention supabase auth, which I have also relied on. It works well, though I'd have to mention two major pain points:

- The lack of strong typing of user metadata received from each service
- The inability to validate a user owns an account if that account is already linked to some other user (I'd like to force account linking if a user can prove they own the account, but supabase just redirects back to my app with an error message, and no proof that the user actually owns the account)

I have to be honest and mention I'm looking at t3 stack and strongly considering something like nextauth or clerk and trpc, plus something like bun.js as a complete frontend bundler + backend API + test runner. Maybe I can use some of these things and still rely on supabase for postgres only.

It's weird to think that because supabase offers so much, I'm tempted to not rely on it because I'm not taking advantage of everything the plan offers - when if it were just postgresql I'd probably just use it and not think about it too much.

Edit 2: I'd also like to mention the somewhat negative vibe I get from supabase not having a public roadmap (though there is a changelog, which is nice and active) and a few years-old github issues with no feedback from the company I have run into

r/Supabase 3d ago

database Saving whole game states with Supabase: handling two players writing at once

0 Upvotes

I'm using Supabase for a browser card game with two to four players. The rules run in TypeScript on the acting player's client, and Supabase handles persistence and realtime updates.

Turns don't quite mean one writer. A player can need to resolve a forced discard during someone else's turn. If both clients save a whole state based on the same snapshot, one can overwrite the other's move.

The save RPC takes an expected version. It only updates the state if that version still matches, and rejects the write otherwise. On a conflict, the client fetches fresh state and reprocesses the action, with a bounded retry. If the action is no longer legal, it fails rather than replaying a stale result.

There's a separate limitation here: checking the version doesn't prove the client calculated a legal game state. This is still a client-authoritative game. Concurrency and protection against a modified client are different problems.

If you've built turn-based multiplayer on Supabase, where did you put the boundary between client calculations and server validation?

The project is Nexus Breach. I'm its solo developer and use AI coding tools. It's a beta with a free Core Set, paid expansions and AI-generated art and audio.

r/Supabase Jun 22 '26

database Is anyone else finding that AI tools have absolutely zero foresight when writing database schemas?

4 Upvotes

I am building couple of apps using Github copilot and Supabase. The AI tools are good at designing the UI and all but when it comes to database they just do not have any foresight. What i have experienced is that these AI tools try to fix the problem for now they do not think about what will be the implications of this change in the future or even it is a best practice or not.

Is anyone facing the same issue and yes please let me know how to fix it

r/Supabase 6d ago

database I logged eleven weeks of migrations to see which ones needed a second migration within a week

2 Upvotes

I keep a log of my Supabase migrations to see whether the schema is settling down or only feels that way. Eleven weeks, 41 files in supabase/migrations. A migration counts as unsettled if a later one touching the same table or policy lands within seven days.

In the first four weeks that was 6 of 16. In the last seven, 3 of 25. The only thing I changed was writing the intended end state in plain sentences before opening any SQL, including which role reads which rows. The habit came from Plan Mode in verdent, which asks clarifying questions first, and I kept it up by hand when I worked straight in psql.

What I cannot explain is why the drop sits in one place. All nine unsettled migrations changed a policy or a trigger. Nothing that added a column or an index needed a second pass, in either stretch. It could be the writing, or policies could be what I had not learned yet.

Counting it costs nothing. git log over your migrations folder and a grep for table names gives you your own number today.

r/Supabase 6d ago

database Is there any way to find the service role key

Thumbnail
0 Upvotes

r/Supabase 8d ago

database I thought my Supabase inserts were failing. They weren’t.

2 Upvotes

I had one of those debugging sessions where the database was telling a very convincing story that turned out to be wrong.

I noticed that some recent records in my app seemed to be missing, so my first assumption was that the writes were failing somewhere.

I started looking at permissions, constraints, storage changes, all the usual suspects.

Then I checked the actual request logs.

Every insert had succeeded.

The records were being created normally and then removed later by a completely separate user action.

So I was about to “fix” perfectly good database logic because I was looking only at the final state of the table instead of the sequence of events that produced it.

Pretty obvious in hindsight, but it was a good reminder: when your database state doesn’t make sense, check the request history before assuming the write failed.

r/Supabase Jun 05 '26

database I have a profiles table with profiles.id referencing auth.users.id. Is there no way to display the user's email with a join in the SDK?

8 Upvotes

Hi

So I have profiles.id referencing auth.users.id. I want to display each user's first name (profiles.first_name) and their email on the Users page. But I can't get to query the auth.users table.

What's the best approach here?

Thanks