Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/routes/blog/post/announcing-realtime-queries/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
layout: post
title: "Introducing Realtime queries: Server-side event filtering for subscriptions"
description: Pass SDK queries when subscribing to realtime channels to automatically filter events server-side, so your callbacks only receive the updates you care about.
date: 2026-02-16
cover: /images/blog/announcing-realtime-channel-helpers/cover.png
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cover image path references a different blog post.

The cover path /images/blog/announcing-realtime-channel-helpers/cover.png points to the channel-helpers announcement, not this realtime-queries post. If this is intentional reuse, consider adding a comment. Otherwise, update the path to a dedicated cover image.

🤖 Prompt for AI Agents
In `@src/routes/blog/post/announcing-realtime-queries/`+page.markdoc at line 6,
The frontmatter cover image path currently points to the other post's asset
(`/images/blog/announcing-realtime-channel-helpers/cover.png`); update the cover
field in the +page.markdoc frontmatter to reference the correct image for this
realtime-queries post (or, if intentionally reusing that image, add an inline
comment above the cover line explaining reuse and why it’s shared) so the cover
is accurate and clear.

timeToRead: 4
author: jake-barnby
category: announcement
featured: false
---

If you've built realtime features with Appwrite, you've likely written filtering logic inside your subscription callbacks: checking payload fields, comparing values, and discarding events you don't need. While this works, it adds boilerplate to your client code and means you're still receiving and processing every event on the channel, even the ones you'll throw away.

To make realtime subscriptions more precise, Appwrite now supports **Realtime queries**: pass SDK queries when subscribing to automatically filter events server-side.

# Filter at the source, not in your callback

Realtime queries let you pass SDK queries as a parameter when subscribing to a channel. Events are filtered on the server based on your queries, so your callback only fires when the payload matches your conditions.

This means less client-side filtering logic, fewer unnecessary callback invocations, and a cleaner subscription model overall.

# How it works

Realtime queries use the same `Query` helpers you already use with Appwrite's database and other services. Pass an array of queries when subscribing, and only events matching those conditions will trigger your callback.

```javascript
import { Client, Realtime, Channel, Query } from "appwrite";

const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>');

const realtime = new Realtime(client);

// Subscribe to all updates on the channel
const allVotes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
}
);

// Subscribe only to updates where person equals 'person1'
const person1Votes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
},
[Query.equal('person', ['person1'])]
);

// Subscribe only to updates where person is not 'person1'
const otherVotes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
},
[Query.notEqual('person', 'person1')]
);
```

Without queries, the first subscription receives every event on the channel. With queries, the second and third subscriptions only receive events where the payload matches the specified conditions. No manual filtering required.

# Supported queries

Realtime queries support a subset of the full SDK query methods, focused on value comparison and logical composition:

- **Comparison**: `Query.equal()`, `Query.notEqual()`, `Query.greaterThan()`, `Query.greaterThanEqual()`, `Query.lessThan()`, `Query.lessThanEqual()`
- **Null checks**: `Query.isNull()`, `Query.isNotNull()`
- **Logical**: `Query.and()`, `Query.or()`

These cover the most common filtering patterns for realtime events. You can combine multiple queries to build precise conditions for your subscriptions.

# Key benefits

- **Server-side filtering**: Events are filtered before reaching your client, reducing unnecessary processing
- **Consistent API**: Uses the same `Query` helpers from Appwrite's database APIs
- **Cleaner code**: Eliminate manual filtering logic inside subscription callbacks
- **Available across all platforms**: Supported in Web, Flutter, Apple, and Android client SDKs

# More resources

- [Read the Realtime queries documentation](/docs/apis/realtime#queries)
- [Learn about Realtime channel helpers](/docs/apis/realtime#channel-helpers)
- [View all available Realtime events](/docs/advanced/platform/events)
174 changes: 174 additions & 0 deletions src/routes/docs/apis/realtime/+page.markdoc
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,180 @@ subscription.close()

{% /multicode %}

# Queries {% #queries %}

You can filter realtime events by passing queries as a third parameter when subscribing. Events are filtered server-side based on your queries, so your callback only receives updates that match your conditions. This allows you to use familiar SDK queries like `Query.equal` to automatically filter events instead of filtering manually in your callback.

{% multicode %}
```client-web
import { Client, Realtime, Channel, Query } from "appwrite";

const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>');

const realtime = new Realtime(client);

// Subscribe to all updates
const allVotes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
}
);

// Subscribe to updates where person equals 'person1'
const person1Votes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
},
[Query.equal('person', ['person1'])]
);

// Subscribe to updates where person is not 'person1'
const otherVotes = await realtime.subscribe(
Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row(),
response => {
console.log(response.payload);
},
[Query.notEqual('person', 'person1')]
);
```

```client-flutter
import 'package:appwrite/appwrite.dart';

final client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>');

final realtime = Realtime(client);

// Subscribe to all updates
final allVotes = realtime.subscribe(
[Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row()]
);

allVotes.stream.listen((response) {
print(response.payload);
});

// Subscribe to updates where person equals 'person1'
final person1Votes = realtime.subscribe(
[Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row()],
queries: [Query.equal('person', ['person1'])]
);

person1Votes.stream.listen((response) {
print(response.payload);
});

// Subscribe to updates where person is not 'person1'
final otherVotes = realtime.subscribe(
[Channel.tablesdb('<DATABASE_ID>').table('<TABLE_ID>').row()],
queries: [Query.notEqual('person', 'person1')]
);

otherVotes.stream.listen((response) {
print(response.payload);
});
```

```client-apple
import Appwrite
import AppwriteModels

let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")

let realtime = Realtime(client)

// Subscribe to all updates
let allVotes = realtime.subscribe(
channels: [Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row()]
) { response in
print(String(describing: response.payload))
}

// Subscribe to updates where person equals 'person1'
let person1Votes = realtime.subscribe(
channels: [Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row()],
callback: { response in
print(String(describing: response.payload))
},
queries: [Query.equal("person", value: ["person1"])]
)

// Subscribe to updates where person is not 'person1'
let otherVotes = realtime.subscribe(
channels: [Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row()],
callback: { response in
print(String(describing: response.payload))
},
queries: [Query.notEqual("person", value: "person1")]
)
```

```client-android-kotlin
import io.appwrite.Client
import io.appwrite.Query
import io.appwrite.services.Realtime
import io.appwrite.extensions.Channel

val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")

val realtime = Realtime(client)

// Subscribe to all updates
val allVotes = realtime.subscribe(
Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row()
) {
print(it.payload.toString())
}

// Subscribe to updates where person equals 'person1'
val person1Votes = realtime.subscribe(
Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row(),
payloadType = Any::class.java,
queries = setOf(Query.equal("person", listOf("person1")))
) {
print(it.payload.toString())
}

// Subscribe to updates where person is not 'person1'
val otherVotes = realtime.subscribe(
Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row(),
payloadType = Any::class.java,
queries = setOf(Query.notEqual("person", "person1"))
) {
print(it.payload.toString())
}
```
Comment on lines +565 to +601
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Appwrite Kotlin SDK Realtime subscribe method signature queries parameter documentation

💡 Result:

In the Appwrite Android (Kotlin) SDK, Realtime.subscribe has overloads that accept a queries parameter with this signature (simplified to the relevant one):

fun <T> subscribe(
    vararg channels: String,
    payloadType: Class<T>,
    queries: Set<String> = emptySet(),
    callback: (RealtimeResponseEvent<T>) -> Unit
): RealtimeSubscription

Source (SDK implementation): io.appwrite.services.Realtime in sdk-for-android repo. [1]

What queries is

  • queries is a set of query strings (the same query-string format produced by Appwrite’s Query helpers, e.g. Query.select(...), Query.equal(...), etc.). Appwrite documents queries as “passed … as an array of query strings, which can be generated using the Query class.” [2]
  • If you pass no queries (emptySet()), the SDK defaults to selecting all attributes by sending Query.select(listOf("*")). [1]

What it affects for Realtime

Realtime subscriptions are still based on channels (per the Realtime docs). [3] In the Android SDK, the queries you pass are additionally sent as URL parameters per-channel/per-subscription-slot (implementation detail), and are used to shape/filter what the server returns for that subscription. [1][3]

Example (select only specific fields in the realtime payload):

realtime.subscribe(
  "databases.$db.collections.$col.documents",
  payloadType = Document::class.java,
  queries = setOf(Query.select(listOf("\$id", "title")).toString())
) { event -> /* ... */ }

(Uses Query.select as documented.) [2]

Sources: [1] [2] [3]


Add .toString() to Query objects in the queries parameter.

The queries parameter expects a Set<String>, not Query objects. Convert each Query using .toString():

Example fix
queries = setOf(Query.equal("person", listOf("person1")).toString())

instead of:

queries = setOf(Query.equal("person", listOf("person1")))

Apply this to all three subscribe calls (lines 580, 587, and 594).

🤖 Prompt for AI Agents
In `@src/routes/docs/apis/realtime/`+page.markdoc around lines 565 - 601, The
subscribe calls are passing Query objects into the queries parameter which
expects a Set<String>; update each Realtime.subscribe invocation (the three
usages where Channel.tablesdb(...).table(...).row() is used) to convert
Query.equal(...) and Query.notEqual(...) to strings by calling .toString() so
queries is a Set<String> (e.g., replace Query.equal("person", listOf("person1"))
with Query.equal(...).toString()), ensuring payloadType/onSubscribe semantics
remain unchanged.


{% /multicode %}

## Supported queries {% #supported-queries %}

The following query methods are supported for realtime filtering:

{% table %}
* Category
* Queries
---
* Comparison
* `Query.equal()`, `Query.notEqual()`, `Query.greaterThan()`, `Query.greaterThanEqual()`, `Query.lessThan()`, `Query.lessThanEqual()`
---
* Null checks
* `Query.isNull()`, `Query.isNotNull()`
---
* Logical
* `Query.and()`, `Query.or()`
{% /table %}

# Payload {% #payload %}

The payload from the subscription will contain following properties:
Expand Down