diff --git a/.optimize-cache.json b/.optimize-cache.json index fe9e872461..0c7716762e 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -179,6 +179,7 @@ "images/blog/announcing-new-push-notifications-features/cover.png": "a0c758cf6c8a95e09a0d2ca562b0775a50d34a4d691d675cda70e44ad21805ac", "images/blog/announcing-opt-in-relationship-loading/cover.png": "e16cc16ea6d968b29af19bcd6274741141584a7efe5e1bb18be19b77c3a380c8", "images/blog/announcing-phone-OTP-pricing/cover.png": "598d55359ca4cb2b46846a8fd76b1f051be7c5f3199b50ffa92a28e84e5f3d67", + "images/blog/announcing-realtime-channel-helpers/cover.png": "cbcffde3edfb77908566ff6361cb31bb1175d64bb1958a038720c52748dfa904", "images/blog/announcing-relationship-queries/cover.png": "7e615c0a9dcbb3949d5fb7ed71f36bb44de40ae67c8cd832b96ff5bbd4b0f451", "images/blog/announcing-screenshots-api/cover.png": "56555006946b9ead5cd4258544b6a9dda44bce6841706749f7539bc31356383e", "images/blog/announcing-spatial-columns/cover.png": "b3e73629df86190fb06b715f4fe24aad473631538c1b3e78ae45cc8c5e7cd7d0", diff --git a/src/routes/blog/post/announcing-realtime-channel-helpers/+page.markdoc b/src/routes/blog/post/announcing-realtime-channel-helpers/+page.markdoc new file mode 100644 index 0000000000..33ba59b3e6 --- /dev/null +++ b/src/routes/blog/post/announcing-realtime-channel-helpers/+page.markdoc @@ -0,0 +1,105 @@ +--- +layout: post +title: "Announcing Realtime Channel helpers: Type-safe subscriptions made simple" +description: Build realtime subscriptions faster with a fluent, chainable API that reduces errors and improves code clarity. +date: 2026-02-13 +cover: /images/blog/announcing-realtime-channel-helpers/cover.png +timeToRead: 5 +author: jake-barnby +category: announcement +featured: false +--- + +If you've built realtime features in your apps, you've likely written channel strings by hand: concatenating IDs, formatting wildcards, and hoping you didn't introduce a typo that would silently break your subscription. While writing channel strings like `databases.*.tables.*.rows.*` works, it's error-prone and harder to maintain as your application grows. + +To make realtime subscriptions clearer and safer, Appwrite is introducing **Channel helpers**: a type-safe, fluent API for building realtime channel subscriptions. + +# Type-safe channels, zero typos + +Channel helpers eliminate the manual work of writing channel strings. Instead of concatenating strings and worrying about syntax errors, you use a chainable API that guides you through building valid channel subscriptions. + +The helper provides IDE autocomplete, catches errors at compile time, and makes your subscription logic self-documenting. No more guessing the correct format or debugging silent subscription failures caused by a misplaced dot or asterisk. + +# How it works + +Channel helpers are available through the `Channel` class in all Appwrite client SDKs. The API is designed to be intuitive and mirrors the structure of your Appwrite resources. + +Here's how you build a subscription to a specific row: + +```javascript +import { Client, Realtime, Channel } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +// Subscribe to a specific row with type-safe helpers +const subscription = await realtime.subscribe( + Channel.tablesdb('').table('').row(''), + response => { + console.log(response); + } +); +``` + +Instead of writing `databases..tables..rows.`, the helper builds the correct string for you while providing autocomplete and validation every step of the way. + +# Flexible and composable + +Channel helpers support the full range of Appwrite's realtime capabilities. You can: + +- **Subscribe to all resources**: Omit IDs to use wildcards automatically +- **Filter by event type**: Chain `.create()`, `.update()`, or `.delete()` to listen only to specific events +- **Build complex subscriptions**: Combine multiple helpers in a single call + +```javascript +// Subscribe to all account events +const subscription = await realtime.subscribe(Channel.account(), response => { + console.log(response); +}); + +// Subscribe to all row updates in a specific table +const rowSubscription = await realtime.subscribe( + Channel.tablesdb('').table('').row().update(), + response => { + console.log('Row updated:', response.payload); + } +); + +// Subscribe to multiple channels at once +const multiSubscription = await realtime.subscribe([ + Channel.tablesdb('').table('').row(''), + Channel.files() +], response => { + console.log(response); +}); +``` + +# Key benefits + +- **Type-safe subscriptions**: Catch errors at compile time instead of runtime +- **IDE autocomplete**: Build channels faster with intelligent suggestions +- **Self-documenting code**: Channel structure is clear and readable +- **Reduced errors**: Eliminate typos and formatting mistakes +- **Consistent API**: Same helper syntax across all client SDKs +- **Backwards compatible**: Existing string-based subscriptions continue to work + +# Available across all platforms + +Channel helpers are available in all Appwrite client SDKs: Web, Flutter, Apple, and Android. Each SDK provides the same fluent API, making it easy to build consistent realtime features across platforms. + +The helpers support all available channels, including: +- Account events +- Database rows +- Storage files +- Team and membership updates +- Function executions + +Existing subscriptions using string channels continue to work, ensuring a smooth transition for current projects. + +# More resources + +- [Read the documentation to get started](/docs/apis/realtime#channel-helpers) +- [Learn about Realtime API events](/docs/advanced/platform/events) diff --git a/src/routes/docs/apis/realtime/+page.markdoc b/src/routes/docs/apis/realtime/+page.markdoc index 86b52ed7d6..9b57188d70 100644 --- a/src/routes/docs/apis/realtime/+page.markdoc +++ b/src/routes/docs/apis/realtime/+page.markdoc @@ -4,7 +4,7 @@ title: Realtime description: Want to build dynamic and interactive applications with real-time data updates? Appwrite Realtime API makes it possible, get started with our intro guide. --- -Appwrite supports multiple protocols for accessing the server, including [REST](/docs/apis/rest), [GraphQL](/docs/apis/graphql), and [Realtime](/docs/apis/realtime). The Appwrite Realtime allows you to listen to any Appwrite events in realtime using the `subscribe` method. +Appwrite supports multiple protocols for accessing the server, including [REST](/docs/apis/rest), [GraphQL](/docs/apis/graphql), and [Realtime](/docs/apis/realtime). The Appwrite Realtime allows you to listen to any Appwrite events in realtime using the `Realtime` service. Instead of requesting new data via HTTP, the subscription will receive new data every time it changes, any connected client receives that update within milliseconds via a WebSocket connection. @@ -12,14 +12,16 @@ This lets you build an interactive and responsive user experience by providing i {% multicode %} ```client-web -import { Client } from "appwrite"; +import { Client, Realtime, Channel } from "appwrite"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject(''); +const realtime = new Realtime(client); + // Subscribe to files channel -client.subscribe('files', response => { +const subscription = await realtime.subscribe(Channel.files(), response => { if(response.events.includes('buckets.*.files.*.create')) { // Log when a new file is uploaded console.log(response.payload); @@ -37,7 +39,7 @@ final client = Client() final realtime = Realtime(client); // Subscribe to files channel -final subscription = realtime.subscribe(['files']); +final subscription = realtime.subscribe([Channel.files()]); subscription.stream.listen((response) { if(response.events.contains('buckets.*.files.*.create')) { @@ -58,7 +60,7 @@ let client = Client() let realtime = Realtime(client) // Subscribe to files channel -let subscription = realtime.subscribe(channels: ["files"]) { response in +let subscription = realtime.subscribe(channels: [Channel.files()]) { response in if (response.events!.contains("buckets.*.files.*.create")) { // Log when a new file is uploaded print(String(describing: response)) @@ -69,6 +71,7 @@ let subscription = realtime.subscribe(channels: ["files"]) { response in ```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Realtime +import io.appwrite.extensions.Channel val client = Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") @@ -77,7 +80,7 @@ val client = Client(context) val realtime = Realtime(client) // Subscribe to files channel -val subscription = realtime.subscribe("files") { +val subscription = realtime.subscribe(Channel.files()) { if(it.events.contains("buckets.*.files.*.create")) { // Log when a new file is uploaded print(it.payload.toString()); @@ -93,6 +96,104 @@ If you subscribe to a channel, you will receive callbacks for a variety of event [View a list of all available events](/docs/advanced/platform/events). +## Channel helpers {% #channel-helpers %} + +Instead of manually writing channel strings, you can use the `Channel` helper class to build type-safe channel subscriptions. The helper provides a fluent API that makes it easier to construct channel strings and reduces errors. + +{% multicode %} +```client-web +import { Client, Realtime, Channel } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +// Subscribe to account channel +const subscription = await realtime.subscribe(Channel.account(), response => { + console.log(response); +}); + +// Subscribe to a specific row +const rowSubscription = await realtime.subscribe( + Channel.tablesdb('').table('').row(''), + response => { + console.log(response); + } +); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final realtime = Realtime(client); + +// Subscribe to account channel +final subscription = realtime.subscribe([Channel.account()]); + +// Subscribe to a specific row +final docSubscription = realtime.subscribe([ + Channel.tablesdb('').table('').row('') +]); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let realtime = Realtime(client) + +// Subscribe to account channel +let subscription = realtime.subscribe(channels: [Channel.account()]) { response in + print(String(describing: response)) +} + +// Subscribe to a specific row +let docSubscription = realtime.subscribe( + channels: [Channel.tablesdb("").table("").row("")] +) { response in + print(String(describing: response)) +} +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Realtime +import io.appwrite.extensions.Channel + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val realtime = Realtime(client) + +// Subscribe to account channel +val subscription = realtime.subscribe(Channel.account()) { + print(it.toString()) +} + +// Subscribe to a specific row +val docSubscription = realtime.subscribe( + Channel.tablesdb("").table("").row("") +) { + print(it.toString()) +} +``` + +{% /multicode %} + +The `Channel` helper supports all available channels and allows you to: +- Build channels with a fluent, chainable API +- Optionally specify resource IDs (omit IDs to subscribe to all resources) +- Add event filters like `.create()`, `.update()`, or `.delete()` {% info title="Permissions" %} All subscriptions are secured by the [permissions system](/docs/advanced/platform/permissions) offered by Appwrite, meaning a user will only receive updates to resources they have permission to access. @@ -114,13 +215,15 @@ In this example we are subscribing to all updates related to our account by usin {% multicode %} ```client-web -import { Client } from "appwrite"; +import { Client, Realtime, Channel } from "appwrite"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject(''); -client.subscribe('account', response => { +const realtime = new Realtime(client); + +const subscription = await realtime.subscribe(Channel.account(), response => { // Callback will be executed on all account events. console.log(response); }); @@ -135,7 +238,7 @@ final client = Client() final realtime = Realtime(client); -final subscription = realtime.subscribe(['account']); +final subscription = realtime.subscribe([Channel.account()]); subscription.stream.listen((response) { // Callback will be executed on all account events. @@ -153,7 +256,7 @@ let client = Client() let realtime = Realtime(client) -let subscription = realtime.subscribe(channel: "account") { response in +let subscription = realtime.subscribe(channels: [Channel.account()]) { response in // Callback will be executed on all account events. print(String(describing: response)) } @@ -162,6 +265,7 @@ let subscription = realtime.subscribe(channel: "account") { response in ```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Realtime +import io.appwrite.extensions.Channel val client = Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") @@ -169,7 +273,7 @@ val client = Client(context) val realtime = Realtime(client) -val subscription = realtime.subscribe("account") { +val subscription = realtime.subscribe(Channel.account()) { // Callback will be executed on all account events. print(it.payload.toString()) } @@ -181,18 +285,23 @@ val subscription = realtime.subscribe("account") { You can also listen to multiple channels at once by passing an array of channels. This will trigger the callback for any events for all channels passed. -In this example we are listening to a specific row and all files by subscribing to the `databases..tables..rows.` and `files` channels. +In this example we are listening to a specific row and all files by subscribing to `Channel.tablesdb("").table("").row("")` and `Channel.files()` channels. {% multicode %} ```client-web -import { Client } from "appwrite"; +import { Client, Realtime, Channel } from "appwrite"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject(''); -client.subscribe(['databases..tables..rows.', 'files'], response => { - // Callback will be executed on changes for the specific row and all files. +const realtime = new Realtime(client); + +const subscription = await realtime.subscribe([ + Channel.tablesdb('').table('').row(''), + Channel.files() +], response => { + // Callback will be executed on changes for the row and all files. console.log(response); }); ``` @@ -206,10 +315,13 @@ final client = Client() final realtime = Realtime(client); -final subscription = realtime.subscribe(['databases..tables..rows.', 'files']); +final subscription = realtime.subscribe([ + Channel.tablesdb('').table('').row(''), + Channel.files() +]); subscription.stream.listen((response) { - // Callback will be executed on changes for the specific row and all files. + // Callback will be executed on changes for the row and all files. print(response); }) ``` @@ -224,8 +336,11 @@ let client = Client() let realtime = Realtime(client) -realtime.subscribe(channels: ["databases..tables..rows.", "files"]) { response in - // Callback will be executed on changes for the specific row and all files. +realtime.subscribe(channels: [ + Channel.tablesdb("").table("").row(""), + Channel.files() +]) { response in + // Callback will be executed on changes for the row and all files. print(String(describing: response)) } ``` @@ -233,14 +348,18 @@ realtime.subscribe(channels: ["databases..tables..rows..cloud.appwrite.io/v1") .setProject("") val realtime = Realtime(client) -realtime.subscribe(listOf("databases..tables..rows.", "files")) { - // Callback will be executed on changes for the specific row and all files. +realtime.subscribe( + Channel.tablesdb("").table("").row(""), + Channel.files() +) { + // Callback will be executed on changes for the row and all files. print(it.toString()) } ``` @@ -253,19 +372,21 @@ If you no longer want to receive updates from a subscription, you can unsubscrib {% multicode %} ```client-web -import { Client } from "appwrite"; +import { Client, Realtime, Channel } from "appwrite"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject(''); -const unsubscribe = client.subscribe('files', response => { +const realtime = new Realtime(client); + +const subscription = await realtime.subscribe(Channel.files(), response => { // Callback will be executed on changes for all files. console.log(response); }); // Closes the subscription. -unsubscribe(); +await subscription.close(); ``` ```client-flutter @@ -277,7 +398,7 @@ final client = Client() final realtime = Realtime(client); -final subscription = realtime.subscribe(['files']); +final subscription = realtime.subscribe([Channel.files()]); subscription.stream.listen((response) { // Callback will be executed on changes for all files. @@ -294,7 +415,7 @@ import Appwrite let client = Client() let realtime = Realtime(client) -let subscription = realtime.subscribe(channel: "files") { response in +let subscription = realtime.subscribe(channels: [Channel.files()]) { response in // Callback will be executed on changes for all files. print(response.toString()) } @@ -306,6 +427,7 @@ subscription.close() ```client-android-kotlin import io.appwrite.Client import io.appwrite.services.Realtime +import io.appwrite.extensions.Channel val client = Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") @@ -313,7 +435,7 @@ val client = Client(context) val realtime = Realtime(client) -val subscription = realtime.subscribe("files") { +val subscription = realtime.subscribe(Channel.files()) { // Callback will be executed on changes for all files. print(it.toString()) } @@ -412,89 +534,78 @@ The response will look like this: # Channels {% #channels %} -A list of channels you can subscribe to. Replace `` with your resource ID or use `*` for wildcards. - -## Account {% #account %} +A list of all channels available you can subscribe to. When using `Channel` helpers, leaving an ID blank will subscribe using `*`. {% table %} * Channel +* Channel Helper * Description --- * `account` +* `Channel.account()` * All account related events (session create, name update...) - -{% /table %} - -## Databases {% #databases %} - -{% table %} -* Channel -* Description +--- +* `tablesdb..tables..rows` +* `Channel.tablesdb('').table('').row()` +* Any create/update/delete events to any row in a table --- * `rows` +* `Channel.rows()` * Any create/update/delete events to any row --- -* `databases..tables..rows` -* Any create/update/delete events to any row in a table ---- -* `databases..tables..rows.` -* Any create/update/delete events to a given row -{% /table %} - -## Storage {% #storage %} - -{% table %} -* Channel -* Description +* `tablesdb..tables..rows.` +* `Channel.tablesdb('').table('').row('')` +* Any update/delete events to a given row --- * `files` +* `Channel.files()` * Any create/update/delete events to any file --- -* `buckets..files` -* Any create/update/delete events to any file of the given bucket ---- * `buckets..files.` -* Any create/update/delete events to a given file of the given bucket - -{% /table %} - -## Functions {% #functions %} - -{% table %} -* Channel -* Description +* `Channel.bucket('').file('')` +* Any update/delete events to a given file of the given bucket --- -* `executions` -* Any execution event ---- -* `executions.` -* Any execution event to a given execution ---- -* `functions.` -* Any execution event to a given function - -{% /table %} - -## Teams & Memberships {% #teams %} - -{% table %} -* Channel -* Description +* `buckets..files` +* `Channel.bucket('').file()` +* Any update/delete events to any file of the given bucket --- -* `teams` +* `teams.*` +* `Channel.teams()` * Any create/update/delete events to any team --- * `teams.` -* Any create/update/delete events to a given team +* `Channel.team('')` +* Any update/delete events to a given team --- * `memberships` +* `Channel.memberships()` * Any create/update/delete events to any membership --- * `memberships.` -* Any create/update/delete events to a given membership +* `Channel.membership('')` +* Any update/delete events to a given membership +--- +* `executions` +* `Channel.executions()` +* Any update to executions +--- +* `executions.` +* `Channel.execution(ID)` +* Any update to a given execution +--- +* `functions.` +* `Channel.function('')` +* Any execution event to a given function {% /table %} +You can also filter events by appending event methods to the channel helpers: +- `.create()` - Listen only to create events +- `.update()` - Listen only to update events +- `.delete()` - Listen only to delete events + +For example, `Channel.tablesdb('').table('').row('').update()` will only trigger on row updates. + # Custom endpoint {% #custom-endpoint %} The SDK will guess the endpoint of the Realtime API when setting the endpoint of your Appwrite instance. If you are running Appwrite with a custom proxy and changed the route of the Realtime API, you can call the `setEndpointRealtime` method on the Client SDK and set your new endpoint value. diff --git a/static/images/blog/announcing-realtime-channel-helpers/cover.png b/static/images/blog/announcing-realtime-channel-helpers/cover.png new file mode 100644 index 0000000000..f3265c740e Binary files /dev/null and b/static/images/blog/announcing-realtime-channel-helpers/cover.png differ