Model relationships

SurrealDB offers two ways to relate records, and Schemic models both. Use a record link when one record points at another; use a graph edge when the connection itself is a record that can carry its own data. This guide covers each.

A record link is a field whose value is the id of another record. Import the target table and call its .record() method, so the table name lives only in that table’s definition:

schema.ts TypeScript
import { s, defineTable } from "@schemic/core";
import { User } from "./user";

export const Post = defineTable("post", {
  id: s.string(),
  title: s.string(),
  author: User.record(),
});

produces:

Generated DDL SurrealQL
DEFINE TABLE post TYPE NORMAL SCHEMAFULL;
DEFINE FIELD title ON TABLE post TYPE string;
DEFINE FIELD author ON TABLE post TYPE record<user>;

author is a record<user> on the wire: a RecordId pointing at a user. In app code it is the link itself; resolve it with a SurrealQL FETCH or a SELECT when you need the linked record.

To store a list of links, add .array()User.record().array(). To link to several tables, you have two equivalent spellings: s.recordId([User, Service]) for a merged record<user | service>, or User.record().or(Service.record()) for a distinct record<user> | record<service>. Both round-trip; the merged form reads cleaner.

Create a graph edge

When the relationship has its own identity or data — a purchased edge with a quantity, a follows edge with a timestamp — model it as an edge table with defineRelation. Chain .from(...) and .to(...) to set its endpoints:

schema.ts TypeScript
import { s, defineRelation } from "@schemic/core";
import { surql } from "surrealdb";
import { User } from "./user";
import { Post } from "./post";

export const Authored = defineRelation("authored", {
  id: s.string(),
  at: s.datetime().$default(surql`time::now()`),
})
  .from(User)
  .to(Post);

produces:

Generated DDL SurrealQL
DEFINE TABLE authored TYPE RELATION FROM user TO post SCHEMAFULL;
DEFINE FIELD at ON TABLE authored TYPE datetime DEFAULT time::now();

.from(User).to(Post) produces TYPE RELATION FROM user TO post. The edge’s in and out endpoints are implicit, like id — Schemic does not emit DEFINE FIELD for them, and RelationDef exposes them as typed in and out. Omit .from/.to to leave the relation unrestricted (TYPE RELATION).

Write and read an edge

Create an edge with SurrealQL RELATE, and read it back with decode like any table:

TypeScript
import { Surreal, RecordId } from "surrealdb";
import { Authored } from "./schema/authored";

const db = new Surreal();
// ...connect...

// in -> out, plus any edge fields ("at" is filled by its default here)
await db.query("RELATE $author->authored->$post", {
  author: new RecordId("user", "ada"),
  post: new RecordId("post", "hello"),
});

// read edges back and decode them to app values
const rows = await db.select("authored");
const edges = rows.map((r) => Authored.decode(r));

Which one to use

UseWhen
User.record()A simple “belongs to” / “points at” link with no data of its own.
defineRelation(...)The connection is itself a record — it has fields, or many-to-many semantics you query as a graph.

Where to go next