Postgres as Institutional Memory: Schemas, Audit Lineage, and Grounded Answers

Stop treating your database as a dumb cache. Make it the source of truth for AI agents.

by
Postgres as Institutional Memory: Schemas, Audit Lineage, and Grounded Answers

Every time an AI agent hallucinates or forgets past decisions, your organization loses credibility. The typical response is to throw more prompts at the problem or duct-tape a vector database onto a chatbot. That’s cargo-cult engineering. The real solution is already sitting in your infrastructure: Postgres.

Postgres is not just a relational store. With schemas, triggers, foreign data wrappers, and extensions like pgvector, it becomes a single source of truth for agent memory, audit lineage, and grounded retrieval. No cloud vector DB, no SaaS knowledge base, no fragile RAG pipeline that breaks when the embedding model changes.

Why Postgres, Not a Vector Store

Vector databases are fast at similarity search, but they are terrible at enforcing relationships, maintaining schema integrity, or tracking provenance. When an agent retrieves a chunk from a vector store, it gets a vector and a payload. Where did that chunk come from? Who wrote it? When was it last updated? The vector store doesn’t care. You have to build that lineage yourself, and you usually do it badly.

Postgres gives you both: structured metadata and vector search. With pgvector, you can store embeddings alongside the original text, timestamps, user IDs, and source documents in the same row. Queries become joins, not API calls. You can filter by date, author, or schema before you even hit the index.

Schema Design for Institutional Memory

Your institutional memory lives in a set of tables that mirror your organization’s ontology. Start with a schema like this:

CREATE SCHEMA IF NOT EXISTS memory;

CREATE TABLE memory.documents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    embedding vector(768),  -- for BGE-M3 or similar
    source_schema TEXT NOT NULL,  -- e.g., 'hr', 'engineering', 'legal'
    created_by TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE memory.chunks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id UUID REFERENCES memory.documents(id) ON DELETE CASCADE,
    chunk_index INT NOT NULL,
    chunk_text TEXT NOT NULL,
    embedding vector(768),
    UNIQUE(document_id, chunk_index)
);

Why two tables? Because you want to retrieve entire documents or specific chunks depending on the agent’s need. The source_schema column lets you restrict queries to a department’s corpus. No more agents answering HR questions with engineering specs.

Audit Lineage with Triggers

Every row change must be logged. Not for debugging — for compliance. The EU AI Act requires that any decision made by an AI system can be traced back to its training data and prompts. If your agent gives a wrong answer, you need to know exactly which document it used.

Postgres triggers are your friend:

CREATE TABLE memory.audit_log (
    id BIGSERIAL PRIMARY KEY,
    table_name TEXT NOT NULL,
    record_id UUID NOT NULL,
    action TEXT NOT NULL,  -- INSERT, UPDATE, DELETE
    old_data JSONB,
    new_data JSONB,
    changed_by TEXT NOT NULL,
    changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION memory.audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO memory.audit_log (table_name, record_id, action, old_data, new_data, changed_by)
    VALUES (
        TG_TABLE_NAME,
        COALESCE(NEW.id, OLD.id),
        TG_OP,
        CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN row_to_json(OLD)::jsonb ELSE NULL END,
        CASE WHEN TG_OP IN ('INSERT','UPDATE') THEN row_to_json(NEW)::jsonb ELSE NULL END,
        current_user
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER documents_audit
    AFTER INSERT OR UPDATE OR DELETE ON memory.documents
    FOR EACH ROW EXECUTE FUNCTION memory.audit_trigger();

CREATE TRIGGER chunks_audit
    AFTER INSERT OR UPDATE OR DELETE ON memory.chunks
    FOR EACH ROW EXECUTE FUNCTION memory.audit_trigger();

Now every insertion, update, or deletion is recorded with a full snapshot. When an auditor asks “Why did the agent recommend this supplier?”, you can query the audit log for the exact chunk that was retrieved, who put it there, and when.

Agents need to answer questions based on facts, not generation. That means retrieval-augmented generation (RAG) with a tight loop: query → retrieve → prompt → verify.

With pgvector, you can do hybrid search — combine vector similarity with keyword matching:

-- Create an index for approximate nearest neighbor search
CREATE INDEX ON memory.chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- Query: find top 5 chunks similar to the query embedding, filtered by source
SELECT c.chunk_text, d.title, d.source_schema
FROM memory.chunks c
JOIN memory.documents d ON d.id = c.document_id
WHERE d.source_schema = 'legal'
ORDER BY c.embedding <=> $query_embedding
LIMIT 5;

But vector similarity alone misses exact matches. Combine with full-text search using Postgres built-in tsvector:

ALTER TABLE memory.chunks ADD COLUMN tsv tsvector
    GENERATED ALWAYS AS (to_tsvector('english', chunk_text)) STORED;

CREATE INDEX ON memory.chunks USING GIN (tsv);

-- Hybrid query: combine vector and text scores
WITH vector_results AS (
    SELECT id, 1 - (embedding <=> $query_embedding) AS score
    FROM memory.chunks
    WHERE source_schema = 'legal'
    ORDER BY score DESC
    LIMIT 100
),
text_results AS (
    SELECT id, ts_rank(tsv, plainto_tsquery('english', $query_text)) AS score
    FROM memory.chunks
    WHERE source_schema = 'legal'
    AND tsv @@ plainto_tsquery('english', $query_text)
    ORDER BY score DESC
    LIMIT 100
)
SELECT c.id, c.chunk_text,
       COALESCE(v.score, 0) * 0.5 + COALESCE(t.score, 0) * 0.5 AS combined_score
FROM memory.chunks c
LEFT JOIN vector_results v ON v.id = c.id
LEFT JOIN text_results t ON t.id = c.id
WHERE v.id IS NOT NULL OR t.id IS NOT NULL
ORDER BY combined_score DESC
LIMIT 5;

This gives you grounded answers that are both semantically similar and keyword-relevant. The agent’s prompt then includes the retrieved chunks as context, with citations pointing back to the document IDs. You can even store the prompt and the retrieved IDs in another audit table for full traceability.

Agent Orchestration: Postgres as the Control Plane

Your agents should not call external APIs for memory. They should query Postgres directly. Use a simple Python agent loop with psycopg2 or asyncpg:

import asyncpg
import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('BAAI/bge-m3')

async def retrieve_context(question: str, schema_filter: str = None):
    conn = await asyncpg.connect(dsn="postgres://...")
    query_emb = model.encode(question).tolist()
    if schema_filter:
        rows = await conn.fetch("""
            SELECT c.chunk_text, d.title
            FROM memory.chunks c
            JOIN memory.documents d ON d.id = c.document_id
            WHERE d.source_schema = $1
            ORDER BY c.embedding <=> $2::vector
            LIMIT 5
        """, schema_filter, query_emb)
    else:
        rows = await conn.fetch("""
            SELECT c.chunk_text, d.title
            FROM memory.chunks c
            JOIN memory.documents d ON d.id = c.document_id
            ORDER BY c.embedding <=> $1::vector
            LIMIT 5
        """, query_emb)
    await conn.close()
    return [(row['chunk_text'], row['title']) for row in rows]

No external vector DB, no Redis cache, no SaaS. Everything lives in one place. You can back up, replicate, and failover Postgres with standard tools. No vendor lock-in.

Scaling Considerations

  • Indexing: For large vector collections, use ivfflat with a reasonable lists value. For very large collections, consider pgvectorscale from Timescale or switch to pg_embedding with HNSW. But for most organizations, a single Postgres instance with sufficient RAM handles millions of vectors easily.
  • Connection pooling: Use pgbouncer to handle hundreds of agent connections. Set pool_mode = transaction for best performance.
  • Read replicas: Offload vector search to a replica. Use pg_stat_replication to monitor lag.
  • Embedding computation: Offload to a separate service and write embeddings asynchronously. Use LISTEN/NOTIFY to trigger re-embedding on updates.

Compliance and the EU AI Act

Under the EU AI Act, high-risk AI systems must maintain logs of data used for training and inference. Postgres audit triggers cover this. Each agent interaction can be logged in a memory.agent_log table:

CREATE TABLE memory.agent_log (
    id BIGSERIAL PRIMARY KEY,
    agent_id TEXT NOT NULL,
    prompt TEXT NOT NULL,
    retrieved_chunk_ids UUID[] NOT NULL,
    response TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Now you have a complete chain: prompt → retrieved documents → response. If a user complains, you can replay the exact context.

When Not to Use This

  • If you need extremely low-latency vector search at very large scale, consider a specialized store with on-prem deployment.
  • If your agents need real-time updates across geo-distributed regions, look at distributed Postgres solutions.
  • If you hate SQL, this isn’t for you. But if you’re reading The Sovereign Stack, you probably love it.

Conclusion

Postgres is not a legacy database. It’s a platform for building institutional memory that is auditable, grounded, and sovereign. By combining schemas, triggers, full-text search, and vector embeddings, you eliminate the need for external memory stores and SaaS knowledge bases. Your agents become accountable. Your compliance team stops panicking. And you sleep better knowing your data never leaves your infrastructure.

#agent-orchestration#audit-lineage#institutional-memory#pgvector#postgres
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.

Related