# Supabase RLS checklist before launch

If your vibe-coded app talks to Supabase from the browser, Row Level Security is the difference between “users see their own data” and “anyone with the anon key reads the whole table.”

The Supabase anon key is *meant* to be public. It lives in your frontend. What keeps it safe is not hiding the key. It is Row Level Security (RLS): Postgres policies that decide which rows each signed-in user can read or write.

This guide is a launch checklist, not a Postgres textbook. Work top to bottom. If any step fails, fix it before you charge anyone.

## 1. Turn RLS on for every table that holds user data

In the Supabase dashboard, open Authentication → Policies (or the table editor). For each table that stores accounts, profiles, posts, orders, files metadata, or anything tied to a user:

- Confirm **RLS is enabled**.
- Confirm you did not leave a temporary “allow all” policy from prototyping.
- Prefer explicit policies per operation (SELECT, INSERT, UPDATE, DELETE) over one giant policy you cannot reason about.

A table with RLS disabled is readable by anyone who has the anon key and can guess or enumerate IDs. That is a common vibe-coding failure mode: the app UI only shows “your” rows, but the API still returns others.

## 2. Write policies that bind to `auth.uid()`

Most solo apps need the same shape:

- **SELECT / UPDATE / DELETE:** `auth.uid() = user_id` (or whatever column stores the owner).
- **INSERT:** `auth.uid() = user_id` so a client cannot insert rows as someone else.

If you use a `profiles` table keyed by `id` matching `auth.users.id`, the policy is usually `auth.uid() = id`.

Avoid policies that only check “user is logged in” (`auth.uid() is not null`) on tables with per-user rows. That lets any signed-in user read every row.

## 3. Never put the service\_role key in the browser

The `service_role` key bypasses RLS. It belongs only on a trusted server: a Next.js route handler, a Supabase Edge Function with secrets, a worker, never `NEXT_PUBLIC_*`, never a Vite env that ships to the client.

Quick check: search your built JS for `service_role` and for long JWTs. Or paste your production URL into the free [Last Mile pre-launch check](https://last-mile-kit.vercel.app/check?utm_source=hashnode&utm_medium=syndication&utm_campaign=rls-checklist), which flags service\_role JWTs and notes anon keys so you remember to verify RLS.

## 4. Storage buckets need policies too

File uploads are easy to leave open. For each bucket:

- Decide public vs private. Avatars might be public; invoices should not be.
- Add storage policies so users can only read/write objects under their own prefix (for example `user_id/...`).
- Validate file type and size on the server or with storage restrictions. Do not trust the client MIME type alone.

## 5. Test like an attacker (two accounts)

Create two test users, A and B. While logged in as A:

- In the app, copy a request from DevTools that loads A’s data.
- Swap in B’s UUID (from the URL, the JSON body, or a list endpoint).
- Confirm the request fails (401/403 or empty), not 200 with B’s row.
- Repeat for update and delete.

Also test logged-out access to routes that should require a session. UI hiding is not access control.

## 6. Views, RPCs and edge functions

Security definer functions and views can accidentally bypass RLS if you are not careful. Before launch:

- List every RPC your client calls. Confirm each one checks the caller or runs as the invoker with RLS still applied.
- Prefer “security invoker” behavior unless you have a specific reason not to.
- If an Edge Function uses the service role, it must re-check authorization itself (session user, ownership) before writing.

## 7. Staging vs production keys

Use separate Supabase projects (or at least separate keys) for development and production. Never point a public production site at a project full of seed users and relaxed policies.

## Launch-day RLS short list

- RLS enabled on every user-data table
- No blanket allow policies left over from demos
- Owner checks use `auth.uid()`
- service\_role only on the server
- Storage policies match your privacy model
- Two-user IDOR test passed

RLS is one slice of a full launch. For secrets, Stripe webhooks, backups and the rest of the list, use the free [checklist preview](https://last-mile-kit.vercel.app/checklist-preview?utm_source=hashnode&utm_medium=syndication&utm_campaign=rls-checklist) or the full [Last Mile kit](https://skotbot.gumroad.com/l/yrpthn).

## 8. Common RLS mistakes in AI-generated apps

Agents often generate a first policy that looks right in the dashboard but fails in practice:

- **Policy on the wrong column.** The app stores `owner_id` but the policy checks `user_id`. Every query returns empty or, worse, you “fix” it by disabling RLS.
- **Using the service role in the client “just for now.”** Temporary becomes production. Search the bundle before every launch.
- **Enabling RLS with zero policies.** That locks everyone out, so someone toggles RLS off again. Add policies in the same change as enabling RLS.
- **Team/admin tables without role checks.** An `is_admin` flag in a client-readable profile table is not authorization. Check roles in policies or on the server with a trusted claim.

When an agent proposes a policy, ask it to explain in one sentence who can read and write which rows, then run the two-user test yourself.

---

*Originally published at [last-mile-kit.vercel.app](https://last-mile-kit.vercel.app/guides/supabase-rls-checklist?utm_source=hashnode&utm_medium=syndication&utm_campaign=rls-checklist). Run the free [pre-launch check](https://last-mile-kit.vercel.app/check?utm_source=hashnode&utm_medium=syndication&utm_campaign=rls-checklist) on your own app.*

