db.live() opens a LIVE SELECT subscription and resolves to a typed LiveSubscription. Each notification carries the action. A change also carries the record id and the record, parsed against your schema.
Live queries need a WebSocket connection. Over HTTP, db.live() rejects with the SDK's UnsupportedFeatureError. The table must also exist before you subscribe, so define it first, for example with DEFINE TABLE. Subscribing to a table that is not defined rejects with the SDK's NotFoundError.
The examples on this page use the tables below, with db created as described in connections.
import { orm, t, table } from "surqlize";
const user = table("user", {
name: t.object({ first: t.string(), last: t.string() }),
email: t.string(),
age: t.number(),
});
const post = table("post", {
title: t.string(),
content: t.string(),
author: t.record("user"),
});
const db = orm(surreal, user, post);DEFINE TABLE user SCHEMALESS;
DEFINE TABLE post SCHEMALESS;Subscribing
db.live() takes a table name. Awaiting it starts the subscription.
const sub = await db.live("user");
// Handle notifications. The return value stops this handler.
const off = sub.subscribe((msg) => {
// msg.action: "CREATE" | "UPDATE" | "DELETE" | "KILLED"
// msg.recordId: the affected RecordId
// msg.value: the affected record, parsed against the schema
if (msg.action === "KILLED") return;
console.log(msg.action, msg.value);
});
// Stop receiving notifications on this handler
off();
// Or end the subscription
await sub.kill();The value of a notification is typed from the table, once you have ruled out KILLED:
const typed = await db.live("user");
typed.subscribe((msg) => {
if (msg.action === "KILLED") return;
msg.value.name.first; // string
msg.value.age; // number
});Subscribing to one record
Pass a RecordId to db.live() to follow one record. The subscription selects the table and filters on the record's id, so it only delivers changes to that record. The two-argument form, db.live(table, id), does the same.
import { RecordId } from "surrealdb";
const byRecordId = await db.live(new RecordId("user", "ada"));
const byTableAndId = await db.live("user", "ada");A .where() on a record subscription applies alongside the id, so a change must match both.
Handling KILLED
KILLED means the server ended the subscription, for example because its table was removed. It is the last message a subscription emits, and it has no record, so recordId and value exist only after you rule it out. LiveMessage is a union, so the compiler enforces this.
sub.isAlive becomes false when a subscription ends. That happens after kill(), after the server ends it, or after a signal given to db.withSignal() aborts.
After a subscription ends
Calling subscribe() on a subscription that is no longer alive does nothing. It returns a function, and calling that function does nothing as well. The handler is never called, and no error is raised. A for await loop over such a subscription ends at once.
Errors are reported as follows. While a subscription is alive, an error from its stream, or an error thrown by a handler, is rethrown from a microtask. It is then an uncaught exception, not a rejected promise that you can catch. Errors from a stream that has already ended, or that you have unsubscribed from, are ignored.
Async iteration
A subscription is async-iterable, so you can read it with for await. The loop ends after the KILLED message, and when you call kill().
const sub = await db.live("user");
// Call kill() from elsewhere, such as a shutdown handler. This example stops after 200 ms.
setTimeout(() => void sub.kill(), 200);
for await (const msg of sub) {
if (msg.action === "KILLED") break;
console.log(msg.action, msg.value);
}Subscribing in one step
Calling .subscribe(handler) on the builder starts the query and returns a function that both removes the handler and kills the subscription.
const stop = await db
.live("user")
.where((user) => user.age.gte(18))
.subscribe((msg) => {
if (msg.action !== "KILLED") console.log(msg.action, msg.value);
});
// later
stop();Filtering, projecting and fetching
A live query takes .where(), .return() and .fetch(), as select does. .where() accepts both fluent and object filters, and chained calls combine with AND. clearWhere() removes every .where() condition set so far. A live query cannot use LIMIT, ORDER BY, START, GROUP or SPLIT, so it has no methods for them.
// Only notify for adult users
const adults = await db.live("user").where((user) => user.age.gte(18));
// Project a subset of fields
const names = await db.live("user").return((user) => ({
name: user.name,
email: user.email,
}));
// Resolve record links in notifications
const posts = await db.live("post").fetch("author");Filtering or projecting a live query relies on query parameters, which need SurrealDB 3.0 or later.
Receiving diffs
.diff() delivers JSON Patch operations instead of full records, which renders LIVE SELECT DIFF. The diff replaces the projection, so do not combine .diff() with .return().
const sub = await db.live("user").diff();
sub.subscribe((msg) => {
// msg.value is an array of JSON Patch operations
});Reconnecting
A subscription to a table, with any of .where(), .fetch() and .diff(), is managed by the SDK. When the connection drops and returns, the SDK registers the live query again and notifications keep arriving. The id of the subscription changes when that happens.
A subscription with a .return() projection, and a subscription to a record id, cannot be expressed that way, so they are unmanaged. They stop delivering after a reconnect, and isAlive stays true. Check sub.isManaged where this matters, or subscribe to the whole record and project it on the client.
import { RecordId } from "surrealdb";
const adults = await db.live("user").where((user) => user.age.gte(18));
adults.isManaged; // true
const names = await db.live("user").return((user) => ({ name: user.name }));
names.isManaged; // false
const one = await db.live(new RecordId("user", "ada"));
one.isManaged; // falseFor the SDK's own live query API, see live queries in the SDK documentation.
Ending subscriptions with a request
A subscription made through db.withSignal(signal) is killed when the signal aborts. See cancellation, timeouts, and retries.