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.
Applicatieguards en databasepolicies lossen verschillende fouten op
Ontwikkelaars behandelen autorisatie op applicatieniveau en Postgres Row-Level Security vaak alsof ze elkaar uitsluiten. In de praktijk vangen ze verschillende fouten af: server-side guards dwingen rolhiërarchie, rechtencontroles en auditlogging af voordat bedrijfslogica draait, terwijl RLS garandeert dat een vergeten WHERE workspace_id = ...-clausule nooit de rijen van alle tenants kan lekken.
Of je nu Supabase gebruikt of zelfgehoste Postgres met Drizzle, elke tabel die bij een tenant hoort hoort een niet-lege workspace_id-foreign key te hebben. Het artikel over workspace-grenzen legt uit waarom die scope altijd op de server moet worden afgeleid.
- Applicatieguards valideren sessieversheid, ledenrollen (
owner,admin,member) en functierechten. - Database-RLS-policies vormen een harde ondergrens bij directe clientquery's of ruwe SQL-helpers.
- Beide lagen steunen op één enkele bron van waarheid: de koppeltabel
workspace_members.
Controleer live lidmaatschap in plaats van verouderde cookies te vertrouwen
Een currentWorkspaceId opslaan in een browsercookie of clientheader is handig voor navigatie, maar een cookie is een voorkeur, geen bevoegdheid. Als een beheerder een teamgenoot uit een workspace verwijdert, bevat de browser van die gebruiker nog steeds de oude workspace-cookie.
Koppel bij elk verzoek de actieve sessie aan workspace_members. Als de lidmaatschapsrij verdwenen is, wijs het verzoek dan direct af in plaats van te wachten tot een JWT of sessiecookie verloopt. Het artikel over authenticatie voorbij het inlogformulier laat zien hoe identiteit doorstroomt naar deze controle.
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 };
}Schrijf snelle, geïndexeerde Row-Level Security-policies
Naïeve RLS-policies die auth.uid() rechtstreeks op elke rij aanroepen, dwingen Postgres om de functie per rij opnieuw te evalueren. Door (select auth.uid()) in een scalaire subquery te plaatsen kan de queryplanner de waarde cachen als een initPlan, en een samengestelde index op (workspace_id, user_id) houdt lidmaatschapszoekopdrachten binnen sub-milliseconde indexscans.
Je kunt kant-en-klare SELECT-, INSERT-, UPDATE- en DELETE-policies voor multi-tenant-tabellen genereren met de gratis Supabase RLS-policygenerator zonder handmatig standaard-SQL te schrijven.
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')
)
);Veelgestelde vragen over RLS en workspace-authenticatie
Teams die multi-tenant Postgres inrichten lopen tegen dezelfde randgevallen aan rond servicerollen, achtergrondtaken en prestaties.
- Omzeilen Drizzle ORM of Prisma RLS? — Ja, als je server verbindt met de
postgres-superuser of deservice_role-sleutel van Supabase. Daarom moeten server actions en API-routes altijdrequireWorkspaceViewer()aanroepen en elke query filteren opworkspaceId. - Waarom zet je
(select auth.uid())tussen haakjes binnen RLS-policies? — Het vertelt de Postgres-optimizer omauth.uid()één keer per statement te evalueren in plaats van één keer per gescande rij, wat trage sequential scans op grote tabellen voorkomt. - Hoe moeten achtergrondtaken toegang krijgen tot tenantgegevens? — Geef het geverifieerde
workspaceIdmee in de job-payload vanuit de geauthenticeerde serverroute en beperk lees- en schrijfacties van de worker tot datworkspaceId. - Hoe verifieer ik dat tenant-isolatie echt standhoudt? — Schrijf geautomatiseerde negatieve tests waarin Gebruiker A via web-, mobiele en API-routes het record-ID van Gebruiker B opvraagt of wijzigt, en controleer dat dit
403/404oplevert zonder gewijzigde rijen.