# Pattern 10 — Multi-tenant INSERT enforcement

## What this is

The INSERT companion to pattern 09. Most tutorials stop at SELECT. An INSERT without `WITH CHECK` lets authenticated users insert rows claiming any `org_id` — including another tenant's.

## The WITH CHECK gap

RLS for SELECT uses `USING`. RLS for INSERT uses `WITH CHECK`. They're separate clauses and both are required:

```sql
-- Only has USING — SELECT is protected, INSERT is not
create policy "broken_select_only"
  on documents for select to authenticated
  using (org_id = (select org_id from users where id = auth.uid()));

-- What actually gates INSERT writes:
create policy "tenant_insert"
  on documents for insert to authenticated
  with check (
    org_id = (select org_id from users where id = auth.uid())
  );
```

Without the INSERT policy with `WITH CHECK`, an attacker (or a bug) can do:

```ts
// Supabase client — inserts into another tenant's namespace
supabase.from('documents').insert({ org_id: 'other-tenant-id', title: 'stolen' })
```

## Even better: trigger-based stamping

If callers must supply `org_id`, they can get it wrong. The trigger approach removes the responsibility from the API surface:

1. API callers don't send `org_id`
2. The trigger reads the authenticated user's org from the `users` table
3. It stamps `org_id` automatically before the row is inserted

No caller error. No adversarial injection. The database enforces the context.

## Testing

```sql
-- As an authenticated user, attempt a cross-tenant insert
-- (Replace with an org_id that belongs to a different tenant)
insert into documents (org_id, title)
values ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'cross-tenant attempt');

-- Expected result:
-- ERROR: new row violates row-level security policy for table "documents"
```

## Common mistake

Seeing a passing SELECT test and assuming writes are also protected. SELECT and INSERT are independent RLS checks. Test both.
