Back to home

The storage layer: Postgres + pgvector and why it works

July 3, 2026 · Technical deep dives

Why Eigen Mesh uses one PostgreSQL instance with pgvector and Apache AGE for relational data, vector embeddings, and the knowledge graph — and what that buys you.

← All posts

Most "AI-native" products have a storage stack that looks like a small distributed system: a relational database for accounts and billing, a vector database for embeddings, a graph database for relationships, a cache in front, a queue on the side, and a search service bolted on. Six services, six sets of credentials, six things to back up, six things that can drift out of sync.

Eigen Mesh has one. This post is the architecture justification for that choice — why one PostgreSQL instance with pgvector and Apache AGE is the right storage layer for a memory product, and what it buys you operationally.

The one-database claim

Eigen Mesh stores three logically different kinds of data:

  • Relational — users, sessions, API keys, thoughts, activity logs, billing state. The system of record.
  • Vectors — embeddings of thought text, used for semantic search.
  • Graph — entities, relations, communities, and the precomputed neighbor bundles used in retrieval.

All three live in the same PostgreSQL 16 instance. Relational data is in normal tables. Vectors are in pgvector columns. Graph data is in Apache AGE, which runs OpenCypher inside the same Postgres. There is no second database.

This is not a "we'll add a vector DB later" decision. It's the design.

Why pgvector for the vector side

The vector workload in a memory product is not the vector workload people benchmark. The standard pgvector-vs-Pinecone comparison assumes a billion-vector corpus with thousands of queries per second. A personal second brain — even a busy one — is a few hundred thousand vectors, queried a handful of times per session. That's a different problem.

On that problem, pgvector is comfortably fast. With HNSW indexes over a few hundred thousand vectors, ANN queries return in low single-digit milliseconds on a small instance. The bottleneck in Eigen Mesh retrieval is not the vector search; it's the LLM rerank pass that runs after it. Optimizing the vector store would be optimizing the wrong thing.

What pgvector buys you that a separate vector DB doesn't:

  • Transactional consistency. The same transaction that inserts a thought inserts its embedding. No drift, no reconciliation job, no "the vector is there but the row isn't" edge case.
  • One backup. pg_dump covers your relational data and your vectors. Restore is one command.
  • One security model. Row Level Security applies to embedding rows the same way it applies to thought rows. The user_id scope is the only isolation primitive, and it works uniformly.
  • Hybrid queries in one SQL statement. Eigen Mesh's retrieval joins vector distance, FTS rank, and precomputed graph artifacts in a single query. Doing that across two databases means either shipping vectors across the wire or denormalizing into one — at which point you've reinvented pgvector.

The trade is raw ANN throughput. A purpose-built vector index will outperform pgvector on pure vector search at scale. For a memory product, that's the wrong end of the curve to optimize.

Why Apache AGE for the graph side

The graph exists because retrieval in a memory product is not just similarity. A thought about "the pricing conversation with Alex last Tuesday" is related to other thoughts about pricing, to thoughts mentioning Alex, to thoughts from last Tuesday, and to the broader "pricing" community. Those relationships aren't captured well by vectors alone, or by FTS. They're captured by a graph.

Apache AGE runs OpenCypher inside Postgres. That means the graph is not a separate service — it's a Postgres extension. No live graph traversal happens at query time; Eigen Mesh precomputes neighbors, entity-anchored paths, and community bundles during enrichment and consolidation. Retrieval reads precomputed artifacts; it doesn't run live AGE traversals on the hot path. And nightly pg_cron runs community detection, summary generation, and bundle materialization — all inside Postgres, touching the graph and relational tables together.

The choice of AGE over a standalone graph database (Neo4j, Memgraph) is the same choice as pgvector over Pinecone: one fewer service to operate, one fewer consistency boundary, one fewer backup target. The graph is small and the queries are precomputed; a standalone graph DB's advantages don't apply.

The three-tier memory pipeline against this storage

The storage choice becomes clearer when you trace the pipeline:

  1. Capture (hot persist). A thought lands in a relational table. The transaction commits. The user gets an acknowledgment. No embedding yet — that's tier 2.
  2. Background enrichment. The server classifies, embeds, writes the pgvector column, builds FTS tokens, syncs the AGE node and edges, and materializes neighbor bundles. All in Postgres. All within transaction boundaries per step.
  3. Nightly consolidation ("sleep"). pg_cron runs salience decay, ontology pruning, entity dedup, community detection, and community summary generation. Relational, vector, and graph data are all touched together.

If these were three databases, every step would be a cross-service coordination problem. In one Postgres, they're queries.

Hybrid retrieval in one query

The retrieval path is where the one-database choice pays off most concretely. When you call retrieve_thoughts with a query, Eigen Mesh runs three channels: pgvector ANN over the query embedding, Postgres FTS over lexical_text and cues, and precomputed graph neighbors / entity-anchored paths / community bundles (read from materialized tables, not live AGE traversal).

These are fused and reranked by an LLM listwise pass. The fusion is a weighted merge of scores from three channels that all live in the same database. No cross-service joins, no shipping intermediate results over the wire. One query plan, one connection, one transaction snapshot.

What this costs you

The honest trade-offs:

  • You're betting on Postgres extensions. pgvector and AGE both have active maintainers and real production usage, but they're extensions, not standalone products with dedicated sales engineers. If you hit a bug, you're reading extension source.
  • Scale has a ceiling. pgvector scales further than people credit, but it's not infinite. At billions of vectors, a purpose-built index wins. A memory product is not at billions of vectors.
  • Operational tooling is Postgres tooling. If you'd rather debug a vector DB with its own dashboard, you don't get that. You get pg_stat_statements and EXPLAIN. A feature for some operators, a limitation for others.

Why we're confident in this

The bet is that the storage layer should be boring and the intelligence should live in the pipeline above it. Postgres + pgvector + AGE is the boring layer. The enrichment, consolidation, hybrid retrieval, and rerank are where the interesting work happens, and they're all just queries against a database that already exists.

Three years from now, the retrieval pipeline will have changed. The storage layer probably won't. That's the point.

Try it

If you want to see the actual schema and retrieval query shapes, the architecture is documented at /developers/architecture and the memory pipeline at /developers/how-memory-works. If you'd rather just drop a thought in and watch it get organized, start on managed with 100 free credits — the storage layer is the same either way.

Eigen Mesh

Open source memory infrastructure — Apache 2.0, self-hostable, and less than a coffee a month on managed hosting. Your context stays yours.

Navigation

© 2026 Eigen Mesh. All rights reserved.