Что это решает
Любой продукт с личным кабинетом упирается в одинаковый набор задач ещё до первой строки бизнес-логики: где держать данные, как завести пользователей, кто имеет право прочитать чужую строку, откуда фронтенд возьмёт JSON. Supabase закрывает весь набор сразу — управляемый PostgreSQL, автоматический HTTP-интерфейс поверх схемы, сервис регистрации и входа, файловое хранилище, подписка на изменения строк. Прослойка, которая только перекладывает данные из базы в браузер, перестаёт быть нужной. Экономятся недели на восстановлении пароля, кнопках входа через внешних провайдеров, рукописных CRUD-эндпоинтах и разграничении доступа.
Как устроено
Фундамент — обычная база PostgreSQL, к которой можно подключиться через psql и посмотреть таблицы. Всё остальное навешано сверху отдельными сервисами:
- PostgREST читает схему базы и отдаёт HTTP-интерфейс. Таблица
ordersпревращается в/rest/v1/orders, фильтры пишутся в query-строке (?status=eq.paid&select=id,total), функция в базе вызывается через/rest/v1/rpc/имя_функции. Новая колонка появляется в API сразу после миграции. - Сервис аутентификации держит пользователей в схеме
authи выдаёт JWT. Умеет пароль, ссылку на почту, одноразовый код, вход через внешних провайдеров. - Realtime слушает журнал репликации Postgres и толкает изменения строк в WebSocket. Там же broadcast-каналы и presence для «кто сейчас на странице».
- Storage хранит файлы, а права на них описываются такими же SQL-политиками, как права на строки таблиц.
- Edge Functions — серверный код на Deno для того, что нельзя доверить браузеру: приём вебхука от платёжной системы, поход во внешний API с секретным ключом, отправка письма.
- Studio и CLI — веб-интерфейс к базе и локальный стек в Docker для разработки.
Путь запроса выглядит так. Браузер подставляет два заголовка: публичный ключ проекта (anon) и Authorization: Bearer <JWT пользователя>. PostgREST разбирает токен, переключает подключение на роль authenticated, кладёт содержимое токена в настройки сессии и переводит query-строку в SQL. Дальше в дело вступает Row Level Security — построчные политики внутри самой базы:
alter table orders enable row level security;
create policy "свои заказы видно"
on orders for select
using (auth.uid() = user_id);
create policy "свои заказы можно менять"
on orders for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
auth.uid() достаёт идентификатор пользователя из токена, auth.jwt() — всё остальное содержимое, включая роль и произвольные метаданные. using управляет тем, какие строки видны операции, with check — какие строки разрешено записать. Пока RLS у таблицы выключен, её содержимое доступно наружу любому, кто взял публичный ключ из исходников фронтенда. Включён без политик — таблица отвечает пустотой. Третьего состояния нет.
Проверка доступа живёт в базе, а не в коде приложения — новая таблица без политики открыта всему интернету.
Ключей два, и путать их нельзя. anon рассчитан на браузер и работает под ограничениями RLS. service_role обходит RLS полностью; ему место в переменных окружения серверного процесса и больше нигде — ни в мобильном приложении, ни в клиентском бандле, ни в репозитории.
Схема разворачивается миграциями. supabase init создаёт структуру проекта, supabase start поднимает весь стек локально в контейнерах, supabase db diff собирает SQL-файл из накопленных изменений, supabase db push накатывает его на облачный проект. Расширения Postgres ставятся обычным create extension — pgvector для эмбеддингов, pg_cron для расписаний, PostGIS для геометрии.
Полезные сценарии
- MVP с личным кабинетом: регистрация, вход через почту и внешнего провайдера, таблицы пользователя, доступ по RLS — без единого серверного маршрута.
- Внутренняя админка или дашборд для команды: видимость режется политиками по роли из токена, интерфейс собирается на любом фронтенд-фреймворке.
- Мобильное приложение: клиентские библиотеки под основные платформы ходят в тот же REST, что и веб.
- Живые счётчики и совместное редактирование: подписка на изменения таблицы через Realtime вместо опроса сервера по таймеру.
- База для поиска по документам: pgvector рядом с бизнес-таблицами, эмбеддинги и метаданные в одном месте, права на документы — теми же политиками.
- Приём вебхуков от платёжек и внешних систем: Edge Function проверяет подпись и пишет в базу под
service_role.
Ограничения
RLS-политики остаются единственным барьером между публичным ключом и данными. Забытая политика на новой таблице открывает её всем, и ни один линтер об этом не предупредит. Каждую таблицу приходится закрывать руками, а набор политик — тестировать так же, как код.
Политики выполняются на каждой строке выборки. Подзапрос в using, который ходит в другую таблицу, легко превращает обычный select в тяжёлую операцию. Лечится выносом проверки в функцию с security definer и индексом под неё, но помнить об этом придётся постоянно.
Сложная бизнес-логика в браузер не помещается. Расчёт цены, статусные переходы, всё, что нельзя показывать клиенту, уезжает в функции базы или в Edge Functions — и код проекта расползается по трём средам: SQL, Deno, фронтенд. Отладка такого расползания стоит дороже, чем один монолитный сервер.
Встроенный пулер соединений в транзакционном режиме плохо дружит с подготовленными выражениями и сессионными настройками. ORM, рассчитанный на постоянное подключение, ведёт себя непредсказуемо; для таких клиентов нужен отдельный режим пулера.
Realtime на журнале репликации обходится недёшево: массовое обновление таблицы порождает поток событий, а фильтрация происходит после доставки. Хранилище файлов не даёт трансформаций уровня специализированных CDN, кроме базового ресайза изображений.
Бесплатный тариф приостанавливает проекты без активности, а квоты на объём базы и трафик упираются в потолок раньше, чем кажется по демо. Самостоятельный хостинг возможен: это docker-compose на десяток сервисов, который придётся обновлять, мониторить и бэкапить своими силами.
Переезд с Supabase дешевле, чем с закрытых платформ, — данные остаются в Postgres, дамп переносится куда угодно. Но авторизация, политики и Edge Functions завязаны на конкретную реализацию и переписываются заново.
Как проверить результат
Первым делом — попытаться украсть данные собственным публичным ключом. Взять anon из фронтенда и без всякого JWT постучаться в каждую таблицу:
curl "https://<проект>.supabase.co/rest/v1/orders?select=*" \
-H "apikey: <anon>"
Пустой массив — политика работает. Строки в ответе — таблица открыта. Затем то же самое с токеном тестового пользователя: заводятся два аккаунта, у каждого своя запись, каждый должен видеть ровно одну.
Список таблиц без RLS выводится запросом к системному каталогу:
select relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
and not c.relrowsecurity;
Результат должен быть пустым. Этот запрос стоит повесить на проверку перед каждой выкладкой — он ловит забытую таблицу раньше, чем её найдут снаружи.
Дальше производительность политик: explain analyze на тяжёлой выборке от имени роли authenticated покажет, во что развернулось условие. Последовательное сканирование вместо индексного означает, что выражение в using неиндексируемо и на росте таблицы упрётся в таймаут.
Секреты проверяются поиском по собранному бандлу: строка service_role не должна встречаться ни в одном файле, уезжающем в браузер. Вход проверяется полным проходом на реальном почтовом ящике — регистрация, письмо, подтверждение, выход, восстановление пароля, повторный вход. Встроенный отправитель писем годится только для разработки, в проде подключается свой SMTP, и это отдельный пункт чек-листа перед запуском.
Локальный стек (supabase start) обязан подниматься с нуля и накатывать все миграции на чистую базу без единой ошибки. Как только это перестаёт работать, схема в облаке разъехалась с миграциями в репозитории, и ближайшая выкладка обнаружит расхождение уже на проде.
