Skip to content

SurrealDB

SurrealDB backend using the embedded SurrealKV engine — file-backed, no server needed. Documents are rendered into <type> tables with hash (bytes) and doc (flexible object) fields. Binary fields use SurrealDB's native bytes type.

Setup

typescript
import { DocumentStore, SurrealdbPersistence } from 'document-store';

// File-backed (SurrealKV embedded engine — use absolute paths)
const storage = new SurrealdbPersistence('surrealkv:///path/to/data');

// In-memory (testing)
const storage = new SurrealdbPersistence('mem://');

// Wait for connection + schema init
await storage.ready();

const store = new DocumentStore({ storage });

store.registerType('bookmark', {
    render: { time: true, uid: true, updated: true }
});

You can also pass a pre-connected Surreal instance:

typescript
import { Surreal } from 'surrealdb';
import { createNodeEngines } from '@surrealdb/node';

const db = new Surreal({ engines: createNodeEngines() });
await db.connect('surrealkv:///path/to/data');
await db.use({ namespace: 'myapp', database: 'main' });

const store = new DocumentStore({
    storage: new SurrealdbPersistence(db)
});

Table structure

Each registered type gets a rendered table:

surql
DEFINE TABLE bookmark SCHEMAFULL;
DEFINE FIELD hash ON bookmark TYPE bytes;
DEFINE FIELD doc ON bookmark TYPE option<object> FLEXIBLE;
DEFINE INDEX hash_idx ON bookmark FIELDS hash UNIQUE;

The doc field contains all rendered fields with native types — Buffers become SurrealDB bytes, numbers stay numbers, etc.

Reading documents

Use SurrealQL to query rendered tables directly. Access fields via doc.<fieldname>.

Simple queries

surql
-- All bookmarks
SELECT * FROM bookmark;

-- By hash
SELECT * FROM bookmark WHERE hash = $hash;

-- Filter by field
SELECT * FROM bookmark WHERE doc.url = 'https://example.com';

From Node.js:

typescript
const db = store.storage.db; // or your own Surreal instance

// All bookmarks, newest first
const [bookmarks] = await db.query(
    'SELECT * FROM bookmark ORDER BY doc.updated DESC'
);

// Filter by owner
const [mine] = await db.query(
    'SELECT doc FROM bookmark WHERE doc.uid = $uid',
    { uid: myUidAsArrayBuffer }
);

Buffer conversion

SurrealDB uses ArrayBuffer for binary data. Convert at the boundary:

typescript
// Buffer → ArrayBuffer (for queries)
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);

// ArrayBuffer → Buffer (from results)
const buf = Buffer.from(ab);

Sorting and pagination

surql
-- Newest first, paginated
SELECT * FROM bookmark
ORDER BY doc.updated DESC
LIMIT 20 START 0;

-- Second page
SELECT * FROM bookmark
ORDER BY doc.updated DESC
LIMIT 20 START 20;

SurrealDB's multi-model nature shines for document relationships. Bookmarks reference parent folders by hash — use subqueries or record links:

surql
-- Bookmarks with their folder name (subquery)
SELECT
    doc,
    (SELECT doc.name FROM folder WHERE hash = $parent.doc.parent)[0] AS folder
FROM bookmark;

For richer relationships, you can create record links or graph edges alongside the rendered documents:

surql
-- Create an edge when a bookmark is added to a folder
RELATE folder:folder_id->contains->bookmark:bookmark_id;

-- Traverse: all bookmarks in a folder
SELECT ->contains->bookmark.doc FROM folder WHERE hash = $folderHash;

-- Reverse: which folder contains this bookmark?
SELECT <-contains<-folder.doc FROM bookmark WHERE hash = $bookmarkHash;

Graph traversal

For deeply nested structures (folder trees, comment threads):

surql
-- All descendants of a folder (recursive graph traversal)
SELECT ->contains->(1..10)->bookmark.doc AS bookmarks
FROM folder WHERE hash = $rootHash;

-- Full folder tree
SELECT doc, ->contains->folder.doc AS subfolders
FROM folder WHERE hash = $rootHash;

Aggregation

surql
-- Count bookmarks per owner
SELECT doc.uid, count() AS total
FROM bookmark
GROUP BY doc.uid;

-- Bookmarks added per day
SELECT
    time::floor(doc.time, 1d) AS day,
    count() AS total
FROM bookmark
GROUP BY day
ORDER BY day DESC;

Indexes

Create indexes for your query patterns:

surql
DEFINE INDEX updated_idx ON bookmark FIELDS doc.updated;
DEFINE INDEX uid_idx ON bookmark FIELDS doc.uid;
DEFINE INDEX parent_idx ON bookmark FIELDS doc.parent;

-- Composite index
DEFINE INDEX uid_updated_idx ON bookmark FIELDS doc.uid, doc.updated;

-- Full-text search index
DEFINE INDEX title_search ON bookmark FIELDS doc.title SEARCH ANALYZER simple BM25;

-- Then query with @@
SELECT * FROM bookmark WHERE doc.title @@ 'example';

Why SurrealDB?

  • Embedded — SurrealKV runs in-process, no server to manage. Same deployment simplicity as SQLite.
  • Multi-model — relations, graph traversals, and record links natively. No need for separate graph or relation databases.
  • SurrealQL — expressive query language with built-in functions for time, math, string ops, geo, and more.
  • Native types — bytes, datetime, geometry, etc. No JSON encoding/decoding overhead for binary fields.
  • Scales up — same query language works against embedded SurrealKV, local TiKV, or distributed SurrealDB cluster.