Skip to content
AI Infrastructure
AI Infrastructure12 min read0 views

Postgres RLS for Multi-Tenant AI: Tenant Isolation Done Right (2026)

Move tenant filtering out of your app and into Postgres. Row Level Security gives you database-enforced isolation that survives ORM bugs, hand-rolled SQL, and AI agents writing queries on the fly.

TL;DR — When AI agents start writing SQL on behalf of users, app-layer tenant filtering becomes a liability. RLS pushes the tenant_id check into the database where no agent can route around it. Set up correctly, it adds <5% query overhead.

What you'll build

A multi-tenant schema where every AI agent query — direct, ORM-generated, or LLM-emitted — is automatically scoped to the calling tenant via session-level GUC variables and RLS policies.

Schema

CREATE TABLE conversations (
  id BIGSERIAL PRIMARY KEY,
  tenant_id UUID NOT NULL,
  agent_id UUID NOT NULL,
  transcript TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE conversations FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON conversations
  USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);

CREATE INDEX conversations_tenant_idx ON conversations (tenant_id, created_at DESC);

Architecture

flowchart TD
  REQ[HTTP request] --> AUTH[Auth middleware]
  AUTH --> SET[SET app.tenant_id GUC]
  SET --> APP[App + Prisma]
  APP --> AGENT[AI agent SQL]
  AGENT --> PG[(Postgres + RLS)]
  PG -->|Policy enforced| ROWS[Only tenant rows]

Step 1 — Set the tenant per request

// Express / Next middleware
import { prisma } from "@/lib/db";

export async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  return prisma.$transaction(async (tx) => {
    await tx.$executeRawUnsafe(
      `SET LOCAL app.tenant_id = '${tenantId}'`,
    );
    return fn();
  });
}

SET LOCAL is critical — it scopes the GUC to the current transaction so pooled connections don't leak.

Step 2 — Use a non-superuser app role

CREATE ROLE app_user NOINHERIT LOGIN PASSWORD '...';
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;

Superusers bypass RLS — never connect your app as one.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →

Step 3 — FORCE RLS on table owners

By default, table owners bypass RLS. FORCE ROW LEVEL SECURITY makes the policy apply to everyone except true superusers.

Step 4 — Test with two tenants

SET app.tenant_id = '11111111-1111-1111-1111-111111111111';
SELECT count(*) FROM conversations;  -- only tenant A

SET app.tenant_id = '22222222-2222-2222-2222-222222222222';
SELECT count(*) FROM conversations;  -- only tenant B

RESET app.tenant_id;
SELECT count(*) FROM conversations;  -- 0 rows (or error if NOT NULL cast)

Step 5 — Handle WITH CHECK for INSERTs

The WITH CHECK half ensures inserts also satisfy the policy. Without it, an attacker could write rows for a different tenant.

INSERT INTO conversations (tenant_id, agent_id, transcript)
VALUES ('22222222-...'::uuid, ...);
-- ERROR: new row violates row-level security policy

Step 6 — Index policies for performance

RLS is a transparent AND tenant_id = $1 appended to every query. Make sure every queried table has an index that leads with tenant_id.

CREATE INDEX conversations_tenant_created_idx
  ON conversations (tenant_id, created_at DESC);

Pitfalls

  • Forgetting FORCE ROW LEVEL SECURITY — table owner bypasses by default.
  • Using SET (not SET LOCAL) in a pooled connection — the GUC leaks to the next request.
  • Connecting as superuser — RLS doesn't apply. Always use a constrained role.
  • Casting in policytenant_id = current_setting(...)::uuid is fine, but mismatched types silently disable index usage.
  • Subqueries that leak — security-definer functions can bypass RLS; audit them carefully.

CallSphere production note

OneRoof — CallSphere's property-management vertical — relies on RLS across 115+ DB tables to keep landlords from ever seeing each other's tenants, leases, or conversation history. Healthcare and Behavioral Health get the same treatment plus a separate healthcare_voice Prisma schema for HIPAA defense-in-depth; UrackIT layers RLS over Supabase + ChromaDB. 37 agents · 90+ tools · 6 verticals. Plans: $149/$499/$1,499 — 14-day trial, 22% affiliate.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

FAQ

Q: RLS or schema-per-tenant? RLS for >50 tenants. Schema-per-tenant scales tooling badly past that.

Q: Performance overhead? 2-5% on indexed queries when policy uses current_setting + indexed column. Measure with EXPLAIN.

Q: Can RLS replace permissions in my app? RLS handles row visibility. App-layer authz still owns RBAC, audit logging, and rate limits.

Q: Does Prisma support RLS? Yes — set the GUC in a transaction, then call your Prisma queries inside it.

Q: How do I let admins see all tenants? Use a separate role with BYPASSRLS, or include OR is_admin() in the policy.

Sources

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Agentic AI

Input and Output Guardrails in the OpenAI Agents SDK: A Production Pattern (2026)

Stop the agent BEFORE it does the wrong thing. How to wire input and output guardrails in the OpenAI Agents SDK with cheap classifiers and an eval suite that proves they work.

Agentic AI

Safety Evaluation for Agents: Jailbreak, Prompt Injection, and Tool-Misuse Test Suites in 2026

How to build a safety eval pipeline that runs known jailbreak corpora, prompt-injection attacks, and tool-misuse scenarios on every release — and gates merges on it.

Agentic AI

Building Your First Agent with the OpenAI Agents SDK in 2026: A Hands-On Walkthrough

Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.

Agentic AI

LangGraph Checkpointers in Production: Durable, Resumable Agents with Eval Replay

Use LangGraph's checkpointer to make agents resumable across crashes and human-in-the-loop pauses, then replay any checkpoint into your eval pipeline.

Agentic AI

LangGraph State-Machine Architecture: A Principal-Engineer Deep Dive (2026)

How LangGraph's StateGraph, channels, and reducers actually work — with a working multi-step agent, eval hooks at every node, and the patterns that survive production.

Agentic AI

Multi-Agent Handoffs with the OpenAI Agents SDK: The Pattern That Actually Scales (2026)

Handoffs done right — when one agent should hand control to another, how to preserve context, and how to evaluate the handoff decision itself.