SurrealDB for AI Agents: Relational, Graph, and Vector in One Database
I replaced three databases with one. Here's why SurrealDB works for AI agent systems and what I gave up.
I replaced three databases with one. Here's why SurrealDB works for AI agent systems and what I gave up.

The typical AI stack has three databases:
Three databases means three connection pools, three migration systems, three query languages, three failure modes.
I use one database for all three: SurrealDB.
It's not just operational overhead. It's consistency.
When an AI agent stores a memory, three things need to happen:
With three databases, you need distributed transactions or eventual consistency. Both are painful.
Store fact in Postgres → success
Create edge in Neo4j → success
Index embedding in Pinecone → timeout
→ Now what? Fact exists but isn't searchable. Inconsistent state.
With one database:
Store fact + create edges + index embedding → single transaction
→ All or nothing. Consistent.
SurrealDB is a multi-model database. It handles relational, document, graph, and vector data in a single engine.
Standard tables with schemas:
DEFINE TABLE agents SCHEMAFULL;
DEFINE FIELD name ON agents TYPE string;
DEFINE FIELD workspace_id ON agents TYPE record<workspaces>;
DEFINE FIELD status ON agents TYPE string
ASSERT $value IN ['active', 'inactive', 'paused'];
DEFINE FIELD capabilities ON agents TYPE array<string>;
This is familiar. Tables, fields, types, constraints.
First-class edges between records:
-- Create a relationship
RELATE message:abc -> DERIVED_FROM -> fact:xyz
SET confidence = 0.85, extracted_at = time::now();
-- Traverse the graph
SELECT <-DERIVED_FROM<-message FROM fact:xyz;
Edges are records too. They have fields. You can query them, filter them, traverse them.
Native vector indexing and similarity search:
DEFINE FIELD embedding ON facts TYPE array<float>;
DEFINE INDEX fact_embedding ON facts
FIELDS embedding MTREE DIMENSION 1536 DIST COSINE;
-- Semantic search
SELECT *, vector::similarity::cosine(embedding, $query_embedding) AS score
FROM facts
WHERE workspace_id = $workspace
ORDER BY score DESC
LIMIT 10;
Embeddings live alongside the data. No separate service. No sync pipeline.
The agent memory system is where SurrealDB's multi-model nature pays off most.
Storing a memory:
-- 1. Create the fact (relational)
CREATE fact SET
content = "Customer prefers email communication",
workspace_id = workspace:abc,
agent_id = agent:xyz,
confidence = 0.9,
embedding = $embedding;
-- 2. Link to source message (graph)
RELATE message:m123 -> DERIVED_FROM -> fact:f456
SET extraction_method = "llm", model = "claude-3.5-sonnet";
-- 3. Link to entity (graph)
RELATE fact:f456 -> ABOUT -> entity:customer_123
SET relation_type = "preference";
One transaction. Three models. Consistent.
Retrieving memories with provenance:
-- Find relevant facts by semantic similarity
LET $facts = SELECT *, vector::similarity::cosine(embedding, $query) AS score
FROM facts
WHERE workspace_id = $workspace AND score > 0.7
ORDER BY score DESC
LIMIT 5;
-- Get provenance for each fact
SELECT
content,
score,
<-DERIVED_FROM<-message.* AS sources,
->ABOUT->entity.* AS entities
FROM $facts;
One query returns the facts, their source messages, and the entities they relate to. In a three-database world, this would be three queries with application-level joins.
Entities and their relationships form a knowledge graph:
-- Entity relationships
RELATE entity:customer_123 -> WORKS_AT -> entity:company_abc;
RELATE entity:customer_123 -> PURCHASED -> entity:product_xyz;
RELATE entity:company_abc -> LOCATED_IN -> entity:city_sf;
-- Graph traversal: "What do we know about this customer's company?"
SELECT
->WORKS_AT->entity.* AS company,
->WORKS_AT->entity->LOCATED_IN->entity.* AS location
FROM entity:customer_123;
This is natural in a graph database. SurrealDB handles it without a separate Neo4j instance.
Execution checkpoints are documents with relational references:
CREATE execution SET
workflow_id = workflow:abc,
status = "running",
current_node = "check_inventory",
state = {
node_outputs: {
"parse_request": { intent: "order", product_id: "xyz" },
"check_inventory": { available: true, quantity: 5 }
},
budget_remaining: 1.50
},
started_at = time::now();
The state is a document (flexible schema). The references are relational (typed foreign keys). Both in one record.
SurrealDB has built-in record-level permissions:
DEFINE TABLE facts SCHEMAFULL
PERMISSIONS
FOR select WHERE workspace_id = $auth.workspace_id
FOR create WHERE workspace_id = $auth.workspace_id
FOR delete WHERE workspace_id = $auth.workspace_id AND $auth.role = 'admin';
Facts are scoped to workspaces. Users only see their workspace's data. Admins can delete. Enforced at the database level, not application level.
SurrealDB isn't free. Here's what I gave up.
Postgres has decades of battle-testing. SurrealDB is younger. The community is smaller. Edge cases in query planning exist.
Mitigation: I run extensive integration tests. I monitor query performance. I have a migration plan to Postgres + pgvector if needed.
Postgres has pgAdmin, hundreds of ORMs, every monitoring tool. SurrealDB's tooling ecosystem is growing but not there yet.
Mitigation: I built a thin DAL (Data Access Layer) in Rust that abstracts database operations. If I need to swap databases, I change the DAL, not the application.
Pinecone and Qdrant are purpose-built for vector search. They have more tuning options, better performance at scale, more index types.
Mitigation: For my current scale, SurrealDB's vector search is sufficient. If I outgrow it, I can add a dedicated vector store for just the search layer while keeping SurrealDB for storage and graph.
Managed Postgres is everywhere. Managed SurrealDB options are more limited.
Mitigation: Self-hosted with Docker. SurrealDB is a single binary, so deployment is straightforward.
SurrealQL is its own language. Not standard SQL. Developers need to learn it.
Mitigation: SurrealQL is close enough to SQL that the learning curve is small. The graph traversal syntax is actually more intuitive than SQL JOINs for relationship queries.
SurrealDB fits when you need multiple data models and consistency between them:
It doesn't fit when:
In my system:
flowchart TD
A[dal] --> B[SurrealDB Client]
B --> C[SurrealDB]
C --> C1["Relational: users, workspaces, agents"]
C --> C2["Graph: memory provenance, relationships"]
C --> C3["Vector: fact & message embeddings"]
D[Redis] --> D1[Hot checkpoints]
D --> D2[Rate limit counters]
D --> D3[Session tokens & OTP codes]
class C,D special
class C1,C2,C3 worker
class D1,D2,D3 default
SurrealDB handles persistent, consistent data. Redis handles ephemeral, high-speed data. Clear boundary.
Three databases for three data models is the default architecture. It works. But for AI agent systems where relational, graph, and vector data intersect on every operation, the consistency and simplicity of one database is worth the trade-offs.
SurrealDB isn't the only option. But the question — "do I really need three databases?" — is worth asking.