Skip to main content

Guide

SurrealDB vs EdgeDB vs ArangoDB 2026

Compare SurrealDB, EdgeDB, and ArangoDB for multi-model use cases. Graph queries, document storage, Node.js SDKs, and which next-gen database fits your stack.

·PkgPulse Team·
0

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

Production Maturity and Operational Considerations

The maturity gap between these three databases is significant and should factor into production deployment decisions. ArangoDB at version 3.11+ has over a decade of production deployments, enterprise customers with billions of documents, and a well-documented operational playbook covering backup, restore, replication configuration, and performance tuning. EdgeDB v4+ builds directly on PostgreSQL, inheriting Postgres's rock-solid storage engine, ACID guarantees, and decades of operational knowledge — your database operations team's Postgres expertise transfers directly to EdgeDB backups, point-in-time recovery, and monitoring with standard Postgres tools like pgBadger. SurrealDB v1.x is the youngest of the three and has seen rapid development including several architectural changes; teams deploying to production should account for the possibility of migration overhead during major version upgrades and should prefer pinning to a specific release rather than following latest.

Security Models and Authentication

Each database takes a fundamentally different approach to authentication and authorization. SurrealDB's built-in scope-based authentication and row-level security (DEFINE TABLE ... PERMISSIONS) lets you expose the database directly to client applications without a backend middleware layer — a compelling architectural simplification for real-time applications where clients maintain WebSocket connections. This design requires careful permissions modeling: a misconfigured PERMISSIONS clause can expose data across tenant boundaries. EdgeDB defers authentication to the application layer, making it unsuitable for direct client database access — all requests must go through your application server, which is the conventional approach. ArangoDB supports TLS, LDAP integration, role-based access via its built-in user management, and VelocyPack-encrypted cluster communication, making it the most enterprise-ready from an IT security compliance perspective. For multi-tenant SaaS applications, SurrealDB's built-in scope isolation is architecturally attractive, while EdgeDB and ArangoDB require the multi-tenancy logic to live in your application code.

Query Language Expressiveness and Learning Curve

Developer productivity with these databases depends significantly on how quickly the team masters the query language. SurrealQL is the most familiar for developers coming from SQL backgrounds — it uses SELECT, WHERE, ORDER BY, and LIMIT with extensions for graph traversal (->relation->) and time functions. EdgeQL's graph-oriented syntax with FILTER, ORDER BY, and nested shape selections has a steeper initial learning curve but becomes more natural for complex relational queries that would require multiple JOINs in SQL. AQL (ArangoDB Query Language) is Turing-complete with support for FOR...IN, FILTER, COLLECT, AGGREGATE, and SORT — powerful but verbose for simple queries. Teams evaluating these databases should benchmark how long it takes developers to write production queries for their specific access patterns, not just run through toy examples in the documentation.

Self-Hosting vs Cloud Managed Deployment

All three offer self-hosted and cloud managed deployment options with different trade-off profiles. ArangoDB Enterprise (ArangoDB's commercial offering) adds Datacenter-to-Datacenter Replication, Hot Standby, and audit logging for compliance-heavy environments; the community edition is Apache 2.0 licensed and fully featured for most use cases. ArangoDB Oasis (managed cloud) handles cluster provisioning, backups, and scaling on AWS, GCP, or Azure. EdgeDB Cloud provides managed EdgeDB instances with automatic backups, connection pooling, and branch-based development workflows — the branching feature is particularly compelling for teams that want isolated development environments per pull request. SurrealDB Cloud is newer and available in beta; self-hosting via Docker is the primary production deployment path for most SurrealDB users in 2026. For teams without dedicated database operations capability, EdgeDB Cloud's managed experience is currently the most polished of the three.

TypeScript SDK Comparison and Query Building

The TypeScript developer experience diverges significantly across the three databases. EdgeDB's generated querybuilder (npx @edgedb/generate edgeql-js) produces a completely typed query API where invalid queries fail at TypeScript compile time rather than at runtime — a transformative developer experience for teams with complex data models. SurrealDB's SDK accepts SurrealQL as strings with typed response hints, requiring manual type annotations for query results; there's no compile-time query validation. ArangoDB's arangojs client similarly accepts AQL strings and returns any-typed cursor results by default, though the community arangojs types package adds result typing when combined with TypeScript generics. For teams prioritizing type safety throughout the stack, EdgeDB's compile-time query validation is in a different league from the string-based query APIs of SurrealDB and ArangoDB. Teams evaluating the total developer experience should prototype their most complex queries in each system before committing to one.

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.

See also: How to Set Up Drizzle ORM with Next.js (2026 Guide), How to Migrate from Mongoose to Prisma, and Mongoose vs Prisma in 2026: MongoDB vs SQL-First

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.