← Back to all posts

Enterprise RAG Architecture: Security, ACLs, and the Power of pgvector

Most Retrieval-Augmented Generation (RAG) tutorials rely on a naive mental model: ingest all enterprise documentation into a cloud vector database, perform an approximate nearest-neighbor search, and dump retrieved chunks into an LLM prompt.

In enterprise production environments, this implementation promptly gets blocked by InfoSec and Compliance audits. The fundamental blocker: absence of inherited Access Control Lists (ACL / RBAC). If a junior engineer asks an internal knowledge assistant: “What were the executive bonus payouts last quarter?”, a naive RAG pipeline will happily retrieve passages from confidential board PDFs and hand them to the model for summarization.

In this article, we examine how to architect an enterprise-compliant RAG system using PostgreSQL, pgvector, and native Row-Level Security (RLS).


1. The Standalone Vector DB Trap: Permissions and Eventual Consistency

The market has seen an explosion of specialized vector stores (Pinecone, Qdrant, Chroma, Milvus). While their ANN (Approximate Nearest Neighbor) benchmarks are impressive, they introduce serious architectural liabilities in corporate systems:

[ Backend Application ] ──(Role / Access Update)──> [ PostgreSQL / Core DB ]
          │
          │  (Non-transactional, async CDC lag)
          ▼
[ Standalone Vector DB ]  <──  Stale ACL Metadata!

When a user’s permissions change in the primary data source (e.g., revoking access to a project folder in SharePoint or a CRM deal room):

  1. The metadata changes must be propagated asynchronously to the external vector database via ETL or Change Data Capture (CDC).
  2. During the replication lag window (eventual consistency), the vector database continues serving confidential chunks to unauthorized clients.
  3. External vector engines typically implement Post-Filtering (retrieve top-50 vectors, then prune unauthorized records in code). If 48 of those 50 results belong to restricted board documents, the end-user receives only 2 results—even if thousands of authorized documents exist in the corpus.

2. The Architectural Solution: Unified Storage with pgvector and RLS

Retaining your business entities, identity tables, and vector embeddings within a single PostgreSQL instance dissolves this entire failure mode.

By combining pgvector with Row-Level Security (RLS), the database query planner evaluates permissions within the exact same ACID transaction. The semantic query cannot physically inspect rows outside the current user’s security envelope.

Database Schema with Role-Based Constraints:

CREATE EXTENSION IF NOT EXISTS vector;

-- Document Chunks Table
CREATE TABLE document_chunks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    document_id UUID NOT NULL,
    content TEXT NOT NULL,
    embedding vector(1536), -- Dimension for OpenAI text-embedding-3-small
    required_role VARCHAR(50) NOT NULL DEFAULT 'employee',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- HNSW Index for sub-millisecond approximate nearest neighbor lookup
CREATE INDEX idx_document_chunks_embedding_hnsw 
ON document_chunks 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Enable Row-Level Security
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;

-- Security Policy: Strictly constrain queries to tenant and allowed role
CREATE POLICY rls_document_chunks_access ON document_chunks
FOR SELECT
USING (
    tenant_id = current_setting('app.current_tenant_id')::UUID
    AND (
        required_role = 'employee' 
        OR current_setting('app.current_user_role') = 'management'
    )
);

Executing a Cryptographically Secure Semantic Query:

The application sets the authenticated session identity before executing the vector distance calculation:

BEGIN;
SET LOCAL app.current_tenant_id = 'a1b2c3d4-0000-0000-0000-000000000001';
SET LOCAL app.current_user_role = 'employee';

-- Vector similarity search with enforced RLS boundaries
SELECT 
    id,
    content,
    1 - (embedding <=> '[0.012, -0.045, 0.089, ...]'::vector) AS similarity
FROM document_chunks
ORDER BY embedding <=> '[0.012, -0.045, 0.089, ...]'::vector
LIMIT 5;

COMMIT;

Even if the absolute nearest vector in the high-dimensional space represents an executive salary schedule, the PostgreSQL engine filters it out before distance ordering. Prompt injection or data exfiltration becomes architecturally impossible at the storage layer.


3. Chunking Mechanics: Parent-Document Retrieval

A second major source of enterprise RAG failure is crude text chunking. Naively splitting documents into arbitrary 500-token blocks with static overlap destroys systemic context:

  • A paragraph reads “Operational expenses increased by 14%”, but the table specifying the business unit was truncated in the preceding chunk.

The robust pattern is Parent-Document Retrieval (Hierarchical Chunking):

[ Source Enterprise PDF / Document ]
                  │
                  ▼
   [ Parent Chunk (e.g., 2,000 tokens / Full Section) ]
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
 [ Child Chunk 1 ]   [ Child Chunk 2 ]   (e.g., 200 tokens)
   (Embedding A)       (Embedding B)
  1. Search Phase: Embeddings are computed against compact, precise child passages (150–300 tokens). This keeps the vector representation dense and laser-focused on specific semantics.
  2. Generation Phase: When a child match occurs, the pipeline pulls the corresponding parent document chunk into the prompt context. The LLM receives the full surrounding narrative and complete tabular headers, preventing hallucinated conclusions.

4. Production Tuning for HNSW in pgvector

When scaling to hundreds of thousands or millions of vector embeddings, tuning PostgreSQL’s HNSW (Hierarchical Navigable Small World) parameters is essential:

  • m (default 16): Maximum number of bidirectional connections per node. Increasing to 24–32 enhances retrieval recall at the cost of higher index RAM footprint.
  • ef_construction (default 64): Search queue size during index generation. Raising to 128 increases build time, but significantly sharpens query precision.
  • hnsw.ef_search (session variable, default 40): Governs graph exploration depth during individual query execution. Can be tuned per transaction:
SET hnsw.ef_search = 100;

For memory-constrained production clusters, consider using the halfvec data type (16-bit floating point numbers instead of standard 32-bit float4). This reduces RAM consumption by 50% with negligible loss in similarity recall.


Summary

Enterprise RAG is 20% prompt tuning and 80% systems data engineering: transactional guarantees, access control, and low-latency storage. Instead of building fragile synchronization bridges to detached vector stores, utilizing PostgreSQL with pgvector and native Row-Level Security establishes a secure, auditable, and production-tested foundation for enterprise AI systems.