1. Введение: RLS — это твой единственный бэкенд
В архитектуре Supabase база данных берет на себя роль контроллера доступа.
- Authentication (Кто ты?): Решает сервис GoTrue (Supabase Auth), выдавая JWT.
- Authorization (Что тебе можно?): Решает Postgres через RLS, валидируя этот JWT.
Запомни: По умолчанию в Supabase всё закрыто. Если ты включил RLS на таблице и не добавил политик — никто (кроме service_role) не увидит ни байта.
2. Анатомия RLS: USING vs WITH CHECK
Это то, на чем валятся мидлы. У политики есть два контекста выполнения. Если ты их путаешь, у тебя будут дыры в безопасности.
Синтаксис:
CREATE POLICY "policy_name"
ON public.table_name
FOR [SELECT | INSERT | UPDATE | DELETE | ALL]
TO [authenticated | anon | public]
USING ( ...expression... ) -- Для фильтрации существующих строк
WITH CHECK ( ...expression... ); -- Для валидации новых данныхРазбор полетов:
USING ( condition ):
- Используется для SELECT, DELETE, UPDATE.
- Определяет, какие строки видны пользователю.
- Пример: "Покажи мне строки, где
user_idравен моему ID".
WITH CHECK ( condition ):
- Используется для INSERT, UPDATE.
- Определяет, можно ли записать/изменить строку с такими данными.
- Пример: "Я пытаюсь создать пост. Проверь, что в поле
author_idя подставил свой ID, а не ID соседа".
Опасный момент в UPDATE:
Команда UPDATE использует и то, и другое.
USING: Проверяет, имеешь ли ты право трогать эту строку.
WITH CHECK: Проверяет, имеешь ли ты право превратить строку в то, что отправляешь.
3. Базовые паттерны (Best Practices)
Включаем RLS для всех таблиц:
alter table public.todos enable row level security;
alter table public.profiles enable row level security;Паттерн 1: "Каждый сам за себя" (User Data)
Самый частый кейс. Юзер видит и правит только свое.
create policy "CRUD own todos"
on public.todos
for all -- Применяется к select, insert, update, delete
to authenticated -- Только для залогиненных
using ( auth.uid() = user_id ) -- Вижу только свои
with check ( auth.uid() = user_id ); -- Пишу только от своего имениОбрати внимание: auth.uid() — это встроенная функция Supabase, извлекающая ID из JWT.
Паттерн 2: "Блог / Комментарии" (Public Read, Auth Write)
Читать могут все (даже гости), писать — только авторы.
-- Политика для чтения (гости + юзеры)
create policy "Public read"
on public.posts
for select
to public -- 'public' включает и anon, и authenticated
using ( true ); -- Доступно всем
-- Политика для авторов (создание)
create policy "Authors can create"
on public.posts
for insert
to authenticated
with check ( auth.uid() = author_id );
-- Политика для авторов (редактирование/удаление)
create policy "Authors can edit own"
on public.posts
for update
to authenticated
using ( auth.uid() = author_id )
with check ( auth.uid() = author_id ); -- Нельзя сменить автора при апдейте4. Сложные сценарии и Performance (SaaS / B2B)
Когда у вас появляется "Организация" или "Команда", простой auth.uid() перестает работать.
Проблема: JOIN в RLS убивает базу
Допустим, вы хотите проверить: "Пользователь состоит в организации, которой принадлежит этот документ".
Плохой код (Anti-pattern):
-- НЕ ДЕЛАЙТЕ ТАК НА HIGH LOAD
create policy "Access via Organization"
on public.documents
for select
using (
exists (
select 1 from public.organization_members om
where om.org_id = documents.org_id
and om.user_id = auth.uid()
)
);Почему это плохо: На каждую строку документа выполняется подзапрос к таблице organization_members. Если вы запрашиваете 1000 документов, вы делаете 1000 лишних селектов. Это положит базу.
Решение 1: Денормализация (Прагматичный подход)
Если пользователь редко меняет организации, проще хранить массив его org_ids прямо в JWT (через Custom Claims, как обсуждали ранее) или в кэширующей таблице.
Решение 2: Security Definer функция (Чистый подход)
Если логика сложная, выносим её в функцию, которая выполняется эффективно (по индексу).
create or replace function public.has_org_access(_org_id uuid)
returns boolean as $$
begin
return exists (
select 1 from public.organization_members
where org_id = _org_id and user_id = auth.uid()
);
end;
$$ language plpgsql security definer stable; -- STABLE кэширует результат в рамках транзакции!
-- Политика
create policy "Access via Org Func"
on public.documents
for select
using ( public.has_org_access(org_id) );Пометка stable критически важна для оптимизатора Postgres.
5. Как дебажить RLS и не сойти с ума
Ты написал политику, а данных нет. Или есть лишние. Как проверить, не ломая прод?
Используй SQL Editor в дашборде Supabase или локальный psql. Мы можем притвориться любым юзером.
Симуляция запроса:
-- 1. Начинаем транзакцию, чтобы ничего не сломать
begin;
-- 2. Притворяемся конкретным юзером (берем реальный UUID из auth.users)
select set_config('request.jwt.claim.sub', 'd0a5e821-68f7-43c3-9823-xxxxxxxxxxxx', true);
select set_config('role', 'authenticated', true);
-- 3. Пробуем сделать запрос, который делает наш фронтенд
select * from public.todos;
-- 4. Смотрим результат. Если пусто — политика не работает.
-- 5. Пробуем вставить данные
insert into public.todos (title, user_id)
values ('Test Todo', 'd0a5e821-68f7-43c3-9823-xxxxxxxxxxxx');
-- 6. Откатываем изменения
rollback;6. Auth Settings: Что нужно включить в дашборде
Помимо SQL, есть пара галочек в UI Supabase Authentication -> Providers / Settings, которые влияют на БД:
- Enable Email Confirmations:
- Dev: Выключить. Бесит подтверждать почту при каждом тесте.
- Prod: Включить. Иначе боты заспамят базу "мертвыми" юзерами.
- User ID Generation:
- Всегда используйте UUID. Supabase делает это по умолчанию. Никогда не используйте integer auto-increment для user_id в распределенных системах.
- Schema
auth:
- Никогда не модифицируйте таблицы в схеме
authвручную (добавлять колонки и т.д.). Supabase обновляет эту схему автоматически, и ваши изменения затрутся или сломают миграцию. Используйтеpublic.profilesи связь поid.
7. Итоговое мнение
RLS — это мощь, но с ней легко перемудрить.
Мои золотые правила:
- KISS: Политика должна быть настолько простой, насколько возможно. Идеально:
uid = auth.uid().
- Индексы: Все колонки, участвующие в RLS (например,
user_id,org_id,status), обязаны быть проиндексированы. Иначе RLS превратит любой запрос в Full Table Scan.
- Безопасность по умолчанию: Сначала
ENABLE RLS, потом пишем код.
Нужен похожий проект?
Свяжитесь с нами для бесплатной консультации.
