Make Sure Users Can Only Access Their Own Data With Supabase

Lock each signed-in user to their own Supabase data, then prove one account cannot read or change another user's private records.

Roadmap & Resources

Find the Private Data

Check the Requirements

This guide starts where sign-in already works.

This guide assumes people can already sign in to your app, and that the private data you want to protect is already stored in Supabase.

Supabase sign-in already works in my app.

The private data I want to protect is already stored in Supabase.

Add User Authentication to Any Website with Supabase

Builds sign up, login, sessions, and protected areas with Supabase.

/video/add-user-authentication-supabase

Start with one feature

Start with one private feature. You can repeat the same workflow for other user-owned data later.

Inspect the Private Data

Nothing changes in this step. The point is to find out what your database actually allows today.

Inspect the Private Data

Tell AI which private feature you want to protect and let it inspect the app plus the live Supabase access rules before changing anything.

I want to make sure each signed-in user can access only their own private data in this existing Supabase app. Private data / feature: {{PRIVATE_DATA}} Where users see or manage it: {{FEATURE_LOCATION}} Maximum actions I want users to have: {{ALLOWED_ACTIONS}} Do not change anything yet. First inspect the existing project. Find: - The feature involved - The Supabase table or tables behind it - The current Supabase queries - Any owner or user column already used - Whether the browser currently sends a user id when creating records - Whether the UI filters records by user id - Whether server routes or Edge Functions are involved - Any Row Level Security or policy SQL present in the repository Do NOT assume repository SQL represents what is currently live in Supabase. Policies may have been created or changed directly in the Supabase dashboard. After inspecting the project, give me ONE read-only SQL query to run in the Supabase SQL Editor. That query must inspect metadata only. For the relevant table or tables only, it should report: - Whether Row Level Security is enabled - Current policy names - Policy roles - Policy commands - USING expressions - WITH CHECK expressions - Relevant owner or user columns and their defaults - Enough metadata to identify whether the rules are actually owner-scoped Do NOT query or print private user-row contents. Do NOT modify the database. Do NOT create policies yet. I will run the read-only query and paste the result back into this same conversation. After I paste the result, classify each relevant table as PROTECTED, EXPOSED, PARTLY PROTECTED, or NOT IN SUPABASE. Then explain in beginner-friendly language: - Who can currently read the rows - Who can create them - Who can edit them - Who can delete them - Whether the database actually checks ownership - Whether an existing policy allows too much - Whether the app currently relies only on browser-side filtering Look specifically for dangerous patterns such as: - Row Level Security disabled - USING (true) - WITH CHECK (true) - Any signed-in user being allowed every row - Owner access based on editable metadata - The browser choosing an arbitrary user_id - INSERT that does not bind ownership to auth.uid() - UPDATE that lets ownership change - Public or anonymous access to private rows - An old broad policy that would still override a new owner policy - Secret or service-role-equivalent credentials in browser code Important: Supabase policies combine permissively. If an existing policy already allows all authenticated users to access every row, adding a narrow owner policy beside it does NOT fix the problem. Identify the policies that must be REPLACED, not merely supplemented. Preserve intentionally public tables, unrelated tables, legitimate trusted admin policies, and app behavior that intentionally shares data. If this feature intentionally shares records between multiple users, STOP and tell me that a simple owner-only model is not appropriate. If existing rows have no reliable owner and ownership cannot be determined safely, STOP and ask before proposing a migration. Do not guess owners. Do not produce fix SQL yet. End with: 1. Current verdict 2. Exact problems 3. Minimal protection plan 4. Any blocker I must resolve first

What private information belongs to each user?

Projects, notes, bookings, saved items, submissions...

Where do users see or manage it?

Dashboard, My Projects page, account area...

What should users be able to do with their own data?

Run the read-only SQL AI gives you in the Supabase SQL Editor, then paste the result back into the same AI conversation before continuing.

Lock It to Its Owner

Protect the User-Owned Rows

One reviewed SQL block, based on what the inspection actually found.

Create the Protection SQL

Create the smallest Supabase rules needed so users can access only their own records without opening permissions the app does not need.

Using the live Supabase policy report and protection plan we just confirmed, prepare the SQL needed to protect ONLY the approved user-owned table or tables. If the live policy report from Step 1 is not present in this conversation, STOP and ask me to complete Step 1 first. Do not guess the current policies. For each approved user-owned table: 1. Enable Row Level Security if it is not already enabled. 2. Reuse an existing trustworthy owner column when possible — typically a column such as user_id or owner_id. Do not rename columns unnecessarily. 3. If an owner column is genuinely missing, add the smallest appropriate owner column and its relationship to the Supabase authenticated user. Where appropriate, use the authenticated user as the database-side default. Do NOT trust an owner id supplied by browser code. 4. Existing rows require care. If existing rows have NULL or missing ownership, or there is no reliable mapping from those rows to their real owners, STOP. Do NOT invent ownership. Do NOT silently assign all historical rows to the current user. Explain what must be decided first. 5. Add an index for the owner column if one is needed and not already present. Grant only the actions the app needs. The maximum I selected is a ceiling, not a request. Inspect what the feature actually does, and grant only the intersection of what the current feature really needs and what I allowed. Do not grant unused actions. If the app performs more actions than I allowed, STOP and explain the mismatch instead of silently granting more or silently breaking the feature. Typical owner rules: - SELECT: authenticated users may read only rows where the owner matches the authenticated user. - INSERT: the new row must belong to the authenticated user. Use WITH CHECK, and prefer database-assigned ownership over trusting a browser-supplied user id. - UPDATE: the user may update only their own rows. Use both USING and WITH CHECK so they cannot change a row's ownership to someone else. - DELETE: create an owner-only delete policy only if the existing feature actually deletes records and I allowed it. - Signed out: do not add private-data policies for anonymous users. Existing policies matter more than new ones. Do not simply add new policies next to an existing permissive policy. If an in-scope policy allows too much — every row, every authenticated user, true, or user-editable ownership metadata — then: 1. Explain that policy in one short plain-language sentence. 2. Explain why it defeats owner isolation. 3. Replace or drop only that unsafe in-scope policy. 4. Create the correct owner-scoped policy. Do NOT remove legitimate public-table policies, trusted admin policies, or unrelated table policies. If a trusted admin policy exists, preserve it. Never use true for private-data access. Give me: 1. A short list of the policies that will be changed, and why 2. ONE reviewed SQL block to run in the Supabase SQL Editor 3. What result I should expect 4. Any rows or table state that prevents safe execution Do not run it for me automatically. Do not modify unrelated tables. Do not disable Row Level Security to make a query work. Do not use a Secret or service-role key in browser code.

Review the policy changes before you run them

The SQL may replace an existing policy. Read the list of changes first, then run the block once in the Supabase SQL Editor and confirm it completes without errors.

Update the App Where Needed

Remove Client-Side Trust

Small changes only. The database stays the security boundary.

Update the App for Owner-Based Access

Make only the small app changes needed so the database — not the browser — decides which user's records are allowed.

The database rules are now in place. Inspect the feature and make only the app-side changes required for those rules to work correctly. Requirements: - Do not redesign the feature - Do not change unrelated pages - Do not add new functionality - Do not weaken database policies to preserve old client behavior If the browser sends the owner or user id when creating a record, remove that authority when the database can assign ownership from the authenticated session. If the app currently downloads all rows and hides other users' records in JavaScript, stop relying on that as security. It is fine to keep an owner filter as a query optimization or a UX detail, but the database rules must remain the security boundary. For blocked reads: handle an empty or not-found result normally. Do not expose another user's record. For blocked updates and deletes: handle a zero-row result safely. Do not assume Supabase must return a visible permission error. If the feature intentionally shares rows between users, STOP and report that the owner-only model is not appropriate. If an in-scope server route or Edge Function uses a Secret or service-role-equivalent key to read private rows for browser requests, check whether that server code independently verifies ownership. If it does not, STOP and report this as a remaining security blocker. Do NOT claim the feature is protected merely because table Row Level Security is correct. Do not redesign the backend in this guide. If you find a Secret or service-role key in browser or client code, STOP and report it clearly. Do not continue as if the feature is secure. When finished, report: - Files changed - Client-side trust removed - Whether owner filters remain only as a UX or query optimization - Whether any server-side bypass still needs attention

Test With Two Accounts

Test User A vs User B

Use test accounts and test data only.

User A can see their own records

User B can see their own records

User B cannot open User A's record by using User A's URL or record id directly, where the app supports that

User B cannot change or delete User A's record

Switching back to User A confirms the record is unchanged

Signed-out access does not reveal the private data

Check the result, not the error

A blocked read may simply return nothing. A blocked update or delete may affect zero rows instead of showing an error. Check the result — not just whether an error message appeared.

Verify the Database Boundary

The interface looking right is not proof. This tests Supabase directly.

Verify User Isolation

Test the Supabase boundary directly so we know the database blocks cross-user access, not just the interface.

Test the Supabase access boundary directly for the feature we just protected, using two test accounts. Use the safest testing method already available in this project: 1. An existing focused test runner or test utilities, if the project has them. 2. Otherwise a temporary LOCAL-ONLY test script using the project's normal Supabase client and its Publishable or public client configuration. Do NOT use the Secret or service-role key for isolation testing — it may bypass Row Level Security, so it would prove nothing. If credentials are required, ask me to place TEST account credentials in local, uncommitted test environment variables. Never ask me to commit passwords, never put them into source code, and never print them into the final report. Test only the actions the feature genuinely uses. Where applicable, verify: - READ: User B directly queries User A's record. Expected: no User A row is returned. - INSERT: try to create a record owned by the other account. Expected: rejected. - UPDATE: User B attempts to change User A's row. Expected: zero allowed rows and no change. Then re-read as User A and confirm the original row is unchanged. - DELETE, only if the feature deletes records: User B attempts to delete User A's row. Expected: no deletion. - SIGNED OUT: a request using the normal public client with no signed-in user must not return private records. Do not treat a page that simply did not display the record as sufficient proof. The test must exercise the Supabase access boundary. If you create a temporary test file or script, keep it local, do not commit it, and remove it after the test. Do not change production data. Use test rows only. When finished, report: - Which operations were tested - Expected versus actual result for each - Whether database-level isolation passed - Any remaining blocker

Run the Final Check

Confirm the Data Is Protected

One read-only pass over the live rules and the app code, ending in a single verdict.

Final Data Access Check

Re-check the live Supabase rules and the app code, then report whether this feature is actually protected.

Run a final READ-ONLY verification of the feature we just protected. First re-inspect the relevant project code. Then give me the read-only Supabase policy query again if you need it — I will run it and paste the result back. Do not modify the database in this final check. Verify: - Row Level Security is enabled on the protected table or tables - No unsafe broad in-scope policy remains - SELECT is owner-scoped - INSERT cannot choose another owner - UPDATE cannot change ownership - DELETE is owner-scoped, and exists only if the feature uses delete - Signed-out users have no private access - The app does not rely on browser filtering as the security boundary - Client code does not contain a Secret or service-role-equivalent credential - No in-scope server route bypasses ownership checks - Intentionally public tables were left public - Trusted admin policies were preserved - The two-account database-level test passed Do not claim this is 100% secure. Return exactly one final verdict: PROTECTED or NEEDS ATTENTION. If NEEDS ATTENTION, list only the remaining issues directly related to this feature. Then list any other user-owned tables you noticed under the heading NEXT TABLES TO REVIEW. Do not change them automatically.

Use User B one last time and confirm User A's test record is still unavailable and unchanged.