Alle skills
Datav1.0.0supabase-migrations

Supabase-migraties

RLS, grants en de schema-reload

Self-hosted Supabase faalt anders dan de gehoste variant, en het faalt stil. De migratie slaagt, de API geeft een lege array terug en nergens staat een foutmelding. Deze skill is de volgorde die dat voorkomt: idempotente DDL, eerst grants dan policies, en die ene notify-regel onderaan zonder welke je tabel wel in Postgres bestaat en niet over de API.

MITCCaiCxAgCuOc
PostgresRLSPostgRESTMigraties
Installeren

Zet hem in je eigen agent

Kies je client. Het commando staat er klaar voor.

claude code
/plugin marketplace add ASNNetworks/floh-skills
/plugin install supabase-migrations@floh-skills

Typ deze twee in een Claude Code-sessie. De skill is meteen daarna beschikbaar; herstarten is niet nodig.

Wat je nodig hebt

  • psql, of docker exec naar je database-container
  • Optioneel curl voor de PostgREST-check
Demo

Zo ziet het eruit

Een echte sessie, stap voor stap afgespeeld.

claude-code
Wat het doet

De kern

De notify-regel

PostgREST cachet het schema bij het opstarten. Zonder notify pgrst, reload schema bestaat je tabel niet over de API en wijst elk symptoom naar je clientcode.

Grants en policies in de goede volgorde

RLS zonder grant geeft permission denied. Een grant zonder RLS zet elke rij open. Allebei nodig, en in die volgorde.

Idempotent, altijd

if not exists, en drop policy voor create policy. Een migratie die niet twee keer kan draaien, draait ooit twee keer.

Verifieert tegen de database

verify.sh controleert of de tabel bestaat, RLS aanstaat, er policies zijn, de grants kloppen en PostgREST hem daadwerkelijk serveert. Niet tegen het bestand, tegen de database.

Wanneer

Wanneer je hem pakt

Een tabel toevoegen

Het sjabloon geeft je tabel, index, RLS, grants, twee policies en de notify-regel in één keer goed.

Een RPC schrijven

security definer met een vastgezet search_path, en revoke van public voor je aan een rol grant. Een functie is standaard voor iedereen uitvoerbaar, en dat wil je bijna nooit.

Een migratie reviewen

Vaste controle op destructieve DDL, ontbrekende where en een alter column zonder using, voordat het tegen productie draait.

Debuggen waarom de API niets teruggeeft

De tabel met symptomen en oorzaken wijst de vier klassieke gevallen meteen aan. Meestal is het geen querybug.

De inhoud

Lees hem voordat je hem draait

Dit is precies wat er in de map komt te staan. Geen verrassingen, geen verborgen stappen.

2 bestanden

SKILL.md
---
name: supabase-migrations
description: Write and review Postgres migrations for a self-hosted Supabase project — numbering, RLS policies, grants, idempotency and the PostgREST schema reload. Use when adding a table, changing a column, writing an RPC, or reviewing a migration before it runs against production.
---

# Supabase migrations

Self-hosted Supabase fails differently from managed Supabase. The failures are quiet: the
migration succeeds, the API returns an empty array, and nothing anywhere reports an error.
This skill is the checklist that catches those before they ship.

## When to use this

Writing a migration, reviewing one, or debugging "the table exists but the API says it
does not".

## The order that matters

1. **Take the next free number.** Never reuse one, never renumber an applied migration.

   ```bash
   ls migrations/ | sort -V | tail -1
   ```

2. **Write the DDL idempotently.** `create table if not exists`, `add column if not
   exists`, `drop policy if exists` before `create policy`. A migration that cannot be
   run twice will eventually be run twice.

3. **Grant, then policy, then enable.** Row Level Security without a grant returns
   permission-denied; a grant without RLS exposes every row. Both, in that order.

4. **End with the schema reload.**

   ```sql
   notify pgrst, 'reload schema';
   ```

   This is the one that gets forgotten. PostgREST caches the schema at boot. Without this
   line the table exists in Postgres and does not exist over the API, and every symptom
   points at your client code.

5. **Verify against the database, not against the file.**

   ```bash
   bash scripts/verify.sh <table_name>
   ```

## Template

```sql
-- 00NN_<what_it_does>.sql
-- <one line: why this exists>

create table if not exists public.thing (
  id          uuid primary key default gen_random_uuid(),
  slug        text unique not null,
  status      text not null default 'draft'
                check (status in ('draft', 'published', 'archived')),
  payload     jsonb not null default '{}'::jsonb,
  created_at  timestamptz not null default now(),
  updated_at  timestamptz not null default now()
);

create index if not exists thing_status_idx on public.thing (status)
  where status = 'published';

alter table public.thing enable row level security;

grant select on public.thing to anon, authenticated;
grant all    on public.thing to service_role;

drop policy if exists "thing: public reads published" on public.thing;
create policy "thing: public reads published"
  on public.thing for select
  to anon, authenticated
  using (status = 'published');

drop policy if exists "thing: service_role full access" on public.thing;
create policy "thing: service_role full access"
  on public.thing for all
  to service_role
  using (true) with check (true);

notify pgrst, 'reload schema';
```

## RPCs

A function the API can call needs `security definer` only when it must bypass RLS, and
then it needs a pinned `search_path` or it is an escalation path:

```sql
create or replace function public.increment_counter(p_slug text, p_kind text)
returns void
language plpgsql
security definer
set search_path = public
as $$
begin
  update public.thing
     set payload = jsonb_set(
           payload, array['counts', p_kind],
           to_jsonb(coalesce((payload->'counts'->>p_kind)::int, 0) + 1)),
         updated_at = now()
   where slug = p_slug;
end;
$$;

revoke all on function public.increment_counter(text, text) from public;
grant execute on function public.increment_counter(text, text) to anon, authenticated;
```

`revoke ... from public` first, then grant to the roles that need it. Creating a function
grants execute to `public` by default, which is almost never what you want for a
`security definer`.

## Failure modes, and what they actually mean

| Symptom | Cause |
|---------|-------|
| API returns `[]`, table has rows | RLS on, no policy for that role. Not a query bug |
| API returns "relation does not exist" | Missing `notify pgrst, 'reload schema'` |
| Works as service_role, not as anon | Grant present, policy missing (or the reverse) |
| Insert silently drops rows | Policy has `using` but no `with check` |
| Migration passes locally, fails in prod | Different extension set. Check `gen_random_uuid` |

## Before it runs against production

- Read the whole file top to bottom, out of the diff.
- Confirm nothing is destructive: no bare `drop table`, no `alter column ... type` without
  a `using`, no `delete from` without a `where`.
- Confirm it ends with the notify line.
- Run it against a scratch database first if one exists.
- Never run it as part of the same step that writes it.

## Files

- `scripts/verify.sh` — post-migration verification: table exists, RLS on, policies
  present, and the anon role can actually read what it should.
Versies

Wat er veranderd is

  1. v1.0.0huidig
    • Eerste versie: migratiesjabloon, RPC-patroon, faaltabel en de checklist voor productie.
    • verify.sh controleert bestaan, RLS, policies, grants en de PostgREST-cache.

Werkt hij voor je?

Vragen

Over Supabase-migraties

Wat mensen vragen voordat ze hem installeren.

Zo een, maar dan voor jouw werk?

Elke skill hier komt uit werk dat ik echt deed. Zit er iets in jouw proces dat je telkens opnieuw uitlegt aan een agent, dan is dat precies een skill.