## TL;DR
Enabling RLS without an INSERT policy blocks every insert, including your own, because Postgres defaults to deny. Add an INSERT policy with a WITH CHECK clause that matches the rows your app should be allowed to create (usually `auth.uid() = user_id`), and the error goes away.

```text
new row violates row-level security policy
```

## Use this when
- INSERTs fail with this error right after you enabled RLS
- An agent inserts rows as an authenticated user and gets rejected
- SELECT works but INSERT doesnt on the same table

## Not for this skill when
- SELECT queries return no rows (thats a missing SELECT policy, different fix)
- The table doesnt exist or the column list is wrong (thats a schema error)
- You are using the service role key and still blocked (check whether RLS is forced)

## Steps

1. List the policies on the table to confirm no INSERT policy exists:

```sql
SELECT policyname, cmd FROM pg_policies WHERE tablename = 'your_table';
```
Expected output: rows for SELECT/UPDATE/DELETE maybe, but no row with `cmd = 'INSERT'`. That gap is the whole bug.

2. Create an INSERT policy scoped to the owning user:

```sql
CREATE POLICY "users_insert_own_rows"
ON your_table FOR INSERT
WITH CHECK (auth.uid() = user_id);
```
Expected output: `CREATE POLICY` confirmation. Adjust the check to your ownership column; the point is WITH CHECK must accept the row being inserted.

3. Test the insert as the app user, not as the table owner:

```sql
-- run as the authenticated role, or test via the app
INSERT INTO your_table (user_id, title) VALUES (auth.uid(), 'test');
```
Expected output: one row inserted. Table owners bypass RLS by default, so testing as owner hides the bug.

4. If inserts must be open (public form, webhook), write the policy to match that intent explicitly:

```sql
CREATE POLICY "public_insert" ON your_table FOR INSERT WITH CHECK (true);
```
Expected output: inserts succeed for anyone. Only do this when you mean it; an open INSERT policy on a table with sensitive columns is a leak.

## Variant phrasings

### violates row-level security on UPDATE instead
Same shape, different command: you need an UPDATE policy with both USING (which rows) and WITH CHECK (what the row may become).

### works in SQL editor, fails from the app
The SQL editor often runs with elevated privileges that bypass RLS. The app uses the anon/authenticated key, which is what the policies actually govern.

## Why it happens
`ALTER TABLE ... ENABLE ROW LEVEL SECURITY` flips the default from allow-all to deny-all for every command. Many tutorials show creating a SELECT policy and stop there, leaving INSERT/UPDATE/DELETE denied. The error message names no policy because there is no policy to name; the fix is always "add the missing policy," not "fix the existing one."

## Edge cases
- `FOR ALL` policies cover INSERT too, but a SELECT-only `FOR ALL`... no, FOR ALL means all commands. Double-check you didnt write FOR SELECT thinking it covers everything.
- Triggers and functions run as their owner by default and can bypass RLS unexpectedly; use SECURITY INVOKER when the function should respect policies.
- Supabase's `auth.uid()` is null for anon requests; a policy comparing `auth.uid() = user_id` rejects all anon inserts, which is usually what you want but surprises webhook setups.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_4nJTuos9UVMK0k58d0h95A
