Skip to content

Diagnosing a Supabase RLS INSERT rejection

Covers confirming which policies exist and why WITH CHECK is rejecting the row, and adding a correct INSERT policy. Does not cover RLS for SELECT/UPDATE/DELETE (governed by USING, a different clause) or auth/JWT configuration beyond noting that the SQL editor bypasses RLS entirely and so can never be used to test a policy.

Unverifiedno reproductions yetWhy this confidence?
Revision 1published by DevYou curationrevision history
Run the diagnosisEvidence and compatibility

Symptoms

The diagnostic path

9 steps. Every step is written out below in full — the interactive version simply follows the branches for you.

  1. Step 1 · StartRLS is rejecting an INSERT the app performs, but the SQL editor succeeds

    That contrast is itself the clue: the SQL editor runs as a privileged role that bypasses RLS entirely (the same is true of a server-side call using the service_role key), so it proves nothing about whether a real app-facing policy exists — it will 'work' even with zero policies on the table.

    What happens next

    • passed step 2, Confirm RLS is enabled and see what policies exist
  2. Step 2 · TestConfirm RLS is enabled and see what policies exist

    List every policy on the table, regardless of which command it covers.

    Read-onlysql
    SELECT policyname, cmd, roles, qual, with_check
    FROM pg_policies
    WHERE schemaname = 'public' AND tablename = 'orders';

    Expected result

     policyname                  | cmd    | roles           | qual | with_check 
    ------------------------------+--------+-----------------+------+------------
     users can view own orders   | SELECT | {authenticated} | ...  | 
    (1 row)

    What happens next

    • passed step 3, Does an INSERT-covering policy's WITH CHECK actually match this row and role?
    • failed step 4, No policy permits this INSERT at all
    • unknown step 4, No policy permits this INSERT at all
  3. Step 3 · TestDoes an INSERT-covering policy's WITH CHECK actually match this row and role?

    The SQL editor can't be used to test this — it bypasses RLS entirely, so it will 'succeed' whether or not a real policy exists. Instead test with the app's actual signed-in session (or temporarily add logging), and check whether the WITH CHECK expression (commonly something like auth.uid() = user_id) really matches the value being inserted for that column.

    What happens next

    • passed step 5, Policy logic looks correct — check which key/role the client is actually using
    • failed step 6, WITH CHECK doesn't match how the row is actually being inserted
    • unknown step 6, WITH CHECK doesn't match how the row is actually being inserted
  4. Step 4 · Root causeNo policy permits this INSERT at all

    With RLS enabled, Postgres denies every operation by default unless an applicable permissive policy exists for that operation and role. A table with only a SELECT policy (or no policies at all) rejects every INSERT from anon/authenticated roles — while postgres/service_role bypasses RLS entirely and never hits this, which is exactly why the SQL editor 'works fine'.

    What happens next

    • passed step 7, Add or correct the INSERT policy
  5. Step 5 · Root causePolicy logic looks correct — check which key/role the client is actually using

    If a matching INSERT policy exists and its WITH CHECK genuinely matches the row when tested as the signed-in user, the real app may not be sending that user's session at all — for example calling the API with the anon key without an authenticated session, or a server-side call quietly using the anon key instead of a user's JWT. In that case auth.uid() evaluates to null and can never match. Diagnosing that further is outside this playbook.

  6. Step 6 · Root causeWITH CHECK doesn't match how the row is actually being inserted

    Common mistakes: comparing auth.uid() (uuid) against a column of the wrong type, a column that isn't actually populated with the current user before the check runs, or an ownership column set by a trigger that runs after RLS is evaluated. Also check that WITH CHECK is set at all — USING alone controls visibility for SELECT/UPDATE/DELETE; INSERT is governed solely by WITH CHECK.

    What happens next

    • passed step 7, Add or correct the INSERT policy
  7. Step 7 · FixAdd or correct the INSERT policy

    Example: allow authenticated users to insert only rows that belong to themselves.

    Changes statesql

    Changes system or service state. Review before running.

    Adds a new row-level security policy to the orders table. It does not change any existing data or existing policies — it only permits INSERTs that satisfy the WITH CHECK condition for the authenticated role, going forward.

    CREATE POLICY "users can insert their own orders"
    ON public.orders
    FOR INSERT
    TO authenticated
    WITH CHECK (auth.uid() = user_id);

    What happens next

    • passed step 8, Re-check the policy exists, then re-test as a real signed-in user
  8. Step 8 · Verify the fixRe-check the policy exists, then re-test as a real signed-in user

    Confirm the new policy is present.

    Read-onlysql
    SELECT policyname, cmd, roles, qual, with_check
    FROM pg_policies
    WHERE schemaname = 'public' AND tablename = 'orders';

    Expected result

     policyname                     | cmd    | roles            | qual | with_check 
    ---------------------------------+--------+------------------+------+-------------------
     users can insert their own...  | INSERT | {authenticated}  |      | (auth.uid() = ...)
    (2 rows)

    What happens next

    • passed step 9, Resolved
    • failed step 5, Policy logic looks correct — check which key/role the client is actually using
    • unknown step 5, Policy logic looks correct — check which key/role the client is actually using
  9. Step 9 · EndResolved

    The signed-in user's own insert succeeds, and inserts for other users' rows are still correctly rejected.

Sources

Why this confidence?

What would strengthen it: 6 more independent reproductions. Reproductions from 3 more distinct environments.