Request the diagnostic

A Founder's Checklist — Multi-Tenant Leaks in Supabase

bringforth · Rohit Chaudhri · Blog · April 25, 2026 · 7 min read

A user loads their dashboard and sees someone else's name, email, and records — not because of a sophisticated attack, but because one default setting was never changed.

A user loads their dashboard and sees someone else's name, email, and records — not because of a sophisticated attack, but because one default setting was never changed. That single missing switch is the most common security failure in AI-built Supabase apps. This checklist shows you how to find it and close it today.

How do I know if my Supabase app is leaking one user's data to another?

When an authenticated user runs a query and receives rows that belong to a different user, your app has a multi-tenant leak. The mechanism is a missing Row Level Security policy — without it, Supabase returns every row in a table to anyone who asks, regardless of who is logged in. In a solo demo that failure is invisible. In production with real users, it is catastrophic.

This is worth naming clearly before anything else. A multi-tenant app is one where many users share the same database tables but must only ever see their own records. When that boundary breaks, the failure is not subtle — a fintech user sees a stranger's account balance, a health app user reads someone else's medical history. The data is not encrypted away from them. It is simply handed over by a query that has no instruction to filter by identity.

You do not need a penetration test to confirm the risk. You need to know whether Row Level Security — RLS — is enabled on your tables and whether the policies attached to those tables actually filter by the logged-in user's identity. The checklist below tells you exactly where to look.

Why does this keep happening to apps built with AI coding tools — is it something I did wrong?

Vibe-coding tools create Supabase tables with row-level security disabled by default — not because you made an error, but because these tools are built to reach a working demo as fast as possible. Speed to demo and security for production are two different design targets. The tools optimise for the first one.

Adam Palicz, Founder of SpiniX, named the pattern precisely: "sometimes, even though Bolt has access to Supabase or Netlify, it can't complete the required config. the worst part? it doesn't tell you this. so you waste hours, burn credits, only to realize - you had to fix it manually elsewhere."

That is the structural problem. The AI tool generates a table, connects it to your app, and the demo works — because in a demo, there is only one user. RLS being off does not surface until a second real user appears. By then you may already be live.

Chris Nurse framed the wider tension this way: "Taking what you said literally, 25000 apps per day means every one needs to be carefully checked by someone who is not a developer, doesn't understand cyber security, compliance, load testing etc. So should it be 25000 prototypes?"

The answer, for your purposes, is practical: the prototype is not wrong. The assumption that it is production-ready is the risk. You did not build incorrectly — you built quickly, which is exactly what the tool was designed for. Now you need to harden what you have, without rebuilding it.

What does an actual multi-tenant leak look like inside my tables?

With RLS switched off, a standard Supabase SELECT query returns every row in the table — regardless of which user is authenticated. There is no filter. There is no wall. One query, every record.

Picture a simple health records table: columns for user_id, date, notes, diagnosis. Your app fetches records for the logged-in user. Without an RLS policy filtering by auth.uid(), Supabase returns all rows — every user's diagnosis, every user's notes — to whoever is authenticated. The app might only display one user's records on screen, but the full dataset crossed the wire. A developer with basic tools could intercept it.

The same scenario plays out in fintech: transaction tables, balance tables, linked account tables. Any table that holds records belonging to individual users is exposed by the same mechanism.

Ken Robinson put the underlying principle plainly: "Build a skyscraper on sand and it will fall over or have to be knocked down. Build it on a proven, stable foundation and it will be solid. AI gets you there quick, but likely on sand. If you use it, make sure you really study what was built."

Studying what was built starts with your Supabase table editor. That is where the gap is visible — and where the fix begins.

The five things to check right now — the RLS checklist for founders who aren't engineers

Every table in your Supabase database should pass all five of these checks before a second real user touches your app. Each one names the exact place in the Supabase dashboard to look.

1. Confirm RLS is enabled on every table. Open the Supabase dashboard, go to Table Editor, select each table, and look for the Row Level Security toggle. If it is off, every query on that table is unrestricted.

2. Verify a user_id column exists and is populated server-side. In Table Editor, check that each row-level table has a user_id column. Confirm it is written by your server or by a Supabase auth trigger — not passed in by the client. A client-supplied user_id can be forged.

3. Confirm each SELECT policy filters by auth.uid(). In the Authentication → Policies section of the dashboard, open the policies for each table. A safe SELECT policy reads: auth.uid() = user_id. If no policy exists, or if the policy condition does not reference auth.uid(), the table is open.

4. Confirm INSERT and UPDATE policies match. SELECT is not the only vector. In the same Policies panel, check that INSERT and UPDATE policies also enforce auth.uid() = user_id. A locked SELECT with an open INSERT means users can write records into another user's space.

5. Run a manual cross-user query test in the Supabase SQL editor. Create two test user accounts. Log in as user A and record their user_id. Open the SQL editor and run: SELECT * FROM your_table WHERE user_id = '[user B id]'. If rows come back, RLS is not working. This test takes under five minutes and is the only proof that matters.

Fraser Seymour identified what founders and tools both tend to skip: "I'd like to see some indication of what architectural and technology choices these constructor apps make. I'd also like to see these tools able to accept guidance in these same domains. Prototypes and POCs are great but for an actual production codebase ignorance of the strategic elements is a nonstarter."

The five checks above are those strategic elements — translated into steps you can execute yourself, today.

My app is already live with real users — what do I do in the next 24 hours?

Enable RLS on every affected table immediately — this stops new leaks from forming, even before the policies are perfect. Then audit existing rows for cross-contamination. Then, if data was exposed, communicate with affected users directly and transparently.

Triage in this order. First: in the Supabase dashboard, switch RLS on for every table that holds user-specific data. A table with RLS enabled but no policies will block all queries by default — that is safe. You can restore access selectively as you add correct policies. Second: query your tables for rows where user_id is null or where records appear in an account they should not belong to. This is the audit step — it tells you whether a leak already happened, not just whether one could. Third: if your audit finds cross-contaminated rows, contact the users whose data was exposed. Brief, factual, specific — name what was visible and for how long, and tell them what you have fixed.

This is not a rebuild. RLS is a well-understood mechanism with a known fix — a policy, added in the Supabase dashboard, that filters every query by auth.uid(). The structural problem is fixable in hours.

The single next step: open the Supabase dashboard now, go to Table Editor, and check the RLS toggle on your first user-data table. If it is off, switch it on. Everything else follows from that one action.