Should I use Postgres Row-Level Security or application-level workspace checks?
It depends on how your application connects to Postgres. If browser or mobile clients query the database directly through PostgREST, Row-Level Security (RLS) is mandatory. If all traffic flows through a server-side TypeScript boundary, explicit membership checks on every query are your primary defense, and RLS serves as defense-in-depth against forgotten WHERE clauses. Combine live session-to-membership resolution with indexed workspace_id policies. supastack enforces server-side workspace guards across web, mobile, and desktop, and provides a free RLS policy generator for Supabase tables.
Проверки приложения и политики базы данных защищают от разных сбоев
Разработчики часто противопоставляют авторизацию на уровне приложения и Postgres Row-Level Security (RLS). На практике они ловят разные ошибки: серверные проверки контролируют иерархию ролей, права на функции и журнал аудита до запуска бизнес-логики, а RLS гарантирует, что забытое условие WHERE workspace_id = ... никогда не вернёт строки всех клиентов сразу.
Используете ли вы Supabase или собственный Postgres с Drizzle, каждая таблица с данными клиентов должна содержать непустой внешний ключ workspace_id. Статья о границах воркспейса объясняет, почему этот контекст всегда должен определяться на сервере.
- Проверки приложения валидируют актуальность сессии, роли участников (
owner,admin,member) и доступность функций. - Политики RLS в базе данных служат жёстким предохранителем при прямых клиентских запросах или использовании необработанного SQL.
- Оба уровня опираются на единый источник истины — связующую таблицу
workspace_members.
Проверяйте актуальное членство вместо доверия устаревшим cookie
Хранить currentWorkspaceId в cookie браузера или заголовке клиента удобно для навигации, но cookie — это предпочтение интерфейса, а не право доступа. Если администратор удалит сотрудника из воркспейса, в браузере этого пользователя всё ещё останется старый cookie.
При каждом запросе соединяйте активную сессию с таблицей workspace_members. Если строка членства удалена, немедленно отклоняйте запрос, не дожидаясь истечения срока действия JWT или сессионного cookie. В статье об аутентификации за пределами формы входа подробно показано, как идентификация переходит в эту проверку.
export async function requireWorkspaceViewer(requestedWorkspaceId?: string) {
const session = await requireAuthenticatedSession();
const membership = await db.query.workspaceMembers.findFirst({
where: and(
eq(workspaceMembers.userId, session.user.id),
requestedWorkspaceId
? eq(workspaceMembers.workspaceId, requestedWorkspaceId)
: undefined,
),
with: { workspace: true },
});
if (!membership) throw new ForbiddenError("Not a member of this workspace");
return { user: session.user, workspace: membership.workspace, role: membership.role };
}Пишите быстрые индексированные политики Row-Level Security
Наивные политики RLS, вызывающие auth.uid() напрямую для каждой строки, заставляют Postgres вычислять функцию на каждом кортеже. Обёртка (select auth.uid()) в скалярный подзапрос позволяет планировщику закэшировать значение как initPlan, а составной индекс по (workspace_id, user_id) удерживает проверку членства в пределах субмиллисекундного индексного поиска.
Вы можете сгенерировать готовые к запуску политики SELECT, INSERT, UPDATE и DELETE для мультитенантных таблиц с помощью бесплатного генератора политик RLS для Supabase, не набирая шаблонный SQL вручную.
CREATE INDEX IF NOT EXISTS idx_workspace_members_lookup
ON workspace_members (workspace_id, user_id);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "workspace_members_manage_documents"
ON documents FOR ALL TO authenticated
USING (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
)
)
WITH CHECK (
workspace_id IN (
SELECT wm.workspace_id FROM workspace_members wm
WHERE wm.user_id = (SELECT auth.uid())
AND wm.role IN ('owner', 'admin', 'member')
)
);Частые вопросы о RLS и авторизации воркспейсов
Команды, настраивающие мультитенантный Postgres, сталкиваются с одними и теми же пограничными случаями вокруг сервисных ролей, фоновых задач и производительности.
- Обходят ли Drizzle ORM или Prisma правила RLS? — Да, если ваш сервер подключается от имени суперпользователя
postgresили с ключомservice_roleв Supabase. Именно поэтому серверные действия и маршруты API обязаны вызыватьrequireWorkspaceViewer()и ограничивать каждый запрос поworkspaceId. - Зачем оборачивать
(select auth.uid())в скобки внутри политик RLS? — Это подсказывает оптимизатору Postgres вычислитьauth.uid()один раз на весь запрос, а не на каждую проверяемую строку, предотвращая медленное последовательное сканирование больших таблиц. - Как фоновые задачи должны обращаться к данным арендатора? — Передавайте проверенный
workspaceIdв полезной нагрузке задачи из аутентифицированного серверного маршрута и ограничивайте чтение и запись воркера этимworkspaceId. - Как убедиться, что изоляция арендаторов действительно работает? — Напишите автоматические негативные тесты, в которых Пользователь A запрашивает или изменяет ID записи Пользователя B через веб-, мобильные и API-маршруты, и проверьте возврат
403/404при нуле изменённых строк.