EnorexaEnorexa
Back to blog

Engineering

Multi-Tenant AI Security: Why ORM Filters Leak Customer Data

October 22, 20264 min readBy Enorexa Team

Giving an AI agent a shared database connection and trusting a system prompt to keep customers separated is a data breach waiting for a trigger. We audited a Series A B2B SaaS platform last month where an autonomous billing agent had 18 tools wired into Prisma ORM. Every single tool relied on application code passing where: { organizationId } into the query builder. One indirect prompt injection buried inside a parsed PDF invoice tricked the agent into calling a fallback search tool where a developer had forgotten that single filter on a joined credit_memos table. In 140 milliseconds, Tenant A's finance bot summarized $420,000 of Tenant B's confidential rebate terms. Prompt guardrails cannot fix broken data boundaries.

Why Application-Layer Filters Fail Autonomous Tool Loops

Traditional CRUD apps are deterministic. A user hits GET /api/v1/invoices, your middleware validates their JWT, and a single tested controller runs one predictable SQL query. AI agents invert that execution model. An agent running a multi-step tool loop dynamically chains four to ten internal functions based on unstructured user input, webhook payloads, and retrieved vector chunks. When you maintain 40+ agent tools across a fast-moving TypeScript or Python codebase, relying on every engineer to manually append AND tenant_id = $1 to every subquery, raw CTE, and vector similarity join is pure wishful thinking. Worse, text-to-SQL and dynamic analytics tools generate queries on the fly. If your security boundary lives in your ORM or your system prompt, your blast radius is the entire database.

Enforcing Tenant Isolation at the Postgres Kernel with FORCE RLS

We push tenant authorization down to the only layer an LLM cannot sweet-talk: the PostgreSQL storage engine. By enabling Row-Level Security (ALTER TABLE invoices ENABLE ROW LEVEL SECURITY) and immediately adding FORCE ROW LEVEL SECURITY, even table owners cannot read or mutate a row unless the active policy passes. We create a dedicated, least-privilege role (ai_agent_executor) with NOBYPASSRLS and define a restrictive policy (AS RESTRICTIVE) checking tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid. Why the NULLIF wrapper? Because if a background worker forgets to set the session variable, Postgres evaluates an empty string, fails to cast or returns NULL, and silently returns zero rows instead of throwing a leaky syntax error or defaulting open.

The PgBouncer Session Leak Trap (And How `SET LOCAL` Fixes It)

Most teams who try Postgres RLS in serverless Next.js or Node.js environments shoot themselves in the foot with connection pooling. If you execute SET app.current_tenant_id = '9b1deb4d-...' on a pooled PgBouncer or Supabase Supavisor connection in transaction mode, that session variable stays attached to the underlying physical socket. The next tenant who grabs that warm connection from the pool inherits the previous customer's identity. We never use session-level SET in production. Every AI tool execution is wrapped in an explicit database transaction that calls SELECT set_config('app.current_tenant_id', $1, true) right after BEGIN. That third boolean argument (is_local = true) scopes the tenant ID strictly to the current transaction. The millisecond the transaction commits or throws ERROR: 42501: new row violates row-level security policy, Postgres wipes the context clean.

Stripping Agent Telemetry Before It Starves Your Primary OLTP

Securing the query path solves half the problem. The other half is keeping your agent's audit trail from taking down production. Compliance teams rightly demand a complete log of every tool call, raw LLM prompt, token count, and RLS policy rejection. But dumping 150KB JSONB trace payloads into your primary Postgres instance on every agent step will spike disk IOPS and bloat autovacuum queues within weeks. We keep the transactional state—checkpoints, human-in-the-loop approval locks, and tenant-scoped memory—inside RLS-protected Postgres tables, while streaming heavy execution traces asynchronously to ClickHouse or BigQuery via an outbox queue. Your primary database stays sub-10ms fast for customer-facing queries, and your security team still gets a tamper-proof audit log.

Hardening Your AI Architecture Before Enterprise Security Audits

Enterprise buyers in 2026 do not just ask which LLM provider you use; their security architects ask how you mathematically prove Tenant A's agent cannot read Tenant B's database rows. You can wire up transaction-scoped RLS wrappers, negative CI/CD isolation tests (asserting an unset app.current_tenant_id always returns 0 rows), and least-privilege database roles across your stack in under a week. If your engineering team is shipping multi-tenant AI workflows, custom SaaS platforms, or automated document pipelines and wants battle-tested data isolation without slowing down product velocity, explore what we build at Enorexa or talk to us to review your architecture.