Skip to main content

SurrealDB vs EdgeDB vs ArangoDB: Multi-Model Databases Compared (2026)

·PkgPulse Team

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

FeatureSurrealDBEdgeDBArangoDB
LanguageRustPython + PostgresC++
Data ModelsDocument, graph, time-seriesGraph-relationalDocument, graph, search
Query LanguageSurrealQLEdgeQLAQL
SchemaOptional (schemaless/schemafull)Required (SDL)Optional
Type SafetyRuntime validationCompile-time (generated client)Runtime validation
MigrationsSchema changes✅ (automatic)Manual
Graph Queries->relates-> syntaxBacklinks + computedTraversals + shortest path
Full-Text SearchBasic (@@ 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 SDKsurrealdbedgedb + generated clientarangojs
Storage EngineCustom (Rust)PostgresRocksDB
Replication✅ (TiKV/cluster)Postgres replication✅ (synchronous)
Cloud ManagedSurrealDB CloudEdgeDB CloudArangoDB Oasis
MaturityYoung (v1.x)Growing (v4+)Mature (v3.11+)
LicenseBSL → Apache 2.0Apache 2.0Apache 2.0
Best ForReal-time + multi-modelType-safe graph-relationalProduction 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.