Грабли переезда
Собрали по разборам инцидентов. Каждый пункт стоил кому-то от десяти минут до получаса, и ни один не виден из кода.
1. Роль в миграции — только через layero.role()
Имя роли содержит идентификатор базы, поэтому в миграцию его не впишешь. Найти по шаблону кажется естественным:
-- 🚨 ТАК НЕЛЬЗЯ
SELECT rolname FROM pg_roles WHERE rolname ~ '^u_.*_anon$' LIMIT 1;
Список ролей общий на весь сервер баз. Шаблон совпадёт с ролями чужих
арендаторов, и GRANT уедет в чужую базу: однажды так права на таблицы одного
продукта достались роли чужой организации, и заметили это только потому, что
свои же запросы продолжали отвечать «таблица не открыта».
-- Так правильно
GRANT SELECT ON public.entries TO layero.role('anon');
Функция layero.role('anon'|'auth'|'svc') есть в каждой базе с включённым API.
Проверить выданное можно так:
SELECT layero.role('anon'), has_table_privilege(layero.role('anon'), 'public.entries', 'SELECT');
2. INSERT … RETURNING проходит политику чтения
supabase-js по умолчанию делает .insert().select(), то есть просит вернуть
вставленную строку. Возврат — это чтение, и он идёт через политику SELECT.
Классический случай: участника рабочего пространства добавляет триггер
AFTER INSERT, но право прочитать возвращаемую строку RLS проверяет до того,
как триггер отработает. Итог — new row violates row-level security policy,
хотя та же вставка без возврата проходит. Ошибка звучит как проблема со
вставкой, а причина в чтении.
Лечится политикой чтения, которая пускает и автора:
CREATE POLICY workspaces_read ON public.workspaces
FOR SELECT TO PUBLIC
USING (owner_id = auth.uid() OR EXISTS (
SELECT 1 FROM public.members m
WHERE m.workspace_id = id AND m.user_id = auth.uid()));
Либо вставкой без возврата: db.from("workspaces").insert(row) без .select().
3. Функции живут только в схеме api
Таблицы шлюз ищет в api, public и app, функции — только в api.
Accept-Profile на это не влияет. Подробности — Функции и RPC.
4. Имя аргумента функции — публичный контракт
Ключи JSON раскладываются по одноимённым аргументам, поэтому имя аргумента
видно снаружи и попадает в код каждого клиента. Внутри функции оно же
конфликтует с одноимённой колонкой (column reference "token" is ambiguous),
и локальная переменная не спасает — она подставляется и в ON CONFLICT.
Рецепт — там же, в Функции и RPC.
5. Пустой ответ — это работающая политика
[] в ответе значит «грант есть, строк по вашей политике нет». Это не ошибка
и не поломка доступа. Отличать полезно так:
| Ответ | Причина |
|---|---|
unknown_table | таблицы нет в каталоге вашей роли — почти всегда грант ушёл не той роли |
no_table_grant | таблица есть, права нет; готовый GRANT лежит в hint |
[] | права есть, политика не пускает строки |
Проверьте в панели, что политика держит чужого: запрос выполнится от роли ключа, а транзакция откатится.