Cookbook

Examples

Every example is generated and verified against the live driver — the TypeScript you author on the left, the native DDL Schemic emits on the right. Switch databases with the picker in the nav, and filter by category below.

tables

SCHEMAFULL (the default)

defineTable is SCHEMAFULL by default — fields are constrained to the declared shape.

schema.ts TypeScript
defineTable("account", { id: s.string(), name: s.string() })
Generated SurrealQL
DEFINE TABLE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD name ON TABLE account TYPE string;

SCHEMALESS

Opt out with .schemaless() — records may carry undeclared fields.

schema.ts TypeScript
defineTable("blob", { id: s.string() }).schemaless()
Generated SurrealQL
DEFINE TABLE blob TYPE NORMAL SCHEMALESS;

TYPE ANY

Accept any record shape (.typeAny()).

schema.ts TypeScript
defineTable("misc", { id: s.string() }).typeAny()
Generated SurrealQL
DEFINE TABLE misc TYPE ANY SCHEMAFULL;

COMMENT

schema.ts TypeScript
defineTable("account", { id: s.string() }).comment("billing accounts")
Generated SurrealQL
DEFINE TABLE account TYPE NORMAL SCHEMAFULL COMMENT "billing accounts";

table PERMISSIONS

Per-operation row guards; an omitted op defaults to NONE.

schema.ts TypeScript
defineTable("doc", { id: s.string() }).permissions({
  select: surql`true`,
  create: surql`$auth.id != NONE`,
  update: surql`$auth.id = id.owner`,
  delete: surql`$auth.admin = true`,
})
Generated SurrealQL
DEFINE TABLE doc
  TYPE NORMAL
  SCHEMAFULL
  PERMISSIONS
    FOR select WHERE true
    FOR create WHERE $auth.id != NONE
    FOR update WHERE $auth.id = id.owner
    FOR delete WHERE $auth.admin = true;

CHANGEFEED

Retain a change feed for the table; INCLUDE ORIGINAL via the option.

schema.ts TypeScript
defineTable("audited", { id: s.string() }).changefeed("7d", { includeOriginal: true })
Generated SurrealQL
DEFINE TABLE audited
  TYPE NORMAL
  SCHEMAFULL
  CHANGEFEED 7d INCLUDE ORIGINAL;

DROP

A DROP table discards writes (TYPE NORMAL DROP) — useful to retire a table's data.

schema.ts TypeScript
defineTable("legacy", { id: s.string() }).drop(true)
Generated SurrealQL
DEFINE TABLE legacy TYPE NORMAL DROP SCHEMAFULL;

TYPE RELATION (edge table) with FROM / TO / ENFORCED

defineRelation + .from()/.to() restrict endpoints; .enforced() requires both to exist on RELATE.

schema.ts TypeScript
defineRelation("likes", { since: s.datetime() })
  .from(defineTable("user", { id: s.string() }))
  .to(defineTable("post", { id: s.string() }))
  .enforced()
Generated SurrealQL
DEFINE TABLE likes TYPE RELATION FROM user TO post ENFORCED SCHEMAFULL;
DEFINE FIELD since ON TABLE likes TYPE datetime;

TYPE RELATION — unrestricted endpoints

Endpoints are optional; a bare relation links any record to any record.

schema.ts TypeScript
defineRelation("touches", {})
Generated SurrealQL
DEFINE TABLE touches TYPE RELATION SCHEMAFULL;

Pre-computed VIEW (AS SELECT)

defineView emits TYPE ANY SCHEMALESS AS <query>; rows are kept in sync by SurrealDB. No authored fields.

schema.ts TypeScript
defineView("adults", surql`SELECT name, age FROM user WHERE age >= 18`)
Generated SurrealQL
DEFINE TABLE adults
  TYPE ANY
  SCHEMALESS AS SELECT name, age FROM user WHERE age >= 18;

field-types

Scalars

Plus aliases: s.int32/uint32/bigint, s.date (datetime).

schema.ts TypeScript
defineTable("scalars", {
  id: s.string(),
  str: s.string(),
  i: s.int(),
  f: s.float(),
  dec: s.decimal(),
  num: s.number(),
  b: s.boolean(),
  when: s.datetime(),
  uid: s.uuid(),
  raw: s.bytes(),
  dur: s.duration(),
  doc: s.file(),
})
Generated SurrealQL
DEFINE TABLE scalars TYPE NORMAL SCHEMAFULL;
DEFINE FIELD str ON TABLE scalars TYPE string;
DEFINE FIELD i ON TABLE scalars TYPE int;
DEFINE FIELD f ON TABLE scalars TYPE float;
DEFINE FIELD dec ON TABLE scalars TYPE decimal;
DEFINE FIELD num ON TABLE scalars TYPE number;
DEFINE FIELD b ON TABLE scalars TYPE bool;
DEFINE FIELD when ON TABLE scalars TYPE datetime;
DEFINE FIELD uid ON TABLE scalars TYPE uuid;
DEFINE FIELD raw ON TABLE scalars TYPE bytes;
DEFINE FIELD dur ON TABLE scalars TYPE duration;
DEFINE FIELD doc ON TABLE scalars TYPE file;

any / null

schema.ts TypeScript
defineTable("loose", { id: s.string(), anything: s.any(), nothing: s.null() })
Generated SurrealQL
DEFINE TABLE loose TYPE NORMAL SCHEMAFULL;
DEFINE FIELD anything ON TABLE loose TYPE any;
DEFINE FIELD nothing ON TABLE loose TYPE null;

Optionality — option<T> vs T | null vs option<T | null>

Kept distinct (absent vs present-null vs both), unlike SQL drivers that collapse them.

schema.ts TypeScript
defineTable("opt", {
  id: s.string(),
  maybe: s.string().optional(),
  nullable: s.string().nullable(),
  both: s.string().nullish(),
})
Generated SurrealQL
DEFINE TABLE opt TYPE NORMAL SCHEMAFULL;
DEFINE FIELD maybe ON TABLE opt TYPE option<string>;
DEFINE FIELD nullable ON TABLE opt TYPE string | null;
DEFINE FIELD both ON TABLE opt TYPE option<string | null>;

Containers — array / set (with max), object, tuple

schema.ts TypeScript
defineTable("containers", {
  id: s.string(),
  tags: s.array(s.string()),
  top3: s.array(s.string(), { max: 3 }),
  uniq: s.set(s.int()),
  coords: s.tuple([s.float(), s.float()]),
  meta: s.object({ k: s.string(), n: s.int() }),
})
Generated SurrealQL
DEFINE TABLE containers TYPE NORMAL SCHEMAFULL;
DEFINE FIELD tags ON TABLE containers TYPE array<string>;
DEFINE FIELD top3 ON TABLE containers TYPE array<string, 3>;
DEFINE FIELD uniq ON TABLE containers TYPE set<int>;
DEFINE FIELD coords ON TABLE containers TYPE [float, float];
DEFINE FIELD meta ON TABLE containers TYPE object;
DEFINE FIELD meta.k ON TABLE containers TYPE string;
DEFINE FIELD meta.n ON TABLE containers TYPE int;

Literals, enums, scalar unions

schema.ts TypeScript
defineTable("choice", {
  id: s.string(),
  kind: s.literal("a"),
  status: s.enum(["draft", "live", "archived"]),
  idOrName: s.union([s.int(), s.string()]),
})
Generated SurrealQL
DEFINE TABLE choice TYPE NORMAL SCHEMAFULL;
DEFINE FIELD kind ON TABLE choice TYPE "a";
DEFINE FIELD status ON TABLE choice TYPE "draft" | "live" | "archived";
DEFINE FIELD idOrName ON TABLE choice TYPE int | string;

Record links — record<table>, multi-table, array<record<…>>

schema.ts TypeScript
defineTable("links", {
  id: s.string(),
  author: s.recordId("user"),
  owner: s.recordId(["user", "org"]),
  tags: s.array(s.recordId("tag")),
})
Generated SurrealQL
DEFINE TABLE links TYPE NORMAL SCHEMAFULL;
DEFINE FIELD author ON TABLE links TYPE record<user>;
DEFINE FIELD owner ON TABLE links TYPE record<user | org>;
DEFINE FIELD tags ON TABLE links TYPE array<record<tag>>;

Geometry — bare and the 7 kinds

schema.ts TypeScript
defineTable("geo", {
  id: s.string(),
  any: s.geometry(),
  pt: s.geometry("point"),
  ln: s.geometry("line"),
  poly: s.geometry("polygon"),
  mpt: s.geometry("multipoint"),
  mln: s.geometry("multiline"),
  mpoly: s.geometry("multipolygon"),
  coll: s.geometry("collection"),
})
Generated SurrealQL
DEFINE TABLE geo TYPE NORMAL SCHEMAFULL;
DEFINE FIELD any ON TABLE geo TYPE geometry;
DEFINE FIELD pt ON TABLE geo TYPE geometry<point>;
DEFINE FIELD ln ON TABLE geo TYPE geometry<line>;
DEFINE FIELD poly ON TABLE geo TYPE geometry<polygon>;
DEFINE FIELD mpt ON TABLE geo TYPE geometry<multipoint>;
DEFINE FIELD mln ON TABLE geo TYPE geometry<multiline>;
DEFINE FIELD mpoly ON TABLE geo TYPE geometry<multipolygon>;
DEFINE FIELD coll ON TABLE geo TYPE geometry<collection>;

field-clauses

DEFAULT and DEFAULT ALWAYS

Literals stay bare; wrap an expression in surql`…`. ALWAYS re-applies on every update.

schema.ts TypeScript
defineTable("d", {
  id: s.string(),
  role: s.string().$default("member"),
  seen: s.int().$default(0),
  touched: s.datetime().$defaultAlways(surql`time::now()`),
})
Generated SurrealQL
DEFINE TABLE d TYPE NORMAL SCHEMAFULL;
DEFINE FIELD role ON TABLE d TYPE string DEFAULT "member";
DEFINE FIELD seen ON TABLE d TYPE int DEFAULT 0;
DEFINE FIELD touched ON TABLE d TYPE datetime DEFAULT ALWAYS time::now();

VALUE and COMPUTED

VALUE computes/coerces on write; COMPUTED is a derived (virtual) value.

schema.ts TypeScript
defineTable("c", {
  id: s.string(),
  email: s.string().$value(surql`string::lowercase($value)`),
  full: s.string().$computed(surql`name.first + ' ' + name.last`),
})
Generated SurrealQL
DEFINE TABLE c TYPE NORMAL SCHEMAFULL;
DEFINE FIELD email ON TABLE c
  TYPE string
  VALUE string::lowercase($value);
DEFINE FIELD full ON TABLE c
  TYPE string COMPUTED name.first + ' ' + name.last;

ASSERT — raw and baked $-constraints

$min/$max/$length/$gt/... bake into ASSERT; $assert(surql`…`) is the raw escape.

schema.ts TypeScript
defineTable("a", {
  id: s.string(),
  age: s.int().$min(0).$max(120),
  name: s.string().$length(64),
  score: s.float().$gte(0).$lte(1),
  slug: s.string().$assert(surql`$value = /^[a-z-]+$/`),
})
Generated SurrealQL
DEFINE TABLE a TYPE NORMAL SCHEMAFULL;
DEFINE FIELD age ON TABLE a
  TYPE int
  ASSERT $value >= 0 AND $value <= 120;
DEFINE FIELD name ON TABLE a
  TYPE string
  ASSERT string::len($value) == 64;
DEFINE FIELD score ON TABLE a
  TYPE float
  ASSERT $value >= 0 AND $value <= 1;
DEFINE FIELD slug ON TABLE a TYPE string ASSERT $value = /^[a-z-]+$/;

String-format builders (reverse to the builder on pull)

Each bakes an ASSERT; pull recovers the builder (s.email()) not the raw string ASSERT.

schema.ts TypeScript
defineTable("fmt", {
  id: s.string(),
  email: s.email(),
  site: s.url(),
  ip: s.ipv4(),
  id2: s.ulid(),
})
Generated SurrealQL
DEFINE TABLE fmt TYPE NORMAL SCHEMAFULL;
DEFINE FIELD email ON TABLE fmt
  TYPE string
  ASSERT string::is_email($value);
DEFINE FIELD site ON TABLE fmt TYPE string ASSERT string::is_url($value);
DEFINE FIELD ip ON TABLE fmt TYPE string ASSERT string::is_ipv4($value);
DEFINE FIELD id2 ON TABLE fmt TYPE string ASSERT string::is_ulid($value);

READONLY and field COMMENT

schema.ts TypeScript
defineTable("rc", {
  id: s.string(),
  createdAt: s.datetime().$readonly(),
  note: s.string().$comment("free-form note"),
})
Generated SurrealQL
DEFINE TABLE rc TYPE NORMAL SCHEMAFULL;
DEFINE FIELD createdAt ON TABLE rc TYPE datetime READONLY;
DEFINE FIELD note ON TABLE rc TYPE string COMMENT "free-form note";

FLEXIBLE object

.flexible() (alias .loose()) lets a typed object also carry undeclared keys.

schema.ts TypeScript
defineTable("fx", { id: s.string(), meta: s.object({ k: s.string() }).flexible() })
Generated SurrealQL
DEFINE TABLE fx TYPE NORMAL SCHEMAFULL;
DEFINE FIELD meta ON TABLE fx TYPE object FLEXIBLE;
DEFINE FIELD meta.k ON TABLE fx TYPE string;

Field PERMISSIONS

Field-level guards: select/create/update (no delete at field level, matching SurrealQL).

schema.ts TypeScript
defineTable("fp", {
  id: s.string(),
  secret: s.string().$permissions({
    select: surql`$auth.admin = true`,
    update: surql`false`,
  }),
})
Generated SurrealQL
DEFINE TABLE fp TYPE NORMAL SCHEMAFULL;
DEFINE FIELD secret ON TABLE fp
  TYPE string
  PERMISSIONS
    FOR select WHERE $auth.admin = true
    FOR update WHERE false;

REFERENCE — record link with ON DELETE

Reference integrity: REJECT | CASCADE | UNSET | IGNORE | THEN <expr>.

schema.ts TypeScript
defineTable("ref", {
  id: s.string(),
  author: s.recordId("user").reference({ onDelete: "cascade" }),
})
Generated SurrealQL
DEFINE TABLE ref TYPE NORMAL SCHEMAFULL;
DEFINE FIELD author ON TABLE ref
  TYPE record<user>
  REFERENCE ON DELETE CASCADE;

indexes

Plain and composite index

schema.ts TypeScript
defineTable("p", { id: s.string(), a: s.string(), b: s.string() }).index("ab", ["a", "b"])
Generated SurrealQL
DEFINE TABLE p TYPE NORMAL SCHEMAFULL;
DEFINE FIELD a ON TABLE p TYPE string;
DEFINE FIELD b ON TABLE p TYPE string;
DEFINE INDEX ab ON TABLE p FIELDS a, b;

UNIQUE (single field via field.unique() or table.index)

schema.ts TypeScript
defineTable("u", { id: s.string(), email: s.string() }).index("uq", ["email"], { unique: true })
Generated SurrealQL
DEFINE TABLE u TYPE NORMAL SCHEMAFULL;
DEFINE FIELD email ON TABLE u TYPE string;
DEFINE INDEX uq ON TABLE u FIELDS email UNIQUE;

COUNT (materialized row-count, no FIELDS)

schema.ts TypeScript
defineTable("ct", { id: s.string() }).index("rows", [], { count: true })
Generated SurrealQL
DEFINE TABLE ct TYPE NORMAL SCHEMAFULL;
DEFINE INDEX rows ON TABLE ct COUNT;

COMMENT

schema.ts TypeScript
defineTable("cm", { id: s.string(), email: s.string() }).index("uq", ["email"], { unique: true, comment: "email is unique" })
Generated SurrealQL
DEFINE TABLE cm TYPE NORMAL SCHEMAFULL;
DEFINE FIELD email ON TABLE cm TYPE string;
DEFINE INDEX uq ON TABLE cm FIELDS email UNIQUE
  COMMENT "email is unique";

Vector HNSW — minimal (defaults stripped)

Only DIMENSION authored; SurrealDB materializes DIST/TYPE/EFC/M/M0/LM — all stripped so it round-trips.

schema.ts TypeScript
defineTable("vh", { id: s.string(), emb: s.array(s.float()) }).index("vec", ["emb"], { hnsw: { dimension: 4 } })
Generated SurrealQL
DEFINE TABLE vh TYPE NORMAL SCHEMAFULL;
DEFINE FIELD emb ON TABLE vh TYPE array<float>;
DEFINE INDEX vec ON TABLE vh FIELDS emb HNSW DIMENSION 4;

Vector HNSW — tuned

schema.ts TypeScript
defineTable("vh2", { id: s.string(), emb: s.array(s.float()) }).index("vec", ["emb"], { hnsw: { dimension: 8, dist: "cosine", type: "f64", efc: 200, m: 16 } })
Generated SurrealQL
DEFINE TABLE vh2 TYPE NORMAL SCHEMAFULL;
DEFINE FIELD emb ON TABLE vh2 TYPE array<float>;
DEFINE INDEX vec ON TABLE vh2 FIELDS emb HNSW DIMENSION 8 DIST COSINE
  TYPE F64 EFC 200 M 16;

Vector DISKANN — minimal

schema.ts TypeScript
defineTable("vd", { id: s.string(), emb: s.array(s.float()) }).index("vec", ["emb"], { diskann: { dimension: 4 } })
Generated SurrealQL
DEFINE TABLE vd TYPE NORMAL SCHEMAFULL;
DEFINE FIELD emb ON TABLE vd TYPE array<float>;
DEFINE INDEX vec ON TABLE vd FIELDS emb DISKANN DIMENSION 4;

FULLTEXT search index + DEFINE ANALYZER

A FULLTEXT index deps on its analyzer (emitted first). Default BM25(1.2,0.75) is stripped.

schema.ts TypeScript
[
  defineAnalyzer("english", {
    tokenizers: ["blank"],
    filters: ["lowercase", "snowball(english)"],
  }),
  defineTable("doc", { id: s.string(), content: s.string() }).index("ft", ["content"], {
    fulltext: { analyzer: "english", highlights: true },
  }),
]
Generated SurrealQL
DEFINE ANALYZER english TOKENIZERS BLANK FILTERS LOWERCASE, SNOWBALL(ENGLISH);
DEFINE TABLE doc TYPE NORMAL SCHEMAFULL;
DEFINE FIELD content ON TABLE doc TYPE string;
DEFINE INDEX ft ON TABLE doc FIELDS content FULLTEXT ANALYZER english HIGHLIGHTS;

events

Inline event with WHEN + THEN

schema.ts TypeScript
defineTable("account", { id: s.string(), balance: s.float() }).event("overdraft", {
  when: surql`$after.balance < 0`,
  then: surql`CREATE alert SET account = $after.id, kind = 'overdraft'`,
})
Generated SurrealQL
DEFINE TABLE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD balance ON TABLE account TYPE float;
DEFINE EVENT overdraft ON TABLE account
  WHEN $after.balance < 0
  THEN CREATE alert SET account = $after.id, kind = 'overdraft';

Event without WHEN (fires on every change)

schema.ts TypeScript
defineTable("doc", { id: s.string() }).event("touch", {
  then: surql`UPDATE audit SET at = time::now()`,
})
Generated SurrealQL
DEFINE TABLE doc TYPE NORMAL SCHEMAFULL;
DEFINE EVENT touch ON TABLE doc THEN UPDATE audit SET at = time::now();

Standalone event with an ordered THEN array

defineEvent declares an event apart from its table; pull regenerates it inline.

schema.ts TypeScript
defineEvent("account", "onCreate", {
  when: surql`$event = "CREATE"`,
  then: [
    surql`CREATE log SET account = $after.id`,
    surql`UPDATE stats SET count += 1`,
  ],
})
Generated SurrealQL
DEFINE EVENT onCreate ON TABLE account
  WHEN $event = "CREATE"
  THEN (CREATE log SET account = $after.id), (UPDATE stats SET count += 1);

functions

Function with args, return type, body

schema.ts TypeScript
defineFunction("greet", { name: s.string() })
  .returns(s.string())
  .body(surql`RETURN "hi " + $name`)
Generated SurrealQL
DEFINE FUNCTION fn::greet($name: string) -> string { RETURN "hi " + $name };

Function with PERMISSIONS and COMMENT

schema.ts TypeScript
defineFunction("tax", { amount: s.float() })
  .returns(s.float())
  .body(surql`RETURN $amount * 0.21`)
  .permissions(surql`$auth.id != NONE`)
  .comment("VAT helper")
Generated SurrealQL
DEFINE FUNCTION fn::tax($amount: float) -> float { RETURN $amount * 0.21 }
  PERMISSIONS $auth.id != NONE
  COMMENT "VAT helper";

access

RECORD access (SIGNUP / SIGNIN / DURATION)

schema.ts TypeScript
defineAccess("user")
  .record()
  .signup(surql`CREATE user SET email = $email, pass = crypto::argon2::generate($pass)`)
  .signin(surql`SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass)`)
  .duration({ token: "1h", session: "12h" })
Generated SurrealQL
DEFINE ACCESS user ON DATABASE
  TYPE RECORD
  SIGNUP { CREATE user SET email = $email, pass = crypto::argon2::generate($pass) }
  SIGNIN { SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) }
  DURATION
    FOR TOKEN 1h,
    FOR SESSION 12h;

RECORD access with AUTHENTICATE

schema.ts TypeScript
defineAccess("api")
  .record()
  .authenticate(surql`RETURN $auth`)
  .duration({ session: "1d" })
Generated SurrealQL
DEFINE ACCESS api ON DATABASE
  TYPE RECORD
  AUTHENTICATE { RETURN $auth }
  DURATION
    FOR SESSION 1d;

JWT access (validate external tokens)

Structure (alg + key/url) applies + introspects, but the signing KEY is redacted on pull.

schema.ts TypeScript
defineAccess("external").jwt({ alg: "HS512", key: "secret" }).onDatabase()
Generated SurrealQL
DEFINE ACCESS external ON DATABASE TYPE JWT ALGORITHM HS512 KEY "secret";

BEARER access (API-key grants)

Subject + duration round-trip; the grant secret is redacted on introspect.

schema.ts TypeScript
defineAccess("apikey").bearer({ for: "user" }).duration({ session: "30d" })
Generated SurrealQL
DEFINE ACCESS apikey ON DATABASE
  TYPE BEARER
    FOR USER
  DURATION
    FOR SESSION 30d;

analyzers

Analyzer with tokenizers + filters

schema.ts TypeScript
defineAnalyzer("english", {
  tokenizers: ["blank", "class"],
  filters: ["lowercase", "snowball(english)"],
})
Generated SurrealQL
DEFINE ANALYZER english TOKENIZERS BLANK, CLASS FILTERS LOWERCASE, SNOWBALL(ENGLISH);

Analyzer with tokenizers only

schema.ts TypeScript
defineAnalyzer("simple", { tokenizers: ["blank"] })
Generated SurrealQL
DEFINE ANALYZER simple TOKENIZERS BLANK;

escape-hatch

.$surreal(wire, codec) — store an instanceof type as a string

App type = Money, wire/DDL type = string; the codec maps both ways. Clears the no-DDL brand.

schema.ts TypeScript
defineTable("wallet", {
  id: s.string(),
  price: s.instanceof(Money).$surreal(s.string(), {
    encode: (m) => m.toString(),
    decode: (v) => new Money(Math.round(Number(v) * 100)),
  }),
})
Generated SurrealQL
DEFINE TABLE wallet TYPE NORMAL SCHEMAFULL;
DEFINE FIELD price ON TABLE wallet TYPE string;

.$surreal on s.custom — store an app-only type (URL) as a string

URL has no native SurrealQL type, so it needs a wire type + codec. (A JS Set, by contrast, is native: use s.set() -> set<T>.)

schema.ts TypeScript
defineTable("site", {
  id: s.string(),
  homepage: s.custom<URL>().$surreal(s.string(), {
    encode: (u) => u.href,
    decode: (v) => new URL(v),
  }),
})
Generated SurrealQL
DEFINE TABLE site TYPE NORMAL SCHEMAFULL;
DEFINE FIELD homepage ON TABLE site TYPE string;

.$internal() — DB-managed, client-hidden field (PERMISSIONS NONE)

schema.ts TypeScript
defineTable("account", { id: s.string(), passhash: s.string().$internal() })
Generated SurrealQL
DEFINE TABLE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD passhash ON TABLE account TYPE string PERMISSIONS NONE;