Row Level Security (RLS) in PostgreSQL is one of the most powerful defense-in-depth mechanisms in modern software development. When configured properly, the database engine guarantees that a tenant can only view their own rows—even if an application endpoint suffers from an authorization or SQL injection flaw.
However, when developers build backends using Cursor, Claude, or ChatGPT, RLS policies are almost always either omitted completely or implemented with fatal logical bypasses.
Here are the 5 most frequent RLS architectural failures we uncover during security audits at Bryxe Shield.
1. The auth.uid() IS NOT NULL Universal Leak Trap
When developers ask AI: *"Allow registered users to view team workspaces"*, AI frequently suggests:
-- CATASTROPHIC FLAW
CREATE POLICY "authenticated_access" ON workspaces
FOR SELECT
USING (auth.uid() IS NOT NULL);Why is this catastrophic?
auth.uid() IS NOT NULL evaluates to TRUE for any authenticated user on your entire platform.
The moment any user registers an account, they can dump the workspaces, financial projections, and private records of every other customer on the platform.
The Correct Tenant Scoping:
CREATE POLICY "tenant_workspace_select" ON workspaces
FOR SELECT
USING (
id IN (
SELECT workspace_id FROM workspace_memberships WHERE user_id = auth.uid()
)
);2. Using the Service Role Key to Circumvent RLS Errors
When developers encounter empty array responses when querying Supabase inside Next.js Server Components, they ask AI: *"Why is my Supabase query returning empty array?"*.
AI's most common recommendation:
// The dangerous AI advice:
import { createClient } from '@supabase/supabase-js';
// "Fix: use service_role key to bypass RLS!"
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);The service_role key completely disables Row Level Security.
The moment you query through supabaseAdmin without manually writing where: { orgId: user.orgId } on every single query in your codebase, you have dismantled your database security layer. One missed where clause in a search route causes a total tenant data breach.
3. RLS Configured for SELECT, Forgotten for UPDATE and DELETE
AI models often generate:
CREATE POLICY "user_view_own" ON notes
FOR SELECT
USING (user_id = auth.uid());And they consider the table secured!
If you do not create explicit policies for UPDATE and DELETE, and someone later creates a general policy:
CREATE POLICY "allow_authed_all" ON notes
FOR ALL
USING (auth.uid() IS NOT NULL);Now, any authenticated user can overwrite or delete notes belonging to any other user simply by passing their UUID.
Always define explicit policies for every CRUD operation: SELECT, INSERT, UPDATE, and DELETE.
4. SQL Injection via Prisma $queryRawUnsafe Bypassing ORM Scoping
When AI attempts to write complex filters or full-text search queries in Prisma, it frequently resorts to raw SQL queries:
export async function searchNotes(query: string, userId: string) {
// Vulnerable to SQL injection:
return await prisma.$queryRawUnsafe(
`SELECT * FROM notes WHERE user_id = '${userId}' AND title ILIKE '%${query}%'`
);
}An attacker inputs ' OR 1=1 --, dumping all records across all users. Always use parameterized queries ($queryRaw).
5. Postgres Views Bypassing Base Table RLS
In PostgreSQL versions prior to 15 (and by default if unspecified), views execute with the permissions of the view creator (security_definer), completely ignoring the Row Level Security policies of underlying tables.
Always declare views with security_invoker = true:
CREATE VIEW team_overview
WITH (security_invoker = true)
AS SELECT * FROM workspaces;The Production Blueprint for Multi-Tenant RLS
Here is the bulletproof schema pattern for multi-tenant Postgres tables:
-- 1. Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
-- 2. Scoped SELECT
CREATE POLICY "projects_select" ON projects
FOR SELECT
USING (org_id = (SELECT org_id FROM users WHERE id = auth.uid()));
-- 3. Scoped INSERT
CREATE POLICY "projects_insert" ON projects
FOR INSERT
WITH CHECK (org_id = (SELECT org_id FROM users WHERE id = auth.uid()));
-- 4. Scoped UPDATE
CREATE POLICY "projects_update" ON projects
FOR UPDATE
USING (org_id = (SELECT org_id FROM users WHERE id = auth.uid()))
WITH CHECK (org_id = (SELECT org_id FROM users WHERE id = auth.uid()));
-- 5. Scoped DELETE
CREATE POLICY "projects_delete" ON projects
FOR DELETE
USING (org_id = (SELECT org_id FROM users WHERE id = auth.uid()));Audit your Supabase and Postgres schema automatically with Bryxe Shield at bryxe.app/tools.
