> Full SurrealDB documentation index: https://surrealdb.com/docs/llms.txt

# Surqlize live queries

Subscribe to real-time changes with db.live() in Surqlize, which compiles to LIVE SELECT and delivers typed CREATE, UPDATE, DELETE and KILLED notifications, with where() filters, return() projections, fetch(), diff(), async iteration and reconnect behaviour.

`db.live()` opens a [`LIVE SELECT`](/docs/reference/query-language/statements/live-select.md) 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.

> [!NOTE]
> 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](/docs/libraries/surqlize-typescript/connections.md).

```ts title="Example schema"
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);
```

```surql title="Define the tables"
DEFINE TABLE user SCHEMALESS;
DEFINE TABLE post SCHEMALESS;
```

## Subscribing

`db.live()` takes a table name. Awaiting it starts the subscription.

```ts title="Subscribe to a table"
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`:

```ts title="Typed notifications"
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.

```ts title="Follow one record"
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()`.

```ts title="Read notifications in a loop"
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.

```ts title="Subscribe and stop"
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](/docs/libraries/surqlize-typescript/querying.md), 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.

```ts title="Filter, project and fetch"
// 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");
```

> [!NOTE]
> Filtering or projecting a live query relies on query parameters, which need SurrealDB 3.0 or later.

## Receiving diffs

`.diff()` delivers [JSON Patch](https://jsonpatch.com) operations instead of full records, which renders `LIVE SELECT DIFF`. The diff replaces the projection, so do not combine `.diff()` with `.return()`.

```ts title="Subscribe to diffs"
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.

```ts title="Check whether a subscription is managed"
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; // false
```

For the SDK's own live query API, see [live queries](/docs/reference/javascript/concepts/live-queries.md) 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](/docs/libraries/surqlize-typescript/request-options.md).

## Next steps

- [Cancellation, timeouts, and retries](/docs/libraries/surqlize-typescript/request-options.md) - Tie queries and subscriptions to the lifetime of a request.

- [Querying](/docs/libraries/surqlize-typescript/querying.md) - Filters and projections work the same way as in a select.
