Migrating production infrastructure? Get up to $10K in migration credits.

Apply now
AI

Multi-Tenant RAG With PostgreSQL and pgvector

TL;DR

  • Multi-tenant RAG on shared PostgreSQL and pgvector fails first on isolation and recall: database-enforced RLS plus indexing under tenant filters keep each customer's context safe without wrecking semantic search.
  • A single database approach partitions context retrieval without the complex state synchronization required by standalone vector databases.
  • A two-table schema with composite foreign keys, row-level security, and a transaction-local tenant setting enforces authorization scope. Backup roles get BYPASSRLS so a full dump includes every tenant.
  • Maintaining retrieval quality means matching the index to the tenant's share of the table: a btree when the slice is small, partial indexes for a few stable tenants, list partitioning when tenants must not share one approximate index, and iterative scans only after you turn them on.
  • Offloading compute-heavy chunking and embedding to asynchronous background workers and enforcing in-memory rate limits protects centralized resources from noisy neighbors.
  • Deploying on Render keeps compute and state on a private network, with Blueprints available when enterprise tenants need their own dedicated Postgres and services.

AI products win or lose on whether retrieval returns the right context. That usually means a vector store next to the app.

PostgreSQL with pgvector is often the natural place to put that store: embeddings live beside tenant, user, and document metadata in one database.

Multi-tenant SaaS makes that choice harder. You must isolate each customer's data and keep semantic recall high under metadata filters. Misconfigure either and you leak data or serve the wrong chunks.

Those are the load-bearing problems once you have already chosen Postgres for RAG. Chunking, prompts, and "which vector database" debates are out of scope here.

This article shows how to set authorization scope with RLS, keep retrieval quality under tenant filters, control noisy neighbors, and deploy a tenant-safe architecture.

What is multi-tenant RAG?

Multi-tenant RAG is a system architecture where a single AI application serves multiple discrete organizations. It uses a shared vector store while strictly partitioning context retrieval. By isolating the vector search scope, the application ensures a Large Language Model (LLM) only sees data authorized for the requesting user. That is the core of multi-tenant pgvector design: one database, many customers, no cross-tenant context.

Keeping embeddings and relational constraints in a single PostgreSQL database helps developers avoid distributed state synchronization. User IDs and document types stay next to the vectors instead of living in a separate store. For the broader managed Postgres and pgvector stack on Render, see Simplify your AI stack with managed PostgreSQL and pgvector.

How do you enforce authorization scope with PostgreSQL RLS?

Designing a defense-in-depth schema

A best practice for RAG data isolation is a two-table schema. One table holds high-level document metadata. A separate relational table holds the embedded document chunks.

To prevent accidental cross-tenant data leaks, place the tenant_id on both tables. Enforce the relationship using a composite foreign key. This explicit binding prevents a chunk labeled for one tenant from pointing to a document belonging to another.

Enforcing row-level isolation and mitigating backup risks

With the schema established, apply pgvector row-level security. By default, PostgreSQL denies access to an RLS-enabled table unless a specific policy allows it.

When configuring RLS, FORCE ROW LEVEL SECURITY is a mandatory requirement. Without this declaration, table owners bypass security policies entirely. The similarity search reads chunks, so that table needs the same enable, force, and policy as documents.

Never trust client-side payloads to dictate tenant context. The backend API must resolve the tenant identity from an authenticated token and set app.tenant_id as a transaction-local variable before executing the retrieval query.

Test database driver behavior with transaction-mode pooled connections so tenant context cannot leak. A tenant setting accidentally applied at the session scope instead of the transaction scope can leak unauthorized data across subsequent API requests.

Writing complex RLS policies introduces severe risks:

  • Sub-SELECTs within an RLS policy can cause query plan degradation
  • Self-referential policy logic or circular foreign keys can cause infinite recursion
  • RLS covert channels exist where unique constraints and foreign keys inherently bypass RLS policies

Use generated surrogate keys rather than keys with external meanings. This prevents malicious tenants from inferring hidden data through duplicate key errors.

Automated database backups present a critical operational risk. Teams must provision a dedicated backup role with the BYPASSRLS privilege to ensure successful, complete backups. Because pg_dump forces row_security = off by default, forgetting the BYPASSRLS privilege will intentionally crash the backup rather than silently outputting a partial database.

How do you maintain retrieval quality with pgvector metadata filters?

Tenant isolation vector search is only useful if filtered queries still return the right neighbors. Under a tenant filter, retrieval quality depends on how small that tenant's slice is and which index answers the query. Embedding choice, chunking, and rerankers still matter, but they are shared with single-tenant RAG.

Work through four shapes. Use a btree on tenant_id when the tenant owns a small slice. Use a partial vector index for a few stable tenants. Partition by tenant_id, or move a tenant to its own table, when tenants must not share one approximate index. Enable iterative scans only when you keep a single shared HNSW and the query has to fill LIMIT.

Indexing strategy
Semantic recall
Performance risk
Ideal use case
B-tree on tenant_id
Exact among that tenant's rows
Exact scan cost grows with that tenant's row count
A tenant that owns a small slice of the table
Partial HNSW or IVFFlat
High inside that tenant's graph
One more index to build, store, and vacuum per slice
A few stable tenants that need predictable latency
List partitioning or separate tables
High inside that partition's index
Planning and catalog cost rise with partition count
Many tenants that must not share one approximate index
Iterative scan on a shared HNSW
High only while the scan stays inside hnsw.max_scan_tuples
CPU and latency climb as the tenant's share of rows shrinks
A shared index after you set hnsw.iterative_scan

Start with a btree on the filter column. HNSW and IVFFlat cannot put tenant_id and the vector in the same index, so the btree is a separate index. Create it on chunks, the table the similarity search reads:

When WHERE tenant_id = … matches a small percentage of rows, Postgres can use that btree and exact-rank the tenant's vectors. Recall among those rows is exact. The cost is that exact sort, and it grows as the tenant owns more of the table. Past that point an approximate index is the better plan. If a shared HNSW also exists, check the query plan. A selective tenant filter should be using the btree.

Building partial indexes for a few tenants

Partial vector indexes fit when a small set of tenants is stable and latency-sensitive. You build one HNSW (or IVFFlat) index per filtered slice, usually WHERE tenant_id = …, so each query hits a tenant-local graph instead of a shared index.

Use them when tenant IDs change rarely and the set you optimize for is small. Skip them when tenants are numerous or short-lived. Every new tenant means another index to build, store, and vacuum, which often costs more than an iterative scan of one shared index. For a tenant that needs its own graph without a pile of partial indexes, use list partitioning or a separate table.

Partitioning so tenants do not share a graph

A shared approximate index lets one tenant's vectors change recall and speed for every other tenant on that index. List partitioning by tenant_id, or a separate table per tenant, keeps those vectors in different indexes.

Partition the chunks table PARTITION BY LIST (tenant_id) and build the HNSW on each partition. The retrieval query has to include tenant_id so the planner opens one partition. Without that predicate, Postgres can scan every partition.

Each tenant you add is a partition to attach and an index to build. Planning time grows with the partition count. When a tenant needs a dedicated database rather than another partition, use the single-tenant path later in this article.

Enabling iterative scans on a shared index

If you keep one HNSW for every tenant, filtering runs after the index scan. hnsw.iterative_scan controls whether pgvector keeps walking until LIMIT is filled. It defaults to off. ivfflat.iterative_scan defaults to off as well. Both settings arrived in pgvector 0.8.0.

With the default, a tenant that owns about 10% of the rows and the default hnsw.ef_search of 40 leaves about four surviving rows, even when the query asks for more. That missing-neighbor result is the recall problem. It is not a sign that iterative scan is already on.

Set hnsw.iterative_scan to strict_order when distance order must be exact, or to relaxed_order when you can accept a slight reorder and want more of the neighbors. The walk stops at hnsw.max_scan_tuples, 20,000 by default. A very small tenant can still return fewer rows than LIMIT. The extra tuples are the CPU and latency cost, so keep the HNSW graph and the heap rows in memory when this query is on the hot path.

How do you manage resource placement and control noisy neighbors?

A noisy neighbor is a tenant whose ingest or query load consumes shared database resources and slows everyone else on the same instance. A shared PostgreSQL instance centralizes relational data and embeddings. Tenants share connection slots, CPU cycles, buffer cache, and index build operations. A sudden spike in document uploads from one massive account degrades the search experience for all other tenants.

Connection pooling is mandatory for SaaS applications. A transaction-mode pooler such as PgBouncer safely manages high-concurrency requests from stateless APIs without exhausting standard PostgreSQL connection limits.

Do not execute heavy AI ingestion and structure-aware text chunking on synchronous API routes. Offload document processing to persistent background workers paired with durable job queues.

Compute limitations dictate placement strategy. Serverless platforms frequently enforce rigid timeouts that predictably interrupt complex embedding tasks. Persistent background workers operate without strict HTTP execution time limits, providing stable runtime environments for heavy vector operations.

Implement an in-memory quota layer to protect database resources. Use a fast caching tier to enforce strict per-tenant rate limits on:

  • Concurrent retrievals
  • Embedding tokens processed
  • Maximum allowable query duration

What does a reference architecture for tenant-safe RAG look like?

The controls above need somewhere to run. Retrieval must stay on a long-lived API process, embedding must stay off that request path, vectors and tenant rows must live in one Postgres, per-tenant limits need a fast side store, and none of that traffic should cross the public internet. A Render reference deploy maps each of those jobs to a primitive.

At the API layer, the retrieval API runs on Render web services. Persistent compute keeps hybrid search and LLM round-trips from dying on short serverless caps, which matters once RLS and tenant filters make each query heavier than a toy demo.

At the ingestion layer, chunking and embedding run on background workers. That is the placement rule from the noisy-neighbor section: heavy ingest never shares the user-facing request path. Use a native Python runtime or Docker when the embedding stack needs custom system dependencies.

At the data and quota layer, Render Postgres holds documents, chunks, and pgvector indexes, the same store your RLS policies and metadata filters already assume. The Redis®-compatible Render Key Value is the in-memory broker for the per-tenant rate limits and queue coordination those controls need.

For the security context, services talk only over Render's private network. RLS stops the wrong rows from returning inside Postgres; the private network stops raw embeddings and tenant metadata from sitting on a public path in the first place.

When a tenant must leave the shared database, Blueprints stamp out the same web/worker/Postgres shape as a dedicated stack, the single-tenant path in the next section.

How do you verify security and plan for enterprise migrations?

Running security audits and compliance checks

Before scaling a shared multi-tenant database, validate data boundaries through regression testing. Use this checklist as a release gate:

  • Cross-tenant read with a valid token for tenant A against tenant B's document_id / chunk_id returns no rows
  • Cross-tenant update and delete with missing or malformed app.tenant_id fail closed
  • app.tenant_id is set transaction-local, not session-scoped, under your pooler's transaction mode
  • FORCE ROW LEVEL SECURITY is on for roles that own the tables
  • Backup role has BYPASSRLS; a dump without it fails instead of writing a partial database
  • Unique constraints and foreign keys cannot be abused as RLS covert channels (prefer opaque surrogate keys)

Keep pgvector and PostgreSQL on current minor versions. Vector indexes and buffer behavior change often enough that stale minors are an avoidable risk.

Building a dedicated single-tenant path

Enterprise SaaS applications eventually acquire clients who demand physical database isolation instead of logical row-level security. When a tenant outgrows the shared PostgreSQL instance, the architecture must accommodate a clean extraction.

Teams manage this infrastructure transition by leveraging infrastructure-as-code platforms to stand up a dedicated single-tenant stack. Using Render Blueprints, engineers programmatically stamp out identical, dedicated compute and PostgreSQL instances from one YAML file.

This declarative approach allows the engineering team to migrate a heavy tenant to an isolated infrastructure silo without manually provisioning hyperscaler VPCs. A full backup still uses the BYPASSRLS role from the checklist, and that dump contains every tenant. To extract one tenant, use a different role that does not have BYPASSRLS, set app.tenant_id for the session, and run pg_dump --enable-row-security. A publication row filter on tenant_id can also stream that tenant's rows and embeddings into the new database with logical replication.

Conclusion

Multi-tenant RAG holds when RLS and explicit tenant bindings protect the shared store, and when you pick indexing and partitioning that match how selective each tenant's filters are. Rate limits and async workers keep one tenant from starving the rest.

Persistent web services, background workers, and managed Postgres on a private network give you that shape without assembling a hyperscaler VPC by hand.

Run retrieval, workers, and Postgres on one private network.

Deploy on Render

Redis® is a registered trademark of Redis Ltd. Any rights therein are reserved to Redis Ltd. Any use by Render is for referential purposes only and does not indicate any sponsorship, endorsement or affiliation between Redis and Render.

Frequently asked questions