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.
Guardas da aplicação e políticas da base de dados resolvem falhas diferentes
Os programadores tratam muitas vezes a autorização ao nível da aplicação e a Row-Level Security do Postgres como mutuamente exclusivas. Na prática, intercetam bugs diferentes: as guardas no servidor aplicam hierarquia de papéis, verificações de direitos e registos de auditoria antes da lógica de negócio correr, enquanto o RLS garante que uma cláusula WHERE workspace_id = ... esquecida nunca expõe as linhas de todos os tenants.
Quer uses Supabase ou Postgres autoalojado com Drizzle, todas as tabelas pertencentes a um tenant devem ter uma chave estrangeira workspace_id não nula. O artigo sobre fronteiras do workspace explica porque é que esse âmbito deve ser sempre derivado no servidor.
- As guardas da aplicação validam a validade da sessão, os papéis dos membros (
owner,admin,member) e os direitos de funcionalidades. - As políticas RLS da base de dados funcionam como uma barreira final quando são usadas queries diretas do cliente ou helpers de SQL bruto.
- Ambas as camadas dependem de uma única fonte de verdade: a tabela de junção
workspace_members.
Resolve a pertença em tempo real em vez de confiar em cookies desatualizados
Guardar um currentWorkspaceId num cookie do navegador ou cabeçalho do cliente é conveniente para a navegação, mas um cookie é uma preferência, não uma autorização. Se um administrador remover um colega de um workspace, o navegador desse utilizador continua a guardar o cookie antigo.
Cruza a sessão ativa com workspace_members em cada pedido. Quando a linha de pertença já não existir, rejeita o pedido de imediato em vez de esperar que um JWT ou cookie de sessão expire. O artigo sobre autenticação além do formulário de login mostra como a identidade alimenta esta verificação.
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 };
}Escreve políticas de Row-Level Security rápidas e indexadas
Políticas RLS ingénuas que chamam auth.uid() diretamente em cada linha obrigam o Postgres a reavaliar a função por cada registo. Envolver (select auth.uid()) numa subquery escalar permite ao planeador colocar o valor em cache como um initPlan, e um índice composto em (workspace_id, user_id) mantém as pesquisas de pertença abaixo do milissegundo.
Podes gerar políticas SELECT, INSERT, UPDATE e DELETE prontas a executar para tabelas multi-tenant usando o gerador gratuito de políticas RLS para Supabase sem escrever SQL repetitivo à mão.
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')
)
);Perguntas frequentes sobre RLS e autenticação de workspace
As equipas que configuram Postgres multi-tenant encontram os mesmos casos limite em torno de service roles, tarefas em segundo plano e desempenho.
- O Drizzle ORM ou o Prisma contornam o RLS? — Sim, se o teu servidor se ligar com o superutilizador
postgresou com a chaveservice_roledo Supabase. É por isso que as server actions e rotas de API devem chamarrequireWorkspaceViewer()e filtrar cada query porworkspaceId. - Porquê envolver
(select auth.uid())entre parênteses nas políticas RLS? — Indica ao otimizador do Postgres para avaliarauth.uid()uma única vez por instrução em vez de uma vez por linha analisada, evitando sequential scans lentos em tabelas grandes. - Como devem as tarefas em segundo plano aceder aos dados do tenant? — Passa o
workspaceIdvalidado no payload do job a partir da rota autenticada do servidor e limita as leituras e escritas do worker a esseworkspaceId. - Como verifico que o isolamento entre tenants funciona mesmo? — Escreve testes negativos automatizados em que o Utilizador A pede ou altera o ID de registo do Utilizador B através de rotas web, mobile e API, e confirma que devolvem
403/404com zero linhas alteradas.