SurrealDB | The context layer for AI agents

12 min read Original article ↗

The database where storage, context, and memory are one transaction.

MULTI-MODEL

Five databases become one

Stop stitching databases together. SurrealDB unifies your entire context layer in a single multi-model engine - one query language, one transaction, one deployment.

Before - Multiple databases

5 SDKs · 5 drivers · 5 connection pools

Vector
database

Proprietary API

Full-text search
database

Search DSL

Time-series
database

InfluxQL / Flux

Relational / document
database

SQL / MQL

Graph
database

Cypher / Gremlin

Eventual consistency only

After - SurrealDB

1 SDK · 1 driver · 1 connection pool

Single ACID transactionStrong consistency

THE PLATFORM

The only vertical stack
from storage to memory

No other product offers this. Object storage to agent memory. One transaction boundary. One permission model. One deployment.

THE ARCHITECTURE

One stack. Four layers.
Every layer under one roof.

Spectron gives agents persistent memory. SurrealDB unifies every data model in one ACID transaction. The storage engine separates compute from storage on commodity object storage. No glue code. No middleware.

The context layer

Entity extraction

Knowledge graph

Temporal facts

Hybrid retrieval

Documents

Graphs

Vectors

Time-series

Auth

APIs

Quorum consensus

Compute-storage sep.

Scale to zero

SurrealDS

Object storage

(S3 / GCS / Azure Blob)

WHY IT BREAKS

Agents fail due to context.
Not models.

Your model can reason. It just has nothing reliable to reason over. Five systems. Five consistency models. Context that fragments at every seam.

Context that leaks at every seam

Data flows between five systems. Relationships, history, and metadata fragment at every boundary.

Data fragments at every system boundary

Writes that half-succeed

Memory updates in one system, state fails in another. Without unified transactions, partial writes corrupt context.

Single transaction

committed

inconsistent state

rolled back

Latency that compounds

Every system adds a network hop. Round trips stack under load until agents miss their response window.

Five systems to keep alive

Five failure modes, five monitoring setups, five sets of credentials. The glue code becomes the product.

Vector DB

Similarity search

Graph DB

Relationship store

Doc store

Raw document index

Auth service

Identity & tokens

Five systems. Five auth configs. Five things to scale, secure, and keep alive.

HOW IT WORKS

The read-think-write loop

Read, think, write - in a single transaction.

WHY SURREALDB

Every failure has a structural fix.

Each failure mode has a structural fix - built into the engine, not bolted on top.

FIXES: CONTEXT LEAKS

Multi-model in one engine

Documents, graphs, vectors, and time-series are native primitives in one query. No stitching across systems.

FIXES: PARTIAL WRITES

ACID across all data models

Graph updates, document writes, and vector index changes happen in a single transaction. All succeed or none do.

FIXES: COMPOUNDING LATENCY

One query, one round trip

SurrealQL composes graph traversal, vector search, and temporal filtering in a single statement. No multi-system orchestration.

FIXES: OPERATIONAL OVERHEAD

Built-in backend

Auth, permissions, API endpoints, live queries, and event triggers. The database is the backend.

SURREALQL

One query. Every model.

Graph traversals, vector search, transactions, and access control in one expressive language.

MULTI-MODEL QUERY

Graph, vector, and temporal data in one round trip.

LET $vec = fn::embed("running shoes");

SELECT
->purchased->product AS history,
->reviewed->product[WHERE
vector::similarity::cosine(
embedding, $vec
) > 0.8
] AS relevant,
->prefs[WHERE valid_at <= time::now()] AS prefs
FROM ONLY $user;

ACID TRANSACTIONS

Multi-model writes succeed together or not at all.

BEGIN TRANSACTION;

LET $order = CREATE order SET
user = $auth.id,
product = product:headphones,
total = product:headphones.price;

RELATE $auth.id->purchased->product:headphones
SET at = time::now();

UPDATE product:headphones SET stock -= 1;

COMMIT TRANSACTION;

BUILT-IN AUTH

Authentication and row-level security in the database.

DEFINE ACCESS account ON DATABASE
TYPE RECORD
SIGNUP (
CREATE user SET
email = $email,
pass = crypto::argon2::generate($pass)
)
SIGNIN (
SELECT * FROM user WHERE
email = $email AND
crypto::argon2::compare(pass, $pass)
);

DEFINE TABLE order PERMISSIONS
FOR select WHERE user = $auth.id
FOR create WHERE $auth.id != NONE;

Explore real-world query patterns across search, graphs, agents, and more.

-- Hybrid RAG retrieval. Blends vector cosine similarity with BM25
-- full-text search into a single weighted score, so results are
-- both semantically relevant and keyword-accurate.

-- Step 1: Embed the user's natural language query

LET $query = "How do I configure access control?";
LET $embedding = fn::embed($query);

-- Step 2: Run a hybrid search combining vector and full-text scores

SELECT
id, title, content,
-- Compute semantic similarity via cosine distance
vector::similarity::cosine(embedding, $embedding) AS vs,
-- Compute keyword relevance via full-text scoring
search::score(1) AS ts,
-- Blend both signals into a single weighted score
(vs * 0.6) + (ts * 0.4) AS score
FROM document
-- Union of both retrieval methods — score blending handles ranking
WHERE embedding <|4,COSINE|> $embedding
OR content @1@ $query
ORDER BY score DESC
LIMIT 5;

-- Graph-enhanced RAG with re-ranking. Over-fetches candidates via
-- hybrid search, then uses graph connectivity to boost documents
-- that are well-cited within the candidate set — surfacing
-- authoritative context that pure similarity would underrank.

-- Step 1: Embed the query

LET $query = "How do I configure access control?";
LET $embedding = fn::embed($query);

-- Step 2: Over-fetch candidates via hybrid vector + full-text search

LET $candidates = SELECT
id, title, content,
vector::similarity::cosine(embedding, $embedding) AS vs,
search::score(1) AS ts,
(vs * 0.6) + (ts * 0.4) AS score
FROM document
WHERE embedding <|10,COSINE|> $embedding
OR content @1@ $query
ORDER BY score DESC
LIMIT 10;

-- Step 3: Re-rank using graph authority within the candidate set

LET $ids = $candidates.id;

SELECT
id, title, content, score,
-- Count inbound references from other candidates (graph authority)
count(<-references<-document[WHERE id IN $ids]) AS authority,
-- Blend retrieval relevance with graph connectivity
(score * 0.7) + (authority * 0.15) AS final_score
FROM $candidates
ORDER BY final_score DESC
LIMIT 5;

-- Graph context expansion. Finds seed documents via hybrid search,
-- then walks the document reference graph to surface related context
-- that pure vector or keyword search would miss.

-- Step 1: Embed the query

LET $query = "How do I configure access control?";
LET $embedding = fn::embed($query);

-- Step 2: Find seed documents via hybrid vector + full-text search

LET $seeds = SELECT
id, title, content,
vector::similarity::cosine(embedding, $embedding) AS vs,
search::score(1) AS ts,
(vs * 0.6) + (ts * 0.4) AS score
FROM document
WHERE embedding <|4,COSINE|> $embedding
OR content @1@ $query
ORDER BY score DESC
LIMIT 3;

-- Step 3: Traverse the document graph to pull in related context

RETURN {
-- The seed documents found by hybrid search
matches: $seeds,
-- Walk outbound edges to find documents these seeds cite
referenced: $seeds->references->document,
-- Walk inbound edges to find documents that cite these seeds
referenced_by: $seeds<-references<-document
};

-- Knowledge graph modelling. Entities are records with vector
-- embeddings, connected by typed graph edges that carry their own
-- properties. Multi-hop traversals query the graph in one statement.

-- Step 1: Create entity records with embeddings for semantic search

CREATE entity:openai SET
name = "OpenAI", type = "organisation",
embedding = fn::embed("OpenAI AI research company");

CREATE entity:gpt4 SET
name = "GPT-4", type = "model",
embedding = fn::embed("GPT-4 large language model");

-- Step 2: Connect entities with typed graph edges

RELATE entity:openai->developed->entity:gpt4 SET
at = d"2023-03-14", context = "Released as a multimodal LLM";

-- Step 3: Query with multi-hop traversals across the graph

SELECT
name,
-- Follow outbound "developed" edges to find what OpenAI built
->developed->entity.name AS built,
-- Continue traversal: who uses what OpenAI built?
->developed->entity<-used_by<-entity.name AS users,
-- Follow a different edge type for partnerships
->partnered_with->entity.name AS partners
FROM entity:openai;

-- Agent memory with state invalidation. Memories carry TTLs and
-- vector embeddings. An event automatically detects when a new fact
-- supersedes an older one via semantic similarity, and links them
-- with a graph edge. Recall uses recency-weighted scoring so newer
-- versions of a fact naturally rank higher.

-- Step 1: Store a memory with a TTL and link the agent to it

CREATE memory SET
agent = agent:main, content = "User prefers dark mode UI",
embedding = fn::embed("User prefers dark mode UI"),
created_at = time::now(), valid_until = time::now() + 7d;

RELATE agent:main->recalls->memory:last SET strength = 1.0, at = time::now();

-- Step 2: Auto-detect superseded facts on every new memory

DEFINE EVENT detect_supersedes ON memory WHEN $event = "CREATE" THEN {
-- Find existing memories that are semantically near-identical
LET $similar = SELECT id FROM memory
WHERE id != $after.id
AND vector::similarity::cosine(embedding, $after.embedding) > 0.92;
-- Link the new memory to each older version it supersedes
FOR $old IN $similar {
RELATE $after.id->supersedes->$old.id SET at = time::now();
};
};

-- Step 3: Recall memories with recency-weighted relevance

LET $context = fn::embed("What are the user preferences?");

SELECT
content, created_at,
vector::similarity::cosine(embedding, $context) AS semantic,
-- Walk the supersedes graph to see what this fact replaced
->supersedes->memory.content AS supersedes,
-- Walk back to find which agent stored this memory
<-recalls<-agent.id AS source,
-- Recency decay so newer facts naturally outrank older versions
semantic * math::pow(math::E, -0.1 * duration::days(time::now() - created_at)) AS score
FROM memory
WHERE embedding <|4,COSINE|> $context
AND valid_until > time::now()
ORDER BY score DESC
LIMIT 10;

-- Conversational memory. Sessions and messages are linked by graph
-- edges, making it easy to retrieve full threads in order. Each
-- message carries a vector embedding so past conversations can be
-- searched semantically across all sessions.

-- Step 1: Create a conversation session for the current user

LET $session = CREATE session SET
user = $auth.id, started_at = time::now(), topic = "Project planning";

-- Step 2: Store a message and link it to the session via graph edge

LET $msg = CREATE message SET
role = "user", content = "What tasks are left for launch?",
embedding = fn::embed("What tasks are left for launch?"), at = time::now();

RELATE $session->contains->$msg SET position = 1;

-- Step 3: Retrieve the full conversation thread via graph traversal

SELECT role, content, at FROM $session->contains->message
ORDER BY at ASC;

-- Step 4: Semantic search across all past conversations

LET $q = fn::embed("launch checklist");

SELECT
content,
-- Walk the graph back to find which session this message belongs to
<-contains<-session.topic AS session,
vector::similarity::cosine(embedding, $q) AS relevance
FROM message
WHERE embedding <|4,COSINE|> $q
ORDER BY relevance DESC
LIMIT 5;

-- Live queries. Subscribe to real-time changes on any table with
-- optional filters. The database pushes notifications on every
-- CREATE, UPDATE, and DELETE that matches the query. Diff mode
-- sends only the changed fields instead of the full record.

-- Step 1: Subscribe to new tasks assigned to the current user

LIVE SELECT * FROM task
WHERE assignee = $auth.id AND status = "open";

-- Step 2: Monitor memory changes across all agents

LIVE SELECT content, valid_until,
-- Walk the graph to include which agent owns each memory
<-recalls<-agent.id AS agent
FROM memory;

-- Step 3: Watch for new messages arriving in a specific session

LIVE SELECT role, content, at
FROM message
WHERE <-contains<-session = session:active;

-- Step 4: Diff mode — receive only changed fields instead of the full record

LIVE SELECT DIFF FROM task WHERE status != "closed";

-- Collaborative filtering. Uses multi-hop graph traversal to find
-- items liked by users with similar taste, then re-ranks those
-- candidates by vector similarity to the user's stated preference.

-- Step 1: Traverse the graph to find candidate items

LET $candidates = SELECT
->liked->item<-liked<-user->liked->item AS recommended
FROM ONLY $auth.id;

-- Step 2: Embed the user's preference for re-ranking

LET $pref = fn::embed("lightweight running shoes");

-- Step 3: Re-rank candidates by blending relevance and popularity

SELECT
id, name, description,
vector::similarity::cosine(embedding, $pref) AS relevance,
-- Count how many users liked each item
count(<-liked<-user) AS popularity
FROM $candidates.recommended
-- Exclude items the user has already liked
WHERE id NOT IN $auth.id->liked->item.id
ORDER BY relevance * 0.7 + popularity * 0.3 DESC
LIMIT 10;

-- Geospatial queries. Proximity lookups powered by geographic
-- distance functions, combined with graph traversals to pull in
-- related data like maintenance history or recent sensor readings.

-- Step 1: Create devices with geographic coordinates

CREATE device:sensor_01 SET
name = "Temperature sensor", location = (51.5074, -0.1278),
status = "active", last_reading = 22.5;

CREATE device:sensor_02 SET
name = "Humidity sensor", location = (51.5080, -0.1260),
status = "active", last_reading = 65.2;

-- Step 2: Find all devices within 5km of a point

LET $point = (51.5074, -0.1278);

SELECT name, status, last_reading,
geo::distance(location, $point) AS distance_m
FROM device
WHERE geo::distance(location, $point) < 5000
ORDER BY distance_m ASC;

-- Step 3: Combine spatial proximity with graph traversal

SELECT name,
geo::distance(location, $point) AS distance_m,
-- Walk inbound edges to find who maintains this device
<-maintains<-technician.name AS maintained_by,
-- Walk outbound edges to get recent sensor readings
->reports->reading[WHERE at > time::now() - 1d] AS recent
FROM device
WHERE geo::distance(location, $point) < 2000;

-- Event-driven automation. Define triggers that fire side effects
-- when data changes. Events can generate embeddings, cascade
-- status updates through the graph, or log changes to an audit
-- trail, all without application-level code.

-- Step 1: Auto-generate embeddings whenever a document is created

DEFINE EVENT embed_on_create ON document WHEN $event = "CREATE" THEN {
UPDATE $after.id SET embedding = fn::embed($after.content);
};

-- Step 2: Cascade status — when a task completes, check if the parent project is done

DEFINE EVENT task_completed ON task
WHEN $event = "UPDATE" AND $before.status != "done" AND $after.status = "done"
THEN {
-- Walk the graph to find the parent project
LET $project = $after.id<-contains<-project[0];
-- Count remaining open tasks under that project
LET $open = count($project->contains->task[WHERE status != "done"]);
-- If none remain, mark the project as complete
IF $open = 0 { UPDATE $project SET status = "complete"; };
};

-- Step 3: Log every change to an immutable audit trail

DEFINE EVENT audit_trail ON document
WHEN $event IN ["CREATE", "UPDATE", "DELETE"]
THEN {
CREATE audit SET table = "document", action = $event,
record = $after.id ?? $before.id, at = time::now(), by = $auth.id;
};

-- Multi-tenant access control. Record-based authentication with
-- row-level permissions that scope data to organisations and
-- agents. Users only see their own tenant's data, and agents
-- can only access their own memories.

-- Step 1: Define record-based signup and signin

DEFINE ACCESS account ON DATABASE TYPE RECORD
SIGNUP (
CREATE user SET email = $email, org = $org,
pass = crypto::argon2::generate($pass)
)
SIGNIN (
SELECT * FROM user
WHERE email = $email AND crypto::argon2::compare(pass, $pass)
);

-- Step 2: Tenant-scoped permissions on the project table

DEFINE TABLE project PERMISSIONS
FOR select, update WHERE org = $auth.org
FOR create WHERE $auth.id != NONE
-- Only admins can delete projects
FOR delete WHERE org = $auth.org AND $auth.role = "admin";

-- Step 3: Agent-scoped permissions on the memory table

DEFINE TABLE memory PERMISSIONS
FOR select, create, update WHERE agent = $auth.id
-- Agents can only delete their own expired memories
FOR delete WHERE agent = $auth.id AND valid_until < time::now();

USE CASES

Built for agents

Context-aware agents, persistent memory, knowledge graphs, real-time collaboration - see what teams are building on the context layer.

See all use cases

AI agents

Build context-aware agents with unified memory, knowledge graphs, and structured context in one system.

Agent memory

Persistent, structured memory - working, semantic, episodic, and procedural - for agents that remember.

Knowledge graphs

Model rich relationships and dependencies. Traverse connections, rank entities, and retrieve structured context in a single query.

Real-time applications

Live queries push changes to subscribers as they commit. No polling, no message broker, no stale reads.

GET STARTED

Start building with the context layer

Object storage to agent memory. A single stack, a single transaction, zero glue code.