Skills api-database-surrealdb
SurrealDB multi-model database - SurrealQL queries, record links, graph relations, live queries, schema definitions, authentication, TypeScript SDK
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/src/skills/api-database-surrealdb" ~/.claude/skills/agents-inc-skills-api-database-surrealdb-df0a9c && rm -rf "$T"
src/skills/api-database-surrealdb/SKILL.mdSurrealDB Patterns
Quick Guide: Use the
SDK (v2+) withsurrealdbandnew Surreal(). Model relationships with record links for simple pointers andconnect()for graph edges with metadata. UseRELATEtables in production withSCHEMAFULLconstraints. Always use parameterized queries (DEFINE FIELD) to prevent injection. Record IDs are$variable-- they are immutable and first-class values in SurrealQL. Live queries push changes without polling.table:id
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use parameterized queries with
for ALL user input -- string interpolation in SurrealQL enables injection attacks)$variables
(You MUST use
in SDK v2 -- plain new RecordId("table", "id")
strings are NOT automatically parsed as record IDs)"table:id"
(You MUST call
or pass namespace/database in db.use({ namespace, database })
options BEFORE any queries -- queries without a selected namespace/database silently fail or error)connect()
(You MUST NOT rely on
tables in production -- use SCHEMALESS
with SCHEMAFULL
to enforce data integrity at the database layer)DEFINE FIELD
(You MUST NOT use
/UPDATE
with DELETE
on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))WHERE
</critical_requirements>
Auto-detection: SurrealDB, Surreal, surrealdb, SurrealQL, RELATE, RecordId, record link, LIVE SELECT, SCHEMAFULL, SCHEMALESS, DEFINE TABLE, DEFINE FIELD, DEFINE ACCESS, surql, graph traversal, ->relation->, <-relation<-
When to use:
- Connecting to SurrealDB and executing queries via the JavaScript SDK
- Modeling data with record links and graph edges (
)RELATE - Defining schemas with
tables and field constraintsSCHEMAFULL - Building real-time features with live queries
- Implementing authentication with
and record-level permissionsDEFINE ACCESS - Multi-tenant architectures using namespaces and databases
Key patterns covered:
- SDK connection setup (v2 API with
,Surreal
,connect
,RecordId
)Table - CRUD operations with type-safe queries
- Record links vs graph edges (when to use each)
- Schema definitions (
,DEFINE TABLE
, permissions)DEFINE FIELD - Live queries for real-time subscriptions
When NOT to use:
- Heavy analytical/OLAP workloads (use a columnar database)
- Simple key-value caching (use a dedicated cache)
- Mature relational schemas that require decades of SQL ecosystem tooling
Detailed Resources:
- For decision frameworks and anti-patterns, see reference.md
Core Patterns:
- examples/core.md - SDK setup, connection, CRUD, TypeScript typing, RecordId
Graph & Relations:
- examples/graph-relations.md - Record links, RELATE, graph traversal, edge metadata
Schema & Auth:
- examples/schema-auth.md - DEFINE TABLE/FIELD, SCHEMAFULL, permissions, DEFINE ACCESS, authentication
Live Queries & Transactions:
- examples/live-queries.md - LIVE SELECT, subscriptions, transactions, events
<philosophy>
Philosophy
SurrealDB is a multi-model database combining document, graph, and relational paradigms with a SQL-inspired query language (SurrealQL). The core principle: model your data the way you think about it -- records link to records, relationships carry metadata, and schemas enforce integrity without separate migration tools.
Core principles:
- Record IDs are first-class -- Every record has a
identity that doubles as a direct pointer. SurrealDB fetches linked records from disk without table scans.table:id - Graph when you need metadata, link when you don't -- Record links (
) are lightweight pointers. Graph edges (friends = [person:tobie]
) store relationship context (timestamps, weights, roles).RELATE person:a->follows->person:b - Schema-full for production --
tables withSCHEMAFULL
constraints enforce types, validation, and defaults at the database layer. UseDEFINE FIELD
only for rapid prototyping.SCHEMALESS - Permissions at every level -- Namespace, database, table, and field-level permissions.
withDEFINE ACCESS
/SIGNUP
enables end-user authentication without a separate auth service.SIGNIN - Real-time by default --
pushes changes to subscribers as they commit. No polling, no message broker.LIVE SELECT - Parameterize everything -- SurrealQL variables (
,$email
) prevent injection and improve query plan caching.$limit
<patterns>
Core Patterns
Pattern 1: SDK Connection
SDK v2 uses
new Surreal() -- always set namespace/database at connection time and use 127.0.0.1 (not localhost, which can fail with IPv6 on Node.js 18+).
import Surreal from "surrealdb"; const db = new Surreal(); await db.connect("http://127.0.0.1:8000", { namespace: "myapp", database: "production", }); await db.signin({ username: "root", password: "root" });
Full connection patterns (production config, event monitoring, graceful shutdown): examples/core.md
Pattern 2: CRUD with RecordId
SDK v2 requires
RecordId objects -- plain strings are NOT automatically parsed as record IDs. Use Table for table-scoped operations, RecordId for specific records.
import { RecordId, Table } from "surrealdb"; const created = await db.create<User>(new Table("user"), { name: "Alice", role: "user", }); const user = await db.select<User>(new RecordId("user", "alice")); await db.merge(new RecordId("user", "alice"), { role: "admin" }); await db.delete(new RecordId("user", "alice"));
Full CRUD patterns (create, select, update, delete, bulk operations): examples/core.md
Pattern 3: Parameterized Queries
Always bind user input as
$parameters -- never interpolate strings into SurrealQL. Multi-statement queries return typed tuples.
const users = await db.query<[User[]]>( `SELECT * FROM user WHERE role = $role LIMIT $limit`, { role: "admin", limit: 20 }, ); // BAD: enables SurrealQL injection await db.query(`SELECT * FROM user WHERE email = '${userInput}'`);
Full query patterns (pagination, multi-statement, RecordId parameters): examples/core.md
Pattern 4: Record Links (Lightweight Pointers)
Record links are field-level pointers fetched via dot notation -- no JOINs required. Use for simple, unidirectional references without relationship metadata.
CREATE person:alice SET best_friend = person:bob, friends = [person:bob, person:carol]; SELECT best_friend.name AS friend_name FROM person:alice;
When NOT to use: When you need relationship metadata, bidirectional traversal, or relationship-level permissions -- use graph edges instead.
Full record link patterns: examples/graph-relations.md
Pattern 5: Graph Edges with RELATE
Graph edges are full records in a relation table, supporting metadata, bidirectional traversal (
<->), and schema constraints via DEFINE TABLE TYPE RELATION.
RELATE person:alice->follows->person:bob SET followed_at = time::now(), strength = "close"; SELECT ->follows->person.name AS following FROM person:alice; -- forward SELECT <-follows<-person.name AS followers FROM person:bob; -- reverse
When to use: Relationships needing metadata, bidirectional queries, social graphs, access control graphs.
Full graph patterns (typed relations, edge metadata, recursive traversal): examples/graph-relations.md
</patterns><red_flags>
RED FLAGS
High Priority Issues:
- Using string interpolation instead of
in SurrealQL queries -- enables injection attacks$parameters - Using
strings instead of"table:id"
in SDK v2 -- strings are not auto-parsed as record IDsnew RecordId("table", "id") - Running queries without selecting namespace/database -- queries silently fail or return errors
- Using
tables in production without explicit field definitions -- data integrity not enforcedSCHEMALESS
Medium Priority Issues:
on large tables without indexes -- SurrealDB does not use indexes for UPDATE/DELETE WHERE (useUPDATE table SET ... WHERE condition
as workaround)UPDATE (SELECT id FROM table WHERE condition) SET ...- Using
without a unique index --UPSERT
is much more performant with unique indexes (avoids table scan)UPSERT - Embedding unbounded arrays as record links -- arrays can grow without limit; use graph edges for unbounded relationships
- Not setting
andDURATION FOR TOKEN
onDURATION FOR SESSION
-- tokens/sessions without expiry are a security riskDEFINE ACCESS
Common Mistakes:
- Creating duplicate record IDs silently fails or errors depending on context -- use
orINSERT ... ON DUPLICATE KEY UPDATE
for idempotent operationsUPSERT - Expecting
strings to sort numerically --record:id
,record:1
,record:10
sorts lexicographically; use numeric IDs (record:2
,record:1
,record:2
) or ULID/UUID for temporal sortingrecord:10 - Forgetting that record IDs are immutable -- you cannot change a record's ID after creation; you must create a new record and delete the old one
- Using
,rand()
, orulid()
inuuid()
bodies -- these generate the same value per function call, causing duplicate key errors on subsequent callsDEFINE FUNCTION - Confusing
(recalculated on create/update) withDEFINE FIELD ... VALUE
(recalculated on access, v3.0+)DEFINE FIELD ... COMPUTED - Setting
field inid
-- the explicit record ID takes precedence and theCREATE table:specific_id SET id = "other"
in SET is silently discardedid
Gotchas & Edge Cases:
- Fields defined with
are recalculated alphabetically -- if fieldVALUE
depends on fieldb
, naming mattersa
on aFLEXIBLE TYPE
table allows schemaless nested objects -- useful for JSON metadata but bypasses type checking on that subtreeSCHEMAFULL
with complexLIVE SELECT
filters may not fire for all edge cases -- test your filters thoroughlyWHERE
in connection strings can fail on Node.js 18+ due to IPv6 preference -- uselocalhost127.0.0.1- Numeric string IDs (
) display as backtick-escaped ("10"
10`table:\
table:10`)) to differentiate from numeric IDs ( - Record References (
) are experimental (requireDEFINE FIELD ... REFERENCE
) -- do not use in production--allow-experimental record_references
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use parameterized queries with
for ALL user input -- string interpolation in SurrealQL enables injection attacks)$variables
(You MUST use
in SDK v2 -- plain new RecordId("table", "id")
strings are NOT automatically parsed as record IDs)"table:id"
(You MUST call
or pass namespace/database in db.use({ namespace, database })
options BEFORE any queries -- queries without a selected namespace/database silently fail or error)connect()
(You MUST NOT rely on
tables in production -- use SCHEMALESS
with SCHEMAFULL
to enforce data integrity at the database layer)DEFINE FIELD
(You MUST NOT use
/UPDATE
with DELETE
on large tables without indexes -- SurrealDB currently does not use indexes for UPDATE/DELETE WHERE clauses (use subquery workaround))WHERE
Failure to follow these rules will cause injection vulnerabilities, silent query failures, or data integrity issues.
</critical_reminders>