Appearance
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;Relations and record links
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.