SurrealDB vs EdgeDB vs ArangoDB: Multi-Model Databases Compared (2026)
TL;DR: SurrealDB is the multi-model database with its own query language (SurrealQL) — documents, graphs, time-series, and real-time subscriptions in one engine with row-level permissions built in. EdgeDB is the graph-relational database built on Postgres — a typed schema language (SDL), EdgeQL for expressive queries, and automatic migrations that make complex data models simple. ArangoDB is the mature multi-model database — documents, graphs, and full-text search with AQL (ArangoDB Query Language), Foxx microservices, and proven production scale. In 2026: SurrealDB for real-time multi-model with built-in auth, EdgeDB for type-safe graph-relational with Postgres foundations, ArangoDB for battle-tested multi-model at scale.
Key Takeaways
- SurrealDB: Rust-based, multi-model (document + graph + time-series). SurrealQL, real-time subscriptions, row-level permissions, schemaless or schemafull. Best for real-time apps needing multiple data models with built-in auth
- EdgeDB: Built on Postgres, graph-relational. Typed SDL schema, EdgeQL, computed properties, automatic migrations. Best for complex data models where type safety and query expressiveness matter
- ArangoDB: C++ based, mature multi-model (document + graph + search). AQL, Foxx microservices, SmartGraphs, full-text search. Best for production graph + document workloads at scale
SurrealDB — Multi-Model with Real-Time
SurrealDB combines documents, graphs, and real-time subscriptions in one database with row-level security and its own query language.
Connection and Basic CRUD
import Surreal from "surrealdb";
const db = new Surreal();
await db.connect("ws://localhost:8000");
// Authenticate
await db.signin({ username: "root", password: "root" });
await db.use({ namespace: "app", database: "production" });
// Create a record with a specific ID
const user = await db.create("user:jane", {
name: "Jane Doe",
email: "jane@acme.com",
role: "admin",
created_at: new Date().toISOString(),
settings: {
theme: "dark",
notifications: true,
},
});
// Create with auto-generated ID
const order = await db.create("order", {
customer: "user:jane",
items: [
{ product: "product:widget-a", quantity: 3, price: 29.99 },
{ product: "product:widget-b", quantity: 1, price: 49.99 },
],
total: 139.96,
status: "pending",
});
// Select records
const users = await db.select("user");
const jane = await db.select("user:jane");
// Update
await db.merge("user:jane", { role: "superadmin" });
// Delete
await db.delete("order:old-order-id");
SurrealQL — Advanced Queries
// SurrealQL — SQL-like but with graph and document features
// Fetch with nested relations
const result = await db.query(`
SELECT
*,
customer.name AS customer_name,
items.*.product.name AS product_names,
(SELECT count() FROM order WHERE customer = $parent.id) AS order_count
FROM order
WHERE status = 'pending'
AND total > 100
ORDER BY created_at DESC
LIMIT 20
`);
// Graph traversal — find connections
const connections = await db.query(`
SELECT
->follows->user.name AS following,
<-follows<-user.name AS followers,
->follows->user->follows->user.name AS friends_of_friends
FROM user:jane
`);
// Create graph edges (relations)
await db.query(`RELATE user:jane->follows->user:john`);
await db.query(`RELATE user:jane->purchased->product:widget-a SET
quantity = 3,
price = 29.99,
purchased_at = time::now()
`);
// Time-series queries
const metrics = await db.query(`
SELECT
math::mean(value) AS avg_value,
math::max(value) AS max_value,
count() AS count
FROM metric
WHERE timestamp > time::now() - 1h
GROUP BY time::floor(timestamp, 5m)
ORDER BY timestamp
`);
// Full-text search
const results = await db.query(`
SELECT * FROM article
WHERE content @@ 'database AND (graph OR multi-model)'
ORDER BY search::score(1) DESC
`);
Schema Definition and Permissions
// Define schema (optional — SurrealDB can be schemaless)
await db.query(`
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string ASSERT string::len($value) > 0;
DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);
DEFINE FIELD role ON user TYPE string DEFAULT 'viewer'
ASSERT $value IN ['viewer', 'editor', 'admin', 'superadmin'];
DEFINE FIELD created_at ON user TYPE datetime DEFAULT time::now();
DEFINE FIELD settings ON user TYPE object;
DEFINE INDEX email_idx ON user COLUMNS email UNIQUE;
`);
// Row-level permissions — built-in, no middleware needed
await db.query(`
-- Users can only read their own data
DEFINE TABLE user_profile SCHEMAFULL
PERMISSIONS
FOR select WHERE id = $auth.id
FOR create NONE
FOR update WHERE id = $auth.id
FOR delete NONE;
-- Orders visible to owner and admins
DEFINE TABLE order SCHEMAFULL
PERMISSIONS
FOR select WHERE customer = $auth.id OR $auth.role = 'admin'
FOR create WHERE $auth.id != NONE
FOR update WHERE customer = $auth.id
FOR delete WHERE $auth.role = 'admin';
`);
// Scope-based authentication
await db.query(`
DEFINE SCOPE user_scope SESSION 24h
SIGNUP (
CREATE user SET
email = $email,
password = crypto::argon2::generate($password),
name = $name
)
SIGNIN (
SELECT * FROM user WHERE
email = $email AND
crypto::argon2::compare(password, $password)
);
`);
// Sign up a user via scope
await db.signup({
namespace: "app",
database: "production",
scope: "user_scope",
email: "new@user.com",
password: "securePassword",
name: "New User",
});
Real-Time Subscriptions
// Live queries — real-time data streaming
const queryUuid = await db.live("order", (action, result) => {
switch (action) {
case "CREATE":
console.log("New order:", result);
notifyDashboard(result);
break;
case "UPDATE":
console.log("Order updated:", result);
updateDashboard(result);
break;
case "DELETE":
console.log("Order deleted:", result);
removeDashboard(result);
break;
}
});
// Live query with filters
await db.query(`
LIVE SELECT * FROM order
WHERE status = 'pending' AND total > 100
`);
// Stop listening
await db.kill(queryUuid);
EdgeDB — Graph-Relational on Postgres
EdgeDB is a graph-relational database built on Postgres — a typed schema language, expressive EdgeQL, and automatic migrations.
Schema Definition (SDL)
# dbschema/default.esdl — EdgeDB Schema Definition Language
module default {
type User {
required name: str;
required email: str {
constraint exclusive; # Unique
};
role: str {
default := 'viewer';
constraint one_of('viewer', 'editor', 'admin');
};
created_at: datetime {
default := datetime_current();
};
multi link orders := .<customer[is Order];
# Computed: count of orders
property order_count := count(.orders);
}
type Product {
required name: str;
required price: decimal;
description: str;
tags: array<str>;
multi link reviews := .<product[is Review];
property avg_rating := math::mean(.reviews.rating);
}
type Order {
required link customer: User;
required status: str {
default := 'pending';
constraint one_of('pending', 'paid', 'shipped', 'delivered', 'cancelled');
};
required created_at: datetime {
default := datetime_current();
};
multi link items: OrderItem;
property total := sum(.items.subtotal);
}
type OrderItem {
required link product: Product;
required quantity: int32 {
constraint min_value(1);
};
required unit_price: decimal;
property subtotal := .quantity * .unit_price;
}
type Review {
required link product: Product;
required link author: User;
required rating: int16 {
constraint min_value(1);
constraint max_value(5);
};
body: str;
created_at: datetime {
default := datetime_current();
};
}
}
EdgeQL Queries
import { createClient } from "edgedb";
const client = createClient();
// Insert a user
const user = await client.querySingle(`
INSERT User {
name := <str>$name,
email := <str>$email,
role := 'admin'
}
`, { name: "Jane Doe", email: "jane@acme.com" });
// Query with nested relations (no JOINs needed)
const orders = await client.query(`
SELECT Order {
id,
status,
created_at,
total,
customer: {
name,
email,
order_count
},
items: {
quantity,
unit_price,
subtotal,
product: {
name,
price,
avg_rating
}
}
}
FILTER .status = 'pending' AND .total > 100
ORDER BY .created_at DESC
LIMIT 20
`);
// Graph traversal — computed backlinks
const userWithOrders = await client.querySingle(`
SELECT User {
name,
email,
order_count,
orders: {
status,
total,
items: {
product: { name },
quantity
}
} ORDER BY .created_at DESC
}
FILTER .email = <str>$email
`, { email: "jane@acme.com" });
// Aggregation queries
const stats = await client.querySingle(`
SELECT {
total_orders := count(Order),
total_revenue := sum(Order.total),
avg_order_value := math::mean(Order.total),
top_products := (
SELECT Product {
name,
total_sold := count(.<product[is OrderItem]),
revenue := sum(.<product[is OrderItem].subtotal)
}
ORDER BY .revenue DESC
LIMIT 5
)
}
`);
Type-Safe Client (Generated)
// Generate typed client from schema
// $ npx @edgedb/generate edgeql-js
import e from "./dbschema/edgeql-js";
// Fully typed queries — compile-time validation
const query = e.select(e.Order, (order) => ({
id: true,
status: true,
total: true,
customer: {
name: true,
email: true,
},
items: {
quantity: true,
product: { name: true, price: true },
},
filter: e.op(order.status, "=", "pending"),
order_by: { expression: order.created_at, direction: e.DESC },
limit: 20,
}));
const orders = await query.run(client);
// orders is fully typed: { id: string; status: string; total: number; ... }[]
// Type-safe insert
const newOrder = await e.insert(e.Order, {
customer: e.select(e.User, (u) => ({
filter_single: e.op(u.email, "=", "jane@acme.com"),
})),
status: "pending",
items: e.set(
e.insert(e.OrderItem, {
product: e.select(e.Product, (p) => ({
filter_single: e.op(p.name, "=", "Widget A"),
})),
quantity: 3,
unit_price: 29.99,
})
),
}).run(client);
Migrations
# EdgeDB automatic migrations
# After editing your .esdl schema:
# Generate migration
edgedb migration create
# → Creates dbschema/migrations/00001.edgeql automatically
# Apply migration
edgedb migrate
# Migration squashing for production
edgedb migration squash
# Show current schema diff
edgedb migration status
ArangoDB — Battle-Tested Multi-Model
ArangoDB is the mature multi-model database — documents, graphs, and full-text search with AQL and proven production reliability.
Document Operations
import { Database, aql } from "arangojs";
const db = new Database({
url: "http://localhost:8529",
databaseName: "app",
auth: { username: "root", password: process.env.ARANGO_PASSWORD! },
});
// Create collections
const users = db.collection("users");
await users.create(); // Document collection
const follows = db.collection("follows");
await follows.create({ type: 3 }); // Edge collection (type: 3)
// Insert documents
const user = await users.save({
_key: "jane",
name: "Jane Doe",
email: "jane@acme.com",
role: "admin",
created_at: new Date().toISOString(),
});
// Read
const jane = await users.document("jane");
// Update
await users.update("jane", { role: "superadmin" });
// Replace
await users.replace("jane", { ...jane, role: "superadmin" });
// Remove
await users.remove("old-user-key");
AQL — ArangoDB Query Language
// AQL queries — SQL-like with document and graph features
// Basic document query
const cursor = await db.query(aql`
FOR user IN users
FILTER user.role == 'admin'
SORT user.created_at DESC
LIMIT 20
RETURN {
name: user.name,
email: user.email,
created_at: user.created_at
}
`);
const admins = await cursor.all();
// Join across collections
const ordersWithCustomers = await db.query(aql`
FOR order IN orders
FILTER order.status == 'pending' AND order.total > 100
LET customer = DOCUMENT(users, order.customer_key)
LET items = (
FOR item IN order.items
LET product = DOCUMENT(products, item.product_key)
RETURN MERGE(item, { product_name: product.name })
)
SORT order.created_at DESC
RETURN {
order_id: order._key,
customer_name: customer.name,
items: items,
total: order.total,
status: order.status
}
`);
// Aggregation
const stats = await db.query(aql`
FOR order IN orders
COLLECT status = order.status
AGGREGATE
count = LENGTH(1),
total = SUM(order.total),
avg_total = AVERAGE(order.total)
SORT count DESC
RETURN { status, count, total, avg_total }
`);
Graph Queries
// Create graph edges
await follows.save({
_from: "users/jane",
_to: "users/john",
since: new Date().toISOString(),
});
// Graph traversal — find followers and following
const socialGraph = await db.query(aql`
// Direct followers
LET followers = (
FOR v, e IN 1..1 INBOUND 'users/jane' follows
RETURN { name: v.name, since: e.since }
)
// Who Jane follows
LET following = (
FOR v, e IN 1..1 OUTBOUND 'users/jane' follows
RETURN { name: v.name, since: e.since }
)
// Friends of friends (2 hops)
LET fof = (
FOR v IN 2..2 OUTBOUND 'users/jane' follows
OPTIONS { uniqueVertices: 'global' }
FILTER v._key != 'jane'
RETURN DISTINCT v.name
)
RETURN { followers, following, friends_of_friends: fof }
`);
// Shortest path
const path = await db.query(aql`
FOR v, e IN OUTBOUND SHORTEST_PATH
'users/jane' TO 'users/bob'
follows
RETURN { vertex: v.name, edge: e }
`);
// Named graph operations
const graph = db.graph("social");
await graph.create({
edgeDefinitions: [
{
collection: "follows",
from: ["users"],
to: ["users"],
},
{
collection: "purchased",
from: ["users"],
to: ["products"],
},
],
});
Full-Text Search (ArangoSearch)
// Create an ArangoSearch view
await db.createView("articles_search", {
type: "arangosearch",
links: {
articles: {
fields: {
title: { analyzers: ["text_en"] },
content: { analyzers: ["text_en"] },
tags: { analyzers: ["identity"] },
},
},
},
});
// Full-text search with ranking
const searchResults = await db.query(aql`
FOR doc IN articles_search
SEARCH ANALYZER(
doc.title IN TOKENS('multi-model database comparison', 'text_en')
OR doc.content IN TOKENS('graph queries real-time', 'text_en'),
'text_en'
)
SORT BM25(doc) DESC
LIMIT 10
RETURN {
title: doc.title,
score: BM25(doc),
snippet: SUBSTRING(doc.content, 0, 200)
}
`);
// Faceted search
const faceted = await db.query(aql`
FOR doc IN articles_search
SEARCH ANALYZER(doc.content IN TOKENS($query, 'text_en'), 'text_en')
COLLECT tag = doc.tags[*] INTO groups
SORT LENGTH(groups) DESC
RETURN { tag, count: LENGTH(groups) }
`, { query: "database" });
Transactions
// Multi-collection ACID transactions
const trx = await db.beginTransaction({
write: ["orders", "inventory", "order_items"],
read: ["products"],
});
try {
// Check inventory
const product = await trx.step(() =>
db.query(aql`
FOR p IN products FILTER p._key == ${productKey} RETURN p
`)
);
if (product.stock < quantity) {
await trx.abort();
throw new Error("Insufficient stock");
}
// Create order
await trx.step(() =>
db.collection("orders").save(
{ customer_key: userId, total: price * quantity, status: "pending" },
{ returnNew: true }
)
);
// Update inventory
await trx.step(() =>
db.query(aql`
UPDATE ${productKey} WITH { stock: ${product.stock - quantity} } IN products
`)
);
await trx.commit();
} catch (error) {
await trx.abort();
throw error;
}
Feature Comparison
| Feature | SurrealDB | EdgeDB | ArangoDB |
|---|---|---|---|
| Language | Rust | Python + Postgres | C++ |
| Data Models | Document, graph, time-series | Graph-relational | Document, graph, search |
| Query Language | SurrealQL | EdgeQL | AQL |
| Schema | Optional (schemaless/schemafull) | Required (SDL) | Optional |
| Type Safety | Runtime validation | Compile-time (generated client) | Runtime validation |
| Migrations | Schema changes | ✅ (automatic) | Manual |
| Graph Queries | ->relates-> syntax | Backlinks + computed | Traversals + shortest path |
| Full-Text Search | Basic (@@ operator) | Basic | ✅ (ArangoSearch / BM25) |
| Real-Time | ✅ (LIVE queries) | ❌ | ❌ (Change streams via Foxx) |
| Auth / Permissions | ✅ (built-in scopes + RLS) | ❌ (external) | ❌ (external) |
| Transactions | ✅ | ✅ (Postgres ACID) | ✅ (multi-collection) |
| Computed Properties | ❌ | ✅ (schema-level) | ❌ (view-level) |
| Node.js SDK | surrealdb | edgedb + generated client | arangojs |
| Storage Engine | Custom (Rust) | Postgres | RocksDB |
| Replication | ✅ (TiKV/cluster) | Postgres replication | ✅ (synchronous) |
| Cloud Managed | SurrealDB Cloud | EdgeDB Cloud | ArangoDB Oasis |
| Maturity | Young (v1.x) | Growing (v4+) | Mature (v3.11+) |
| License | BSL → Apache 2.0 | Apache 2.0 | Apache 2.0 |
| Best For | Real-time + multi-model | Type-safe graph-relational | Production graph + docs |
When to Use Each
Choose SurrealDB if:
- You need multiple data models (document + graph + time-series) in one database
- Built-in authentication and row-level permissions reduce middleware
- Real-time subscriptions (LIVE queries) are part of your architecture
- You want to start schemaless and add schema constraints later
- SurrealQL's SQL-like syntax with graph traversal fits your query patterns
Choose EdgeDB if:
- Type safety from schema to query to application code is important
- Complex data models with computed properties and backlinks are your use case
- You want automatic migrations that understand your schema changes
- Postgres as the underlying engine gives you production confidence
- The generated TypeScript client eliminates query-result mismatches
Choose ArangoDB if:
- You need production-proven multi-model at scale (documents + graphs + search)
- ArangoSearch with BM25 ranking replaces a separate search engine
- Graph traversals (shortest path, pattern matching) are core to your queries
- Multi-collection ACID transactions are required
- The mature ecosystem with Foxx microservices and satellite collections matters
Methodology
Feature comparison based on SurrealDB v1.x, EdgeDB v4+, and ArangoDB v3.11+ documentation as of March 2026. Code examples use official Node.js clients (surrealdb, edgedb, arangojs). Schema capabilities evaluated on type system, constraints, and migration support. Query capabilities evaluated on document operations, graph traversals, aggregation, and full-text search.