From 466c2da096dae0100744057f74b28ac755c2fc5f Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Wed, 26 Nov 2025 16:02:27 +0000 Subject: [PATCH 01/33] docs: add guide for building a scalable notifications center using Ably --- src/data/nav/pubsub.ts | 9 + .../guides/pub-sub/notifications-center.mdx | 513 ++++++++++++++++++ 2 files changed, 522 insertions(+) create mode 100644 src/pages/docs/guides/pub-sub/notifications-center.mdx diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index e3ac85b0aa..053214728f 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -511,6 +511,15 @@ export default { }, ], }, + { + name: 'Guides', + pages: [ + { + name: 'Notifications center', + link: '/docs/guides/pub-sub/notifications-center', + }, + ], + }, { name: 'REST API', link: '/docs/api/rest-api', diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx new file mode 100644 index 0000000000..b1fa4a90f8 --- /dev/null +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -0,0 +1,513 @@ +--- +title: "Guide: Building a notification center at scale with Ably" +meta_description: "Architecting a scalable notification center with Ably: outbox/inbox pattern, authentication, integrations, and cost optimization." +meta_keywords: "notifications, notification center, push, push-notifications, inbox, outbox, pub/sub, scalability, Ably, realtime messaging, authentication, integrations, cost optimization" +--- + +Ably provides the infrastructure to build a robust, scalable notification center that can handle everything from individual user notifications to system-wide broadcasts. Whether you're building friend requests for a social platform, order updates for e-commerce, or alerts for a gaming application, Ably's platform enables you to deliver notifications reliably at any scale. + +Building with Ably means you can focus on your application logic while Ably handles the complexities of realtime delivery, connection management, and global distribution. This guide explains how to architect a notification center using the outbox/inbox pattern, with a focus on security, scalability, and cost optimization. + +## Why Ably for notification centers? + +Ably is trusted by organizations delivering notifications to millions of users in realtime. Its platform is engineered around the four pillars of dependability: + +* **[Performance](/docs/platform/architecture/performance):** Ultra-low latency messaging ensures notifications reach users instantly, even at global scale. +* **[Integrity](/docs/platform/architecture/message-ordering):** Guaranteed message ordering and delivery, with no duplicates or data loss. +* **[Reliability](/docs/platform/architecture/fault-tolerance):** 99.999% uptime SLA, with automatic failover and seamless re-connection. +* **[Availability](/docs/platform/architecture/edge-network):** Global edge infrastructure ensures users connect to the closest point for optimal experience. + +![Ably Architecture Overview Diagram](../../../../images/content/diagrams/architecture-overview.png) + +Delivering notifications in realtime is critical for user engagement. Ably's [serverless architecture](/docs/platform/architecture) eliminates the need to manage websocket servers. It automatically scales to handle millions of concurrent connections without provisioning or maintenance, while handling all edge-cases around delivery, failover, and scaling. + +For notifications that need to reach users even when they're offline, Ably integrates seamlessly with push notification services like [Apple Push Notification Service (APNS) and Firebase Cloud Messaging (FCM)](/docs/push), ensuring your users never miss important updates. + +## Architecting your notification center: The outbox/inbox pattern + +The outbox/inbox pattern is a proven architecture for building scalable notification systems. It provides clear separation of concerns, simplified authentication, and flexible processing workflows. + +### The pattern + +The architecture consists of three main components: + +* **Outbox channel:** Clients publish notification requests to an outbox channel. This could be a friend request, an alert, or any other client-initiated action. +* **Processing pipeline:** An integration (Lambda function, webhook, or queue consumer) receives the outbox message, validates it, applies business logic, and determines the target recipients. +* **Inbox channels:** After processing, notifications are published to client-specific inbox channels (e.g., `inbox:clientId`), where recipients are subscribed. + +[/TODO/]: # (Should add some diagram here showing the flow from user -> outbox -> processing -> inbox -> recipient tp break up the text a bit) + +### Key benefits + +This pattern provides several advantages: + +* **Security:** Clients have publish-only access to the outbox and subscribe-only access to their own inbox. This prevents clients from accessing notifications they shouldn't see, or bypassing processing logic. +* **Flexibility:** The processing pipeline can implement any business logic - validation, rate limiting, enrichment, filtering, or integration with other services. +* **Scalability:** Each component scales independently. Ably handles the channels, your processing pipeline scales based on load, and inboxes grow with your client base. +* **Auditability:** All notification requests pass through your processing pipeline, enabling logging, analytics, and compliance tracking. + +### Channel structure + +When designing your notification center, consider the following channel structure: + + +```javascript +// User publishes to a shared outbox +const outboxChannel = 'notifications:outbox'; + +// Processing pipeline publishes to client-specific inboxes +const inboxChannel = 'notifications:inbox:clientId123'; + +// Optional: general broadcast channel for system-wide notifications +const generalChannel = 'notifications:inbox:general'; +``` + + +The outbox can be a single channel, or any numbers of channels to distribute load in high throughput scenarios. +Inboxes are client-specific, one per client. +The general channel is optional and used for notifications that should reach all clients. + +#### Scaling outbox channels + +Each Ably account has [channel limits](/docs/platform/pricing/limits#channels) that govern the allowed rate of publish. +If you have high throughput with many clients publishing to the same outbox simultaneously, +you may need to split the outbox into multiple channels to avoid overwhelming a single channel and being rate limited. +For example, with 100,000 clinets all publishing notifications, a single outbox could become a bottleneck. + +How many outbox channels you need depends on your expected traffic and account limits. +You can shard outbox channels based on some meaningful dimension, such as: + +* **User group:** `notifications:outbox:group1`, `notifications:outbox:group2`, etc. + +Multiple outbox channels can be routed through one or more integrations using a namespace filter. For example, the filter `^notifications:outbox` matches all channels starting with `notifications:outbox`, allowing your Lambda or webhook to process messages from all outbox channels. + +### Deciding between individual inboxes and a general channel + +For notifications that need to reach all clients, you have two architectural options: + +**Option 1: Individual inboxes** +Iterate through the list of target clients and publish to each clients's inbox channel individually. Ably's [batch publish REST endpoint](/docs/api/rest-api#batch-publish) makes this efficient, allowing you to publish to multiple channels in a single HTTP request. + + +**Option 2: General broadcast channel** +Create a shared channel where all clients subscribe. +Notifications published to this channel reach all subscribers. + +The decision comes down to cost and frequency: + +* **Individual inboxes:** Best when broadcast notifications are infrequent. You pay per message published and per user receiving it. +* **General channel:** Best when broadcast notifications are frequent. You pay per channel attachment per minute, but save on message costs since you publish once regardless of the number of subscribers. + +See the [cost optimization section](#cost-optimization) for detailed calculations to help you decide. + +## Authentication: Securing your notification center + +Authentication is critical in a notification center. +You need to ensure that clients can only publish to an outbox and only receive notifications intended for them. + +### Token-based authentication + +Ably's [token authentication](/docs/auth/token) with JSON Web Tokens (JWT) provides the flexibility to implement fine-grained access control. Tokens are short-lived, can be easily revoked, and include [capabilities](/docs/auth/capabilities) that define what actions a client can perform. + +For a notification center, you typically need two types of tokens: + +* **Outbox tokens:** Grant publish-only access to the outbox channel(s). +* **Inbox tokens:** Grant subscribe-only access to a user's specific inbox channel. + +### Creating an outbox token + +The following example shows how to generate a JWT that allows a user to publish to the outbox: + + +```javascript +const jwt = require("jsonwebtoken"); + +const header = { + "typ": "JWT", + "alg": "HS256", + "kid": "{{ API_KEY_NAME }}" +} + +const currentTime = Math.round(Date.now() / 1000); + +// Allow publish-only access to the outbox channel +const claims = { + "iat": currentTime, + "exp": currentTime + 3600, // Token expires in 1 hour + "x-ably-capability": JSON.stringify({ + "notifications:outbox": ["publish"] + }), + "x-ably-clientId": "client123" // Identify the client +} + +const token = jwt.sign( + claims, + "{{ API_KEY_SECRET }}", + { header: header } +); + +console.log('Outbox JWT:', token); +``` + + +### Creating an inbox token + +The following example shows how to generate a JWT that allows a client to subscribe to their inbox: + + +```javascript +const jwt = require("jsonwebtoken"); + +const header = { + "typ": "JWT", + "alg": "HS256", + "kid": "{{ API_KEY_NAME }}" +} + +const currentTime = Math.round(Date.now() / 1000); + +// Allow subscribe-only access to the client's inbox +const claims = { + "iat": currentTime, + "exp": currentTime + 3600, // Token expires in 1 hour + "x-ably-capability": JSON.stringify({ + "inbox:user123": ["subscribe", "history"] + }), + "x-ably-clientId": "client123" +} + +const token = jwt.sign( + claims, + "{{ API_KEY_SECRET }}", + { header: header } +); + +console.log('Inbox JWT:', token); +``` + + +**History capability:** +Inbox tokens can also include `history` capability to allow clients to retrieve notifications they might have missed while offline. +Message persistence must be enabled to use history - see [message history documentation](/docs/channels/history) for details. + +### Best practices + +* **Use short-lived tokens:** Set token expiry to 1-4 hours to limit exposure if a token is compromised. Ably's SDKs automatically handle token renewal. +* **Tie tokens to clientId:** Always include a `clientId` in your tokens to identify the client and enable auditing. You can also setup a rule to prevent anonymous connections. +* **Implement token refresh:** Use [`authUrl`](/docs/auth/token#auth-url) or [`authCallback`](/docs/auth/token#auth-callback) to automatically refresh expiring tokens. +* **Validate on the server:** Never trust client-provided data. Your processing pipeline should validate all notification request data. +* **Custom Rate Limits:** It is possible to apply custom rate limits to tokens using Ably's [rate limiting feature](docs/auth/capabilities#jwt-limits). This is recommended for outbox tokens to help prevent abuse. Standard connection rate limits still apply based on your account plan. + +## Integration: Processing notifications + +The processing pipeline is where your business logic lives. It receives notifications from the outbox, validates them, applies transformations, and publishes to the appropriate inbox channels. + +Ably provides multiple integration options: + +* **[Webhooks](/docs/platform/integrations/webhooks):** HTTP endpoints (including AWS Lambda, Azure Functions, Google Cloud Functions) +* **[Streaming](/docs/platform/integrations/streaming):** Kafka, Kinesis, SQS, AMQP +* **[Queues](/docs/platform/integrations/queues):** Ably-managed queues for fault-tolerant processing + +### Choosing your integration approach + +The right integration depends on your throughput, ordering requirements, and infrastructure: + +**AWS Lambda / Serverless Functions:** +* **Best for:** Low to high throughput with automatic scaling (up to account limits) +* **Pros:** Scales automatically, no server management, pay per execution +* **Cons:** Possible to quickly overspend without careful monitoring and appropriate limits, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +* **Recommended when:** You want simplicity and automatic scaling + +**Webhooks to your own servers:** +* **Best for:** Custom infrastructure or specific processing requirements +* **Pros:** Full control over processing logic and infrastructure +* **Cons:** You manage scaling and reliability, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +* **Recommended when:** You have existing infrastructure or need specific processing capabilities + +**Ably Queues:** +* **Best for:** Strong ordering guarantees and fault-tolerant processing +* **Pros:** Guaranteed ordering, at-least-once delivery, fault-tolerant +* **Cons:** Non-enterprise accounts limited to 200 msg/s per account, [concurrency limits](/docs/platform/pricing/limits#integrations) apply +* **Recommended when:** Message ordering is critical and throughput is within limits + +Enterprise customers can scale Ably Queues to millions of messages per second. Non-enterprise customers with higher throughput needs should consider [outbound streaming](/docs/platform/integrations/streaming) to Kafka, Kinesis, or other external queueing services. + +### Example: Friend request processing with Lambda + +The following example demonstrates a notification flow for a social media application using AWS Lambda: + +#### Step 1: Configure the integration + +Configure an [AWS Lambda integration](/docs/platform/integrations/webhooks/lambda) in your Ably dashboard: + +* **Event type:** `channel.message` +* **Channel filter:** `^notifications:outbox$` (only trigger for outbox messages) +* **Enveloped:** Enabled (to receive full message metadata) + +#### Step 2: Client publishes a friend request + + +```javascript +const ably = new Ably.Realtime({ authUrl: '/auth/outbox-token' }); +const outbox = ably.channels.get('notifications:outbox'); + +// User sends a friend request +await outbox.publish('friend-request', { + type: 'friend-request', + fromUserId: 'user123', + toUserId: 'user456', + timestamp: Date.now() +}); +``` + + +#### Step 3: Lambda processes and validates + + +```javascript +const Ably = require('ably'); + +exports.handler = async (event) => { + console.log('Received notification request:', JSON.stringify(event)); + + // Parse the incoming event + const message = event.messages[0]; + const data = JSON.parse(message.data); + + // Validate the request, checking: + // - User existence + // - Payload integrity + // - Business rules (e.g., not already friends, not blocked) + + if (!data.fromUserId || !data.toUserId) { + console.error('Invalid friend request data'); + return { statusCode: 400, body: 'Invalid request' }; + } + + const isValid = await validateFriendRequest(data.fromUserId, data.toUserId); + if (!isValid) { + console.log('Friend request validation failed'); + return { statusCode: 403, body: 'Request denied' }; + } + + // Prepare the notification payload + const fromUserProfile = await getUserProfile(data.fromUserId); + + const notification = { + type: 'friend-request', + fromUserId: data.fromUserId, + fromUserName: fromUserProfile.name, + fromUserAvatar: fromUserProfile.avatar, + timestamp: data.timestamp, + }; + + // Use REST client to publish to the target user's inbox + const ably = new Ably.Rest({ key: process.env.ABLY_API_KEY }); + const inbox = ably.channels.get(`inbox:${data.toUserId}`); + + try { + await inbox.publish('notification', notification); + console.log(`Notification sent to inbox:${data.toUserId}`); + return { statusCode: 200, body: 'Success' }; + } catch (error) { + console.error('Failed to publish notification:', error); + return { statusCode: 500, body: 'Failed to send notification' }; + } +}; +``` + + +#### Step 4: Recipient receives the notification + + +```javascript +const ably = new Ably.Realtime({ authUrl: '/auth/inbox-token' }); +const inbox = ably.channels.get('inbox:user456'); + +inbox.subscribe('notification', (message) => { + const notification = message.data; + + if (notification.type === 'friend-request') { + displayFriendRequest(notification); + } +}); + +function displayFriendRequest(notification) { + console.log(`Friend request from ${notification.fromUserName}`); + // Update UI to show the notification +} +``` + + +### Integration considerations + +When implementing your processing pipeline, consider: + +* **Idempotency:** Design your pipeline to handle duplicate messages gracefully. Ably provides [idempotent publishing](/docs/platform/architecture/idempotency), which can help prevent duplicate notifications at the publish stage. +* **Error handling:** Implement proper error handling and monitoring. Use Ably's [`[meta]log` channel](/docs/platform/errors#meta) to track integration errors. +* **Scalability:** + * All integrations have [concurrency limits](/docs/platform/pricing/limits#integrations) based on your account plan. For high throughput, consider streaming to a queue service to handle traffic spikes. + * Lambda functions scale automatically, but monitor for rate limits and usage. + * Ably Queues provide guaranteed ordering but have throughput limits on non-enterprise accounts (200 msg/s) +* **Retry behavior:** Understand your integration's retry behavior: + * [Lambda retries](/docs/platform/integrations/webhooks/lambda#retry) up to 2 times with delays between attempts + * [Webhooks retry](/docs/platform/integrations/webhooks#retry) up to 2 times with delays between attempts + * [Queues](/docs/platform/integrations/queues) provide at-least-once delivery with message acknowledgment, failed messages are moved to a dead-letter queue after max attempts +* **Ordering guarantees:** If strong ordering is required, use [Ably Queues](/docs/platform/integrations/queues) which provide reliable ordering of messages by channel. + +## Cost optimization + +Understanding the cost implications of different architectural decisions helps you build efficiently at scale. + +### Individual inboxes vs general channel: A calculation + +Ably's pricing includes two main components relevant to notifications: + +* **Channel attachments:** Priced per channel minute. When a client subscribes to a channel, that channel is "attached" for as long as the client remains subscribed. +* **Messages:** Priced per message. Each message published and delivered counts toward your usage. + +#### Scenario: System-wide notification + +Assume you want to send a notification to 10,000 clients. Let's compare the costs: + +**Option 1: Individual inboxes (using batch publish)** + +* Publish to 10,000 individual inbox channels (using [batch publish](/docs/api/rest-api#batch-publish) to reduce API calls) +* Each channel publish counts as a separate inbound message +* Each client receives 1 message +* Cost: 10,000 inbound messages + 10,000 outbound messages = **20,000 messages** + +If you send this notification once per day: +* Monthly messages: 20,000 × 30 = **600,000 messages/month** +* No additional channel attachment costs (clients are already attached to their inboxes) + +**Option 2: General broadcast channel** + +* Publish 1 notification to the general channel +* 10,000 clients are subscribed to the general channel +* Cost: 1 inbound message + 10,000 outbound messages = **10,001 messages** +* Plus: Channel attachment cost for 10,000 clients subscribed to the general channel + +If clients are connected for an average of 4 hours per day: +* Channel minutes per client per day: 4 × 60 = 240 minutes +* Total channel minutes per month: 10,000 clients × 240 minutes × 30 days = **72,000,000 channel minutes/month** + +#### Cost comparison + +Using Ably's pricing (check current [pricing page](/pricing) for exact rates): + +**Individual inboxes (1 notification/day):** +* 600,000 messages/month +* No extra channel costs (inbox channels needed anyway) + +**General channel (1 notification/day):** +* ~300,000 messages/month (half the message cost) +* 72 million channel minutes/month (significant channel attachment cost) + +#### The crossover point + +For infrequent broadcasts (daily or less), the general channel's message savings are offset by its channel attachment costs. As notification frequency increases, the message cost differential becomes more significant. + +Example with 100 notifications per day: + +**Individual inboxes:** +* Messages: 20,000 × 100 × 30 = **60,000,000 messages/month** + +**General channel:** +* Messages: 10,001 × 100 × 30 = **30,003,000 messages/month** (half the messages) +* Channel minutes: **72,000,000 channel minutes/month** (same channel cost) + +At higher frequencies, the general channel's message cost advantage becomes significant - +using half the messages compared to individual inboxes, which can justify the channel attachment costs. +Messages are typically more expensive than channel minutes. + +#### Recommendation + +* **Use individual inboxes** for targeted notifications or when broadcast notifications are rare +* **Use a hybrid approach** with both individual inboxes for targeted notifications and a general channel for high-frequency system-wide broadcasts + + + +### Other cost optimizations + +* **Connection management:** Call `close()` on Ably clients when users log out to immediately clean up connections. Adjust [heartbeat intervals](/docs/connect#heartbeat) to detect dropped connections faster. +* **Message batching:** If publishing to multiple inboxes, use the [batch publish REST endpoint](/docs/api/rest-api#batch-publish) to reduce API calls. +* **Token lifetime:** Use appropriate token TTLs to balance security and token refresh overhead. +* **Batch outbound messages:** If inboxes receive multiple notifications per second, consider [batching](/docs/messages/batch#server-side) them with Ably's server-side batching to reduce outbound message counts. + +## Handling offline notifications + +Clients may not always be online when a notification arrives. Ably provides multiple mechanisms to ensure they receive important notifications: + +### Message history + +Ably stores [message history](/docs/channels/history), if enabled, for 24 hours by default +(up to 1 year with configuration). +When clients come online, they can retrieve missed notifications from their inbox: + + +```javascript +const ably = new Ably.Realtime({ authUrl: '/auth/inbox-token' }); +const inbox = ably.channels.get('inbox:client456'); + +// Subscribe to new notifications +inbox.subscribe('notification', (message) => { + handleNotification(message.data); +}); + +// Retrieve notifications received while offline +const historyPage = await inbox.history({ limit: 50, untilAttach:true }); +historyPage.items.forEach(message => { + handleNotification(message.data); +}); +``` + + +Message history is particularly useful for notifications that clients need to see when they return, +but that don't require immediate push notification delivery. +This is common in internal systems where notifications might inform clients of completed processes, +status updates, or system alerts that can be reviewed when a client connects. + +### Push notifications for critical alerts + +For user-facing applications where notifications require attention even when the app is not running, +push notifications can be used. These are particularly useful for social interactions, +time-sensitive alerts, or critical updates that users should act upon immediately. + +Ably provides native support for push notifications through integration with [Apple Push Notification Service (APNs)](/docs/push/configure/device) and [Firebase Cloud Messaging (FCM)](/docs/push/configure/device). This enables your processing pipeline to send notifications directly to users' devices, ensuring delivery even when they're offline. + +For detailed information on configuring and using push notifications with Ably, including device registration, +notification payloads, and platform-specific setup, see the [push notifications guide](/docs/guides/pubsub/push-notifications). + +### Best practices for offline handling + +* **Enable history on inbox channels** to allow users to retrieve missed notifications +* **Set appropriate history retention** based on your use case (24 hours to 1 year) +* **Use push notifications for critical alerts** that require immediate user attention +* **Consider notification priorities** - not all notifications need push delivery + +## Production-ready checklist + +Before launching your notification center, review these key points: + +* **Authentication:** Use token authentication for all client-side communication with short TTLs (1-4 hours). +* **Capabilities:** Apply the principle of least privilege - clients should only have publish access to an outbox and subscribe access to their own inbox. +* **Validation:** Validate all notification request data in your processing pipeline, never trust client data. +* **Monitoring:** Subscribe to [`[meta]log` channels](/docs/platform/errors#meta) to track integration errors and issues. +* **Error handling:** Implement proper error handling in your processing pipeline and consider dead letter queues for failed notifications. +* **Rate limiting:** Implement rate limiting in tokens to help prevent abuse. +* **Testing:** Test your notification flow end-to-end, including error cases and retry behavior. +* **Scalability:** Ensure your processing pipeline can scale to handle peak loads (consider integration concurrency limits). +* **Cost monitoring:** Set up billing alerts and monitor usage patterns to optimize costs. + +## Next steps + +* Read the [token authentication documentation](/docs/auth/token) for detailed auth implementation. +* Explore [integration options](/docs/platform/integrations) to choose the right processing pipeline for your needs. +* Learn about [push notifications](/docs/push) to handle offline delivery. +* Review the [batch publish API](/docs/api/rest-api#batch-publish) for efficient multi-channel publishing. +* Understand [message history](/docs/channels/history) for retrieving missed notifications. +* Check the [pricing page](/pricing) to understand costs at your scale. +* Try the [pub/sub getting started guide](/docs/getting-started) to build a proof of concept. From f4a666642917105c123d8bec16211fa0a5f9d917 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Tue, 9 Dec 2025 16:30:02 +0000 Subject: [PATCH 02/33] docs: move navbar location for guides --- src/data/nav/pubsub.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index 053214728f..f2e0f52df6 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -325,6 +325,15 @@ export default { }, ], }, + { + name: 'Guides', + pages: [ + { + name: 'Notifications center', + link: '/docs/guides/pub-sub/notifications-center', + }, + ], + }, ], api: [ { @@ -511,15 +520,6 @@ export default { }, ], }, - { - name: 'Guides', - pages: [ - { - name: 'Notifications center', - link: '/docs/guides/pub-sub/notifications-center', - }, - ], - }, { name: 'REST API', link: '/docs/api/rest-api', From cd6ecfda5b445f686bb3bc9f3d10731ab8978392 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Wed, 10 Dec 2025 12:26:40 +0000 Subject: [PATCH 03/33] docs: add note on resume and show history defaults to 2 mins unless specifically extended. --- .../docs/guides/pub-sub/notifications-center.mdx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index b1fa4a90f8..c674e2544d 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -23,7 +23,7 @@ Delivering notifications in realtime is critical for user engagement. Ably's [se For notifications that need to reach users even when they're offline, Ably integrates seamlessly with push notification services like [Apple Push Notification Service (APNS) and Firebase Cloud Messaging (FCM)](/docs/push), ensuring your users never miss important updates. -## Architecting your notification center: The outbox/inbox pattern +## Architecting your notification center The outbox/inbox pattern is a proven architecture for building scalable notification systems. It provides clear separation of concerns, simplified authentication, and flexible processing workflows. @@ -441,10 +441,17 @@ General channels save on message costs (1 inbound vs N inbound) but add channel Clients may not always be online when a notification arrives. Ably provides multiple mechanisms to ensure they receive important notifications: -### Message history +### Short-term message history + +Ably stores messages by default for 2 minutes to support short-term [history](/docs/storage-history/storage) and automatic connection recovery. +Ably's [resume feature](/docs/platform/architecture/connection-recovery#why) allows clients to reconnect and receive any messages they missed during a temporary disconnection. +It is enabled by default in all Ably SDKs and handled automatically. + +### Longer-term message history + +If longer retention is required, you can enable this using a rule to [persist all messages](/docs/storage-history/storage#all-message-persistence) for a particular channel or namespace. +This defaults to 24 hours, but can be configured up to 1 year for some plans. -Ably stores [message history](/docs/channels/history), if enabled, for 24 hours by default -(up to 1 year with configuration). When clients come online, they can retrieve missed notifications from their inbox: From 1086a39337b59d2d27fca458edc411e420f568b1 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Wed, 10 Dec 2025 12:32:58 +0000 Subject: [PATCH 04/33] fixing more PR comments --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index c674e2544d..6d652f9c75 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -491,7 +491,7 @@ notification payloads, and platform-specific setup, see the [push notifications ### Best practices for offline handling * **Enable history on inbox channels** to allow users to retrieve missed notifications -* **Set appropriate history retention** based on your use case (24 hours to 1 year) +* **Set appropriate history retention** based on your use case (2 minutes to 1 year) * **Use push notifications for critical alerts** that require immediate user attention * **Consider notification priorities** - not all notifications need push delivery @@ -499,7 +499,7 @@ notification payloads, and platform-specific setup, see the [push notifications Before launching your notification center, review these key points: -* **Authentication:** Use token authentication for all client-side communication with short TTLs (1-4 hours). +* **Authentication:** Use JWT authentication for all client-side communication with short TTLs (1-4 hours max). * **Capabilities:** Apply the principle of least privilege - clients should only have publish access to an outbox and subscribe access to their own inbox. * **Validation:** Validate all notification request data in your processing pipeline, never trust client data. * **Monitoring:** Subscribe to [`[meta]log` channels](/docs/platform/errors#meta) to track integration errors and issues. @@ -511,7 +511,7 @@ Before launching your notification center, review these key points: ## Next steps -* Read the [token authentication documentation](/docs/auth/token) for detailed auth implementation. +* Read the [JWT authentication documentation](/docs/auth/token) for detailed auth implementation. * Explore [integration options](/docs/platform/integrations) to choose the right processing pipeline for your needs. * Learn about [push notifications](/docs/push) to handle offline delivery. * Review the [batch publish API](/docs/api/rest-api#batch-publish) for efficient multi-channel publishing. From 4f2ae60eef86f52009c577326c75a461dbc48ebf Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 16:11:51 +0000 Subject: [PATCH 05/33] Simplify token usage section in notifications --- .../guides/pub-sub/notifications-center.mdx | 53 ++++--------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 6d652f9c75..a80a1d3160 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -109,14 +109,14 @@ You need to ensure that clients can only publish to an outbox and only receive n Ably's [token authentication](/docs/auth/token) with JSON Web Tokens (JWT) provides the flexibility to implement fine-grained access control. Tokens are short-lived, can be easily revoked, and include [capabilities](/docs/auth/capabilities) that define what actions a client can perform. -For a notification center, you typically need two types of tokens: +For a notification center, you typically need two types of access: -* **Outbox tokens:** Grant publish-only access to the outbox channel(s). -* **Inbox tokens:** Grant subscribe-only access to a user's specific inbox channel. +* **Outbox publish:** Grant publish-only access to the outbox channel(s). +* **Inbox subscribe:** Grant subscribe-only access to a user's specific inbox channel. -### Creating an outbox token +### Creating a token -The following example shows how to generate a JWT that allows a user to publish to the outbox: +The following example shows how to generate a JWT that strictly allows publish access to the outbox channel and subscribe access to a specific inbox channel. It also includes the `clientId` to identify the client: ```javascript @@ -135,7 +135,8 @@ const claims = { "iat": currentTime, "exp": currentTime + 3600, // Token expires in 1 hour "x-ably-capability": JSON.stringify({ - "notifications:outbox": ["publish"] + "notifications:outbox": ["publish"] // Outbox publish access + "inbox:client123": ["subscribe", "history"] // Inbox subscribe + history access }), "x-ably-clientId": "client123" // Identify the client } @@ -150,44 +151,8 @@ console.log('Outbox JWT:', token); ``` -### Creating an inbox token - -The following example shows how to generate a JWT that allows a client to subscribe to their inbox: - - -```javascript -const jwt = require("jsonwebtoken"); - -const header = { - "typ": "JWT", - "alg": "HS256", - "kid": "{{ API_KEY_NAME }}" -} - -const currentTime = Math.round(Date.now() / 1000); - -// Allow subscribe-only access to the client's inbox -const claims = { - "iat": currentTime, - "exp": currentTime + 3600, // Token expires in 1 hour - "x-ably-capability": JSON.stringify({ - "inbox:user123": ["subscribe", "history"] - }), - "x-ably-clientId": "client123" -} - -const token = jwt.sign( - claims, - "{{ API_KEY_SECRET }}", - { header: header } -); - -console.log('Inbox JWT:', token); -``` - - **History capability:** -Inbox tokens can also include `history` capability to allow clients to retrieve notifications they might have missed while offline. +Inbox tokens can also include the `history` capability to allow clients to retrieve notifications they might have missed while offline. Message persistence must be enabled to use history - see [message history documentation](/docs/channels/history) for details. ### Best practices @@ -254,7 +219,7 @@ const outbox = ably.channels.get('notifications:outbox'); // User sends a friend request await outbox.publish('friend-request', { type: 'friend-request', - fromUserId: 'user123', + fromUserId: 'client123', toUserId: 'user456', timestamp: Date.now() }); From 4e3ddd97a4d26c90f20ec3d3dc7026bc51cf7933 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 16:14:14 +0000 Subject: [PATCH 06/33] Minor grammar fix --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index a80a1d3160..8237c77328 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -161,7 +161,7 @@ Message persistence must be enabled to use history - see [message history docume * **Tie tokens to clientId:** Always include a `clientId` in your tokens to identify the client and enable auditing. You can also setup a rule to prevent anonymous connections. * **Implement token refresh:** Use [`authUrl`](/docs/auth/token#auth-url) or [`authCallback`](/docs/auth/token#auth-callback) to automatically refresh expiring tokens. * **Validate on the server:** Never trust client-provided data. Your processing pipeline should validate all notification request data. -* **Custom Rate Limits:** It is possible to apply custom rate limits to tokens using Ably's [rate limiting feature](docs/auth/capabilities#jwt-limits). This is recommended for outbox tokens to help prevent abuse. Standard connection rate limits still apply based on your account plan. +* **Custom rate Limits:** It is possible to apply custom rate limits to tokens using Ably's [rate limiting feature](docs/auth/capabilities#jwt-limits). This is recommended for outbox tokens to help prevent abuse. Standard connection rate limits still apply based on your account plan. ## Integration: Processing notifications From f814ef4935dd6c51e52a19c870bfc385901f45be Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 16:18:25 +0000 Subject: [PATCH 07/33] reformat and simplify integration options --- .../guides/pub-sub/notifications-center.mdx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 8237c77328..6c1d4cd441 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -177,26 +177,22 @@ Ably provides multiple integration options: The right integration depends on your throughput, ordering requirements, and infrastructure: -**AWS Lambda / Serverless Functions:** -* **Best for:** Low to high throughput with automatic scaling (up to account limits) -* **Pros:** Scales automatically, no server management, pay per execution -* **Cons:** Possible to quickly overspend without careful monitoring and appropriate limits, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +#### AWS Lambda / Serverless Functions +Serverless functions offer automatic scaling with no server management and pay-per-execution pricing, making them ideal for low to high throughput scenarios. However, costs can escalate quickly without proper monitoring and limits. Keep in mind that [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. + * **Recommended when:** You want simplicity and automatic scaling -**Webhooks to your own servers:** -* **Best for:** Custom infrastructure or specific processing requirements -* **Pros:** Full control over processing logic and infrastructure -* **Cons:** You manage scaling and reliability, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +#### Webhooks to your own servers +This approach gives you full control over your processing logic and infrastructure, though you're responsible for managing scaling and reliability yourself. Like Lambda, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. + * **Recommended when:** You have existing infrastructure or need specific processing capabilities -**Ably Queues:** -* **Best for:** Strong ordering guarantees and fault-tolerant processing -* **Pros:** Guaranteed ordering, at-least-once delivery, fault-tolerant -* **Cons:** Non-enterprise accounts limited to 200 msg/s per account, [concurrency limits](/docs/platform/pricing/limits#integrations) apply +#### Ably Queues +Ably Queues provide guaranteed ordering, at-least-once delivery, and fault-tolerant processing, making them the best choice when message ordering is critical. Non-enterprise accounts are limited to 200 msg/s per account, and [concurrency limits](/docs/platform/pricing/limits#integrations) still apply. + * **Recommended when:** Message ordering is critical and throughput is within limits Enterprise customers can scale Ably Queues to millions of messages per second. Non-enterprise customers with higher throughput needs should consider [outbound streaming](/docs/platform/integrations/streaming) to Kafka, Kinesis, or other external queueing services. - ### Example: Friend request processing with Lambda The following example demonstrates a notification flow for a social media application using AWS Lambda: From eaf1d23f8476630449b1a1c6bf45dfe520b3ac1e Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 16:19:26 +0000 Subject: [PATCH 08/33] simplify auth token usage in notifications center guide --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 6c1d4cd441..5f1fa4f7c3 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -209,7 +209,7 @@ Configure an [AWS Lambda integration](/docs/platform/integrations/webhooks/lambd ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/outbox-token' }); +const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const outbox = ably.channels.get('notifications:outbox'); // User sends a friend request From 45d5538bf566637c98b1928361efcb74563534a7 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 16:20:31 +0000 Subject: [PATCH 09/33] minor grammar fix --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 5f1fa4f7c3..7c665dd9f4 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -411,7 +411,7 @@ It is enabled by default in all Ably SDKs and handled automatically. ### Longer-term message history If longer retention is required, you can enable this using a rule to [persist all messages](/docs/storage-history/storage#all-message-persistence) for a particular channel or namespace. -This defaults to 24 hours, but can be configured up to 1 year for some plans. +This defaults to 24 hours, but can be configured up to 1 year for some packages. When clients come online, they can retrieve missed notifications from their inbox: From 2d2a391de6185189c170100ccb46ef84966b2e39 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 17:05:21 +0000 Subject: [PATCH 10/33] wip --- .../guides/pub-sub/notifications-center.mdx | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 7c665dd9f4..b048ca5050 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -413,30 +413,58 @@ It is enabled by default in all Ably SDKs and handled automatically. If longer retention is required, you can enable this using a rule to [persist all messages](/docs/storage-history/storage#all-message-persistence) for a particular channel or namespace. This defaults to 24 hours, but can be configured up to 1 year for some packages. -When clients come online, they can retrieve missed notifications from their inbox: +When clients come online, they can retrieve missed notifications from their inbox. To avoid processing duplicate messages, clients should track the last message they've seen using the [`message.id`](/docs/api/realtime-sdk/types#message) field. Optionally, you can also store the [`message.timestamp`](/docs/api/realtime-sdk/types#message) and use it with the [`start`](/docs/api/realtime-sdk/channels#history) parameter to limit how far back you query in history. + +#### Avoiding duplicate processing + +Each message has a unique [`id`](/docs/api/realtime-sdk/types#message) assigned by Ably. If your client stores the `id` of the last message it successfully processed, you can query history and stop processing when you encounter that message: ```javascript const ably = new Ably.Realtime({ authUrl: '/auth/inbox-token' }); const inbox = ably.channels.get('inbox:client456'); +// Retrieve the last processed message ID and timestamp from local storage +const lastProcessedId = localStorage.getItem('lastNotificationId'); +const lastProcessedTimestamp = localStorage.getItem('lastNotificationTimestamp'); + // Subscribe to new notifications inbox.subscribe('notification', (message) => { handleNotification(message.data); + // Store both the message ID and timestamp after successful processing + localStorage.setItem('lastNotificationId', message.id); + localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); }); // Retrieve notifications received while offline -const historyPage = await inbox.history({ limit: 50, untilAttach:true }); -historyPage.items.forEach(message => { +// Use start parameter to limit how far back we query (optional but can help limit result set) +const historyOptions = { + limit: 100, + untilAttach: true +}; + +if (lastProcessedTimestamp) { + historyOptions.start = parseInt(lastProcessedTimestamp); +} + +const historyPage = await inbox.history(historyOptions); + +// Messages are fetched backwards when using untilAttach +for (const message of historyPage.items) { + // Stop when we find the last message we processed + if (message.id === lastProcessedId) { + break; + } handleNotification(message.data); -}); + localStorage.setItem('lastNotificationId', message.id); + localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); +} ``` -Message history is particularly useful for notifications that clients need to see when they return, -but that don't require immediate push notification delivery. -This is common in internal systems where notifications might inform clients of completed processes, -status updates, or system alerts that can be reviewed when a client connects. +The `start` parameter is inclusive and helps limit the query range to messages from that timestamp onwards, but you still need to check `message.id` to detect which specific message you last processed. This approach works because history with `untilAttach: true` returns messages in reverse order, allowing you to paginate backwards and stop when you find the last message you've already seen. + +Message history is particularly useful for notifications that clients need to see when they return, but that don't require immediate push notification delivery. This is common in internal systems where notifications might inform clients of completed processes, status updates, or system alerts that can be reviewed when a client connects. ### Push notifications for critical alerts From 006f8d61c670665db484689862fa6b5fa6a6b6f0 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 17:14:48 +0000 Subject: [PATCH 11/33] Expand section on offline handling to use message.id for tracking --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index b048ca5050..d1227daf01 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -468,14 +468,11 @@ Message history is particularly useful for notifications that clients need to se ### Push notifications for critical alerts -For user-facing applications where notifications require attention even when the app is not running, -push notifications can be used. These are particularly useful for social interactions, -time-sensitive alerts, or critical updates that users should act upon immediately. +For user-facing applications where notifications require attention even when the app is not running, push notifications can be used. These are particularly useful for social interactions, time-sensitive alerts, or critical updates that users should act upon immediately. Ably provides native support for push notifications through integration with [Apple Push Notification Service (APNs)](/docs/push/configure/device) and [Firebase Cloud Messaging (FCM)](/docs/push/configure/device). This enables your processing pipeline to send notifications directly to users' devices, ensuring delivery even when they're offline. -For detailed information on configuring and using push notifications with Ably, including device registration, -notification payloads, and platform-specific setup, see the [push notifications guide](/docs/guides/pubsub/push-notifications). +For detailed information on configuring and using push notifications with Ably, including device registration, notification payloads, and platform-specific setup, see the [push notifications guide](/docs/guides/pubsub/push-notifications). ### Best practices for offline handling From 1b18c1f4bad355e055b401430a8700e2992fe795 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 17:44:50 +0000 Subject: [PATCH 12/33] minor comment fixes --- .../docs/guides/pub-sub/notifications-center.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index d1227daf01..28e7401592 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -282,7 +282,7 @@ exports.handler = async (event) => { ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/inbox-token' }); +const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const inbox = ably.channels.get('inbox:user456'); inbox.subscribe('notification', (message) => { @@ -429,7 +429,7 @@ const lastProcessedId = localStorage.getItem('lastNotificationId'); const lastProcessedTimestamp = localStorage.getItem('lastNotificationTimestamp'); // Subscribe to new notifications -inbox.subscribe('notification', (message) => { +await inbox.subscribe('notification', (message) => { handleNotification(message.data); // Store both the message ID and timestamp after successful processing localStorage.setItem('lastNotificationId', message.id); @@ -437,10 +437,9 @@ inbox.subscribe('notification', (message) => { }); // Retrieve notifications received while offline -// Use start parameter to limit how far back we query (optional but can help limit result set) const historyOptions = { limit: 100, - untilAttach: true + untilAttach: true // Fetch messages in reverse order from the last attach point }; if (lastProcessedTimestamp) { @@ -456,8 +455,6 @@ for (const message of historyPage.items) { break; } handleNotification(message.data); - localStorage.setItem('lastNotificationId', message.id); - localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); } ``` @@ -466,6 +463,10 @@ The `start` parameter is inclusive and helps limit the query range to messages f Message history is particularly useful for notifications that clients need to see when they return, but that don't require immediate push notification delivery. This is common in internal systems where notifications might inform clients of completed processes, status updates, or system alerts that can be reviewed when a client connects. + + ### Push notifications for critical alerts For user-facing applications where notifications require attention even when the app is not running, push notifications can be used. These are particularly useful for social interactions, time-sensitive alerts, or critical updates that users should act upon immediately. From 5abe093ffc0486d837f45978f2da98b377d7dbde Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 17:49:14 +0000 Subject: [PATCH 13/33] simplify section title and merge short-term disconnection details into a single paragraph --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 28e7401592..e93672f4c9 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -402,11 +402,9 @@ General channels save on message costs (1 inbound vs N inbound) but add channel Clients may not always be online when a notification arrives. Ably provides multiple mechanisms to ensure they receive important notifications: -### Short-term message history +### Temporary disconnections -Ably stores messages by default for 2 minutes to support short-term [history](/docs/storage-history/storage) and automatic connection recovery. -Ably's [resume feature](/docs/platform/architecture/connection-recovery#why) allows clients to reconnect and receive any messages they missed during a temporary disconnection. -It is enabled by default in all Ably SDKs and handled automatically. +Ably stores messages by default for 2 minutes to support short-term [history](/docs/storage-history/storage) and automatic connection recovery. Ably's [resume feature](/docs/platform/architecture/connection-recovery#why) allows clients to reconnect and receive any messages they missed during a temporary disconnection. It is enabled by default in all Ably SDKs and handled automatically. ### Longer-term message history From 9dc62d639203a135a607bdbcd9a251a1bc520883 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 18:09:13 +0000 Subject: [PATCH 14/33] fix formatting and minor grammar issues --- .../docs/guides/pub-sub/notifications-center.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index e93672f4c9..8378ba2211 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -63,16 +63,12 @@ const generalChannel = 'notifications:inbox:general'; ``` -The outbox can be a single channel, or any numbers of channels to distribute load in high throughput scenarios. -Inboxes are client-specific, one per client. -The general channel is optional and used for notifications that should reach all clients. +The outbox can be a single channel, or any numbers of channels to distribute load in high throughput scenarios. Inboxes are client-specific, one per client. The general channel is optional and used for notifications that should reach all clients. #### Scaling outbox channels Each Ably account has [channel limits](/docs/platform/pricing/limits#channels) that govern the allowed rate of publish. -If you have high throughput with many clients publishing to the same outbox simultaneously, -you may need to split the outbox into multiple channels to avoid overwhelming a single channel and being rate limited. -For example, with 100,000 clinets all publishing notifications, a single outbox could become a bottleneck. +If you have high throughput with many clients publishing to the same outbox simultaneously, you may need to split the outbox into multiple channels to avoid overwhelming a single channel and being rate limited. For example, with 100,000 clinets all publishing notifications, a single outbox could become a bottleneck. How many outbox channels you need depends on your expected traffic and account limits. You can shard outbox channels based on some meaningful dimension, such as: @@ -102,8 +98,7 @@ See the [cost optimization section](#cost-optimization) for detailed calculation ## Authentication: Securing your notification center -Authentication is critical in a notification center. -You need to ensure that clients can only publish to an outbox and only receive notifications intended for them. +Authentication is critical in a notification center. You need to ensure that clients can only publish to an outbox and only receive notifications intended for them. ### Token-based authentication From 6c96ef6c346c1835f1d053257ef726e8532552ec Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Mon, 22 Dec 2025 18:25:01 +0000 Subject: [PATCH 15/33] fix auth token usage --- src/pages/docs/guides/pub-sub/notifications-center.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 8378ba2211..9bc27c7294 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -414,7 +414,7 @@ Each message has a unique [`id`](/docs/api/realtime-sdk/types#message) assigned ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/inbox-token' }); +const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const inbox = ably.channels.get('inbox:client456'); // Retrieve the last processed message ID and timestamp from local storage From e8b231017f9c59ec1f294bdca865e519dedc20f9 Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Wed, 24 Dec 2025 11:21:25 +0000 Subject: [PATCH 16/33] add section on using annotations for message tracking and ack --- .../guides/pub-sub/notifications-center.mdx | 117 +++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 9bc27c7294..3f603f98be 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -406,15 +406,16 @@ Ably stores messages by default for 2 minutes to support short-term [history](/d If longer retention is required, you can enable this using a rule to [persist all messages](/docs/storage-history/storage#all-message-persistence) for a particular channel or namespace. This defaults to 24 hours, but can be configured up to 1 year for some packages. -When clients come online, they can retrieve missed notifications from their inbox. To avoid processing duplicate messages, clients should track the last message they've seen using the [`message.id`](/docs/api/realtime-sdk/types#message) field. Optionally, you can also store the [`message.timestamp`](/docs/api/realtime-sdk/types#message) and use it with the [`start`](/docs/api/realtime-sdk/channels#history) parameter to limit how far back you query in history. +When clients come online, they can retrieve missed notifications from their inbox. To avoid processing duplicate messages, clients should track the last message they've seen using the [`message.id`](/docs/api/realtime-sdk/types#message) field. If client tracking is not possible, [message annotations](/docs/messages/annotations) can be used to mark messages as "read" or "delivered" – which can also be used as a means to accurately track message acknowledgment. -#### Avoiding duplicate processing +Optionally, you can also store the [`message.timestamp`](/docs/api/realtime-sdk/types#message) and use it with the [`start`](/docs/api/realtime-sdk/channels#history) parameter to limit how far back you query in history. + +#### Tracking via message ID Each message has a unique [`id`](/docs/api/realtime-sdk/types#message) assigned by Ably. If your client stores the `id` of the last message it successfully processed, you can query history and stop processing when you encounter that message: ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const inbox = ably.channels.get('inbox:client456'); // Retrieve the last processed message ID and timestamp from local storage @@ -460,6 +461,116 @@ Message history is particularly useful for notifications that clients need to se History returns a paginated list of messages. As such, you may need to paginate through multiple pages to find the last processed message, depending on how many messages were sent while the client was offline. +#### Tracking via message annotations + +[Message annotations](/docs/messages/annotations) enable clients to mark messages with metadata such as "read" or "delivered" receipts. Annotating a message will mutate the original, and this change is persisted by Ably – on querying history, clients can see which messages have been marked without needing to track individual message IDs. + +The act of annotating a message will result in a new annotation type message being published to the same channel. As such, it can also be used to track if a message has been acknowledged by a client, backed by all the same reliability and delivery guarantees as any other message. + + + +##### Enable annotations + +To use annotations, you must first enable them on your inbox channel namespace. Follow the instructions in the [message annotations documentation](/docs/messages/annotations#enable) to configure the *Message annotations, updates, and deletes* rule for your inbox namespace. + +##### Configure capabilities + +Annotations are controlled by specific [capabilities](/docs/auth/capabilities) that must be included in your client JWT tokens: + +* **`annotation-publish`:** Required for clients to publish annotations (such as marking messages as "delivered" or "read") +* **`subscribe`:** Required to receive annotation summaries (aggregated views with a minimum 1-second configurable delay) +* **`annotation-subscribe`:** Required to subscribe to individual annotation events (no delay, but higher message rates) + +For most notification center implementations, the `annotation-publish` and `subscribe` capabilities are sufficient. Use `annotation-subscribe` only if you need immediate annotation delivery and can handle the higher message volume. + +Update your token generation to include the `annotation-publish` capability: + + +```javascript +const jwt = require("jsonwebtoken"); + +const header = { + "typ": "JWT", + "alg": "HS256", + "kid": "{{ API_KEY_NAME }}" +} + +const currentTime = Math.round(Date.now() / 1000); + +const claims = { + "iat": currentTime, + "exp": currentTime + 3600, + "x-ably-capability": JSON.stringify({ + "inbox:client456": ["subscribe", "history", "annotation-publish"] + }), + "x-ably-clientId": "client456" +} + +const token = jwt.sign(claims, "{{ API_KEY_SECRET }}", { header: header }); +``` + + +##### Publishing annotations + +Clients can publish annotations to mark notifications as delivered or read: + + +```javascript +const inbox = ably.channels.get('inbox:client456'); +// Mark a notification as delivered when received +await inbox.subscribe('notification', async (message) => { + // Display the notification + handleNotification(message.data); + + // Publish a "delivered" annotation + await inbox.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' + }); +}); +``` + + +##### Receiving annotation summaries + +To track which messages have been acknowledged, subscribe to annotation summaries. Summaries are automatically aggregated by Ably and delivered to the channel with a minimum delay of 1 second (configurable): + + +```javascript +const inbox = ably.channels.get('inbox:client456'); + +// Subscribe to annotation summaries +await inbox.subscribe((message) => { + if (message.action === 'message.summary') { + const summary = message.annotations.summary; + + if (summary['receipts:flag.v1']) { + const { delivered, read } = summary['receipts:flag.v1']; + console.log(`Message ${message.serial}: ${delivered?.total || 0} delivered, ${read?.total || 0} read`); + + // Update your tracking system + updateDeliveryStatus(message.serial, { + delivered: delivered?.clientIds || [], + read: read?.clientIds || [] + }); + } + } +}); +``` + + +If you need immediate notification of annotations (without the 1-second delay), you can subscribe to individual annotation events by including the `annotation-subscribe` capability and using `channel.annotations.subscribe()`. However, this approach generates more messages and should only be used when the delay is unacceptable. See the [subscribe to individual annotation events](/docs/messages/annotations#individual-annotations) documentation for details. + +Annotations are automatically included when [querying history](/docs/messages/annotations#annotation-summaries), so you can see which historical messages have been marked as read without additional tracking infrastructure. + + + ### Push notifications for critical alerts For user-facing applications where notifications require attention even when the app is not running, push notifications can be used. These are particularly useful for social interactions, time-sensitive alerts, or critical updates that users should act upon immediately. From 9dd4e7c8569abcd8925491fe2afe2775c399845e Mon Sep 17 00:00:00 2001 From: Steven Lindsay Date: Wed, 24 Dec 2025 13:31:01 +0000 Subject: [PATCH 17/33] Update integrations to use streaming --- .../guides/pub-sub/notifications-center.mdx | 182 ++++++++++-------- 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/src/pages/docs/guides/pub-sub/notifications-center.mdx b/src/pages/docs/guides/pub-sub/notifications-center.mdx index 3f603f98be..51f85d12ef 100644 --- a/src/pages/docs/guides/pub-sub/notifications-center.mdx +++ b/src/pages/docs/guides/pub-sub/notifications-center.mdx @@ -32,7 +32,7 @@ The outbox/inbox pattern is a proven architecture for building scalable notifica The architecture consists of three main components: * **Outbox channel:** Clients publish notification requests to an outbox channel. This could be a friend request, an alert, or any other client-initiated action. -* **Processing pipeline:** An integration (Lambda function, webhook, or queue consumer) receives the outbox message, validates it, applies business logic, and determines the target recipients. +* **Processing pipeline:** An integration (Lambda function, webhook, or stream consumer) receives the outbox message, validates it, applies business logic, and determines the target recipients. * **Inbox channels:** After processing, notifications are published to client-specific inbox channels (e.g., `inbox:clientId`), where recipients are subscribed. [/TODO/]: # (Should add some diagram here showing the flow from user -> outbox -> processing -> inbox -> recipient tp break up the text a bit) @@ -70,8 +70,7 @@ The outbox can be a single channel, or any numbers of channels to distribute loa Each Ably account has [channel limits](/docs/platform/pricing/limits#channels) that govern the allowed rate of publish. If you have high throughput with many clients publishing to the same outbox simultaneously, you may need to split the outbox into multiple channels to avoid overwhelming a single channel and being rate limited. For example, with 100,000 clinets all publishing notifications, a single outbox could become a bottleneck. -How many outbox channels you need depends on your expected traffic and account limits. -You can shard outbox channels based on some meaningful dimension, such as: +How many outbox channels you need depends on your expected traffic and account limits. You can shard outbox channels based on some meaningful dimension, such as: * **User group:** `notifications:outbox:group1`, `notifications:outbox:group2`, etc. @@ -166,28 +165,45 @@ Ably provides multiple integration options: * **[Webhooks](/docs/platform/integrations/webhooks):** HTTP endpoints (including AWS Lambda, Azure Functions, Google Cloud Functions) * **[Streaming](/docs/platform/integrations/streaming):** Kafka, Kinesis, SQS, AMQP -* **[Queues](/docs/platform/integrations/queues):** Ably-managed queues for fault-tolerant processing ### Choosing your integration approach The right integration depends on your throughput, ordering requirements, and infrastructure: #### AWS Lambda / Serverless Functions -Serverless functions offer automatic scaling with no server management and pay-per-execution pricing, making them ideal for low to high throughput scenarios. However, costs can escalate quickly without proper monitoring and limits. Keep in mind that [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +Serverless functions offer automatic scaling with no server management and pay-per-execution pricing. However, costs can escalate quickly without proper monitoring and limits. Keep in mind that [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not always guaranteed. * **Recommended when:** You want simplicity and automatic scaling #### Webhooks to your own servers -This approach gives you full control over your processing logic and infrastructure, though you're responsible for managing scaling and reliability yourself. Like Lambda, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not guaranteed. +This approach gives you full control over your processing logic and infrastructure, though you're responsible for managing scaling and reliability yourself. Like serverless functions, [concurrency limits](/docs/platform/pricing/limits#integrations) apply and [ordering](/docs/platform/integrations/webhooks#ordering) is not always guaranteed. -* **Recommended when:** You have existing infrastructure or need specific processing capabilities +* **Recommended when:** You have existing infrastructure or require custom processing not suited to serverless functions -#### Ably Queues -Ably Queues provide guaranteed ordering, at-least-once delivery, and fault-tolerant processing, making them the best choice when message ordering is critical. Non-enterprise accounts are limited to 200 msg/s per account, and [concurrency limits](/docs/platform/pricing/limits#integrations) still apply. +#### Streaming to message brokers +[Streaming integrations](/docs/platform/integrations/streaming) enable you to stream notification events from Ably to external message brokers like Kafka, Amazon Kinesis, Google Pub/Sub, or RabbitMQ. This approach is designed for high-throughput scenarios and provides the most flexibility for complex processing workflows. -* **Recommended when:** Message ordering is critical and throughput is within limits +Streaming integrations are particularly well-suited for notification centers because they enable you to handle high message volumes that exceed webhook concurrency limits, implement custom ordering strategies (see [ordering considerations](#ordering-considerations) below), build fault-tolerant processing with at-least-once delivery semantics, and integrate with existing data processing infrastructure. + +* **Recommended when:** You need high throughput or complex processing pipelines + +### Ordering considerations + +Message ordering can be important in notification systems. You may want to ensure that notifications from the same client are processed in the order they were sent. + +#### Webhooks and Serverless functions + +**Batched mode:** +[Webhooks](/docs/platform/integrations/webhooks) configured in batched mode preserve ordering for messages published to the same channel by the same client. Batched webhooks group messages into batches (up to 1000 messages per batch) and deliver them sequentially to your webhook endpoint. However, if your throughput exceeds the rate at which the webhook can process batches, messages may be dropped. + +**Single message mode:** +Webhooks in single message mode do not guarantee ordering due to concurrent processing and retry behavior. If a webhook request fails and is retried, it may be processed after later messages have already been delivered. + +#### Streaming integrations +Streaming integrations provide reliable ordering control for high-throughput scenarios. When streaming to Kafka, Kinesis, or similar systems, you can [implement ordering strategies](/docs/messages#routing) by using message attributes as topic or partition keys. + +For example, when configuring a Kafka reactor rule, you could route to a dynamic topic based on the channel name, and use the `clientId` as the partition key. -Enterprise customers can scale Ably Queues to millions of messages per second. Non-enterprise customers with higher throughput needs should consider [outbound streaming](/docs/platform/integrations/streaming) to Kafka, Kinesis, or other external queueing services. ### Example: Friend request processing with Lambda The following example demonstrates a notification flow for a social media application using AWS Lambda: @@ -204,9 +220,7 @@ Configure an [AWS Lambda integration](/docs/platform/integrations/webhooks/lambd ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const outbox = ably.channels.get('notifications:outbox'); - // User sends a friend request await outbox.publish('friend-request', { type: 'friend-request', @@ -277,10 +291,8 @@ exports.handler = async (event) => { ```javascript -const ably = new Ably.Realtime({ authUrl: '/auth/token' }); const inbox = ably.channels.get('inbox:user456'); - -inbox.subscribe('notification', (message) => { +await inbox.subscribe('notification', (message) => { const notification = message.data; if (notification.type === 'friend-request') { @@ -302,20 +314,20 @@ When implementing your processing pipeline, consider: * **Idempotency:** Design your pipeline to handle duplicate messages gracefully. Ably provides [idempotent publishing](/docs/platform/architecture/idempotency), which can help prevent duplicate notifications at the publish stage. * **Error handling:** Implement proper error handling and monitoring. Use Ably's [`[meta]log` channel](/docs/platform/errors#meta) to track integration errors. * **Scalability:** - * All integrations have [concurrency limits](/docs/platform/pricing/limits#integrations) based on your account plan. For high throughput, consider streaming to a queue service to handle traffic spikes. + * All integrations have [concurrency limits](/docs/platform/pricing/limits#integrations) based on your account plan. For high throughput, consider streaming to a message broker like Kafka or Kinesis to handle traffic spikes. * Lambda functions scale automatically, but monitor for rate limits and usage. - * Ably Queues provide guaranteed ordering but have throughput limits on non-enterprise accounts (200 msg/s) + * Streaming integrations can handle millions of messages per second with appropriate broker infrastructure. * **Retry behavior:** Understand your integration's retry behavior: * [Lambda retries](/docs/platform/integrations/webhooks/lambda#retry) up to 2 times with delays between attempts * [Webhooks retry](/docs/platform/integrations/webhooks#retry) up to 2 times with delays between attempts - * [Queues](/docs/platform/integrations/queues) provide at-least-once delivery with message acknowledgment, failed messages are moved to a dead-letter queue after max attempts -* **Ordering guarantees:** If strong ordering is required, use [Ably Queues](/docs/platform/integrations/queues) which provide reliable ordering of messages by channel. + * Streaming integrations provide at-least-once delivery with broker-specific acknowledgment and retry mechanisms +* **Ordering guarantees:** If ordering is required, run webhooks in batched mode or use [streaming integrations](/docs/platform/integrations/streaming) with partition keys to control message ordering. See [ordering considerations](#ordering-considerations) for details. ## Cost optimization Understanding the cost implications of different architectural decisions helps you build efficiently at scale. -### Individual inboxes vs general channel: A calculation +### Individual inboxes vs general channel Ably's pricing includes two main components relevant to notifications: @@ -326,7 +338,7 @@ Ably's pricing includes two main components relevant to notifications: Assume you want to send a notification to 10,000 clients. Let's compare the costs: -**Option 1: Individual inboxes (using batch publish)** +**Option 1: Individual inboxes** * Publish to 10,000 individual inbox channels (using [batch publish](/docs/api/rest-api#batch-publish) to reduce API calls) * Each channel publish counts as a separate inbound message @@ -352,44 +364,60 @@ If clients are connected for an average of 4 hours per day: Using Ably's pricing (check current [pricing page](/pricing) for exact rates): +We will assume an approximate cost per million messages of $2.50, and a cost per million channel minutes of $1.00. + **Individual inboxes (1 notification/day):** * 600,000 messages/month * No extra channel costs (inbox channels needed anyway) +* **Estimated cost:** 600,000 messages × ($2.50 / 1,000,000) = **$1.50/month** **General channel (1 notification/day):** * ~300,000 messages/month (half the message cost) * 72 million channel minutes/month (significant channel attachment cost) +* **Estimated cost:** + * Messages: 300,000 × ($2.50 / 1,000,000) = $0.75 + * Channel minutes: 72,000,000 × ($1.00 / 1,000,000) = $72.00 + * **Total: $72.75/month** + +For this scenario, individual inboxes are significantly more cost-effective. #### The crossover point For infrequent broadcasts (daily or less), the general channel's message savings are offset by its channel attachment costs. As notification frequency increases, the message cost differential becomes more significant. -Example with 100 notifications per day: +Example with just 100 notifications per day: **Individual inboxes:** * Messages: 20,000 × 100 × 30 = **60,000,000 messages/month** +* **Estimated cost:** 60,000,000 × ($2.50 / 1,000,000) = **$150/month** **General channel:** * Messages: 10,001 × 100 × 30 = **30,003,000 messages/month** (half the messages) * Channel minutes: **72,000,000 channel minutes/month** (same channel cost) +* **Estimated cost:** + * Messages: 30,003,000 × ($2.50 / 1,000,000) = $75.01 + * Channel minutes: 72,000,000 × ($1.00 / 1,000,000) = $72.00 + * **Total: $147.01/month** -At higher frequencies, the general channel's message cost advantage becomes significant - -using half the messages compared to individual inboxes, which can justify the channel attachment costs. -Messages are typically more expensive than channel minutes. +At 100 notifications per day, the general channel pattern is already cheaper, and the savings grow with frequency: -#### Recommendation +| Notifications per day | Individual inboxes | General channel | Cost difference | +|-----------------------|-------------------|-----------------|-----------------| +| 100 | $150.00 | $147.01 | -$2.99 (2% cheaper) | +| 200 | $300.00 | $222.02 | -$77.98 (26% cheaper) | +| 400 | $600.00 | $372.04 | -$227.96 (38% cheaper) | +| 800 | $1,200.00 | $672.08 | -$527.92 (44% cheaper) | -* **Use individual inboxes** for targeted notifications or when broadcast notifications are rare -* **Use a hybrid approach** with both individual inboxes for targeted notifications and a general channel for high-frequency system-wide broadcasts +Individual inbox costs scale linearly with notification frequency (doubling notifications doubles the cost), while the general channel's primary cost (channel minutes at $72/month) remains constant regardless of notification frequency. This makes the general channel increasingly attractive as broadcast frequency grows and message costs dominate. - +#### Recommendation + +* **Use individual inboxes** for targeted notifications or when broadcast notifications are rare. +* **Use a hybrid approach** with both individual inboxes for targeted notifications and a general channel for high-frequency system-wide broadcasts. ### Other cost optimizations * **Connection management:** Call `close()` on Ably clients when users log out to immediately clean up connections. Adjust [heartbeat intervals](/docs/connect#heartbeat) to detect dropped connections faster. -* **Message batching:** If publishing to multiple inboxes, use the [batch publish REST endpoint](/docs/api/rest-api#batch-publish) to reduce API calls. * **Token lifetime:** Use appropriate token TTLs to balance security and token refresh overhead. * **Batch outbound messages:** If inboxes receive multiple notifications per second, consider [batching](/docs/messages/batch#server-side) them with Ably's server-side batching to reduce outbound message counts. @@ -406,9 +434,9 @@ Ably stores messages by default for 2 minutes to support short-term [history](/d If longer retention is required, you can enable this using a rule to [persist all messages](/docs/storage-history/storage#all-message-persistence) for a particular channel or namespace. This defaults to 24 hours, but can be configured up to 1 year for some packages. -When clients come online, they can retrieve missed notifications from their inbox. To avoid processing duplicate messages, clients should track the last message they've seen using the [`message.id`](/docs/api/realtime-sdk/types#message) field. If client tracking is not possible, [message annotations](/docs/messages/annotations) can be used to mark messages as "read" or "delivered" – which can also be used as a means to accurately track message acknowledgment. +When clients come online, they can retrieve missed notifications from their inbox. If notfications need to be handled idempotently, messages have a [`message.id`](/docs/api/realtime-sdk/types#message) field that can be tracked client-side to avoid processing duplicates. -Optionally, you can also store the [`message.timestamp`](/docs/api/realtime-sdk/types#message) and use it with the [`start`](/docs/api/realtime-sdk/channels#history) parameter to limit how far back you query in history. +Server-side tracking is also possible with [message annotations](/docs/messages/annotations), and can be used to mark messages as "read" or "delivered". This same process can also be used as a means to accurately track message acknowledgment from recipient clients. #### Tracking via message ID @@ -422,18 +450,26 @@ const inbox = ably.channels.get('inbox:client456'); const lastProcessedId = localStorage.getItem('lastNotificationId'); const lastProcessedTimestamp = localStorage.getItem('lastNotificationTimestamp'); +// Track whether we're still processing history +let processingHistory = true; +const queuedMessages = []; + // Subscribe to new notifications await inbox.subscribe('notification', (message) => { - handleNotification(message.data); - // Store both the message ID and timestamp after successful processing - localStorage.setItem('lastNotificationId', message.id); - localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); + if (processingHistory) { + // Queue messages that arrive while we're processing history + queuedMessages.push(message); + } else { + handleNotification(message.data); + localStorage.setItem('lastNotificationId', message.id); + localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); + } }); // Retrieve notifications received while offline const historyOptions = { limit: 100, - untilAttach: true // Fetch messages in reverse order from the last attach point + untilAttach: true // Fetch messages from before subscription up to the attached point }; if (lastProcessedTimestamp) { @@ -442,13 +478,22 @@ if (lastProcessedTimestamp) { const historyPage = await inbox.history(historyOptions); -// Messages are fetched backwards when using untilAttach -for (const message of historyPage.items) { - // Stop when we find the last message we processed - if (message.id === lastProcessedId) { +// Process historical messages (they arrive in reverse order with untilAttach) +for (const message of historyPage.items.reverse()) { + if (lastProcessedId && message.id === lastProcessedId) { break; } handleNotification(message.data); + localStorage.setItem('lastNotificationId', message.id); + localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); +} + +// Now process any messages that arrived while we were handling history +processingHistory = false; +for (const message of queuedMessages) { + handleNotification(message.data); + localStorage.setItem('lastNotificationId', message.id); + localStorage.setItem('lastNotificationTimestamp', message.timestamp.toString()); } ``` @@ -465,11 +510,7 @@ Message history is particularly useful for notifications that clients need to se [Message annotations](/docs/messages/annotations) enable clients to mark messages with metadata such as "read" or "delivered" receipts. Annotating a message will mutate the original, and this change is persisted by Ably – on querying history, clients can see which messages have been marked without needing to track individual message IDs. -The act of annotating a message will result in a new annotation type message being published to the same channel. As such, it can also be used to track if a message has been acknowledged by a client, backed by all the same reliability and delivery guarantees as any other message. - - +The act of annotating a message will result in a new annotation type message being published to the same channel. As such, it can also be used to track if a message has been acknowledged by a client in realtime, backed by all the same reliability and delivery guarantees as any other message. ##### Enable annotations @@ -480,41 +521,16 @@ To use annotations, you must first enable them on your inbox channel namespace. Annotations are controlled by specific [capabilities](/docs/auth/capabilities) that must be included in your client JWT tokens: * **`annotation-publish`:** Required for clients to publish annotations (such as marking messages as "delivered" or "read") -* **`subscribe`:** Required to receive annotation summaries (aggregated views with a minimum 1-second configurable delay) +* **`subscribe`:** Required to receive annotation summaries (aggregated views of annotations applied to a message, with a small delay) * **`annotation-subscribe`:** Required to subscribe to individual annotation events (no delay, but higher message rates) -For most notification center implementations, the `annotation-publish` and `subscribe` capabilities are sufficient. Use `annotation-subscribe` only if you need immediate annotation delivery and can handle the higher message volume. - -Update your token generation to include the `annotation-publish` capability: - - -```javascript -const jwt = require("jsonwebtoken"); - -const header = { - "typ": "JWT", - "alg": "HS256", - "kid": "{{ API_KEY_NAME }}" -} - -const currentTime = Math.round(Date.now() / 1000); - -const claims = { - "iat": currentTime, - "exp": currentTime + 3600, - "x-ably-capability": JSON.stringify({ - "inbox:client456": ["subscribe", "history", "annotation-publish"] - }), - "x-ably-clientId": "client456" -} +If you only require annotations for client-side tracking of message delivery/read status, you can include the additional `annotation-publish` in your clients' capability set when generating a token. -const token = jwt.sign(claims, "{{ API_KEY_SECRET }}", { header: header }); -``` - +For message acknowledgment tracking, it is recommended to use annotation summaries (with the `subscribe` capability) rather than subscribing to individual annotation events, as this reduces message volume and cost. ##### Publishing annotations -Clients can publish annotations to mark notifications as delivered or read: +Messages delivered to clients on a channel with annotations enabled also include a `serial` field. This unique identifier is used to reference the specific message when annotating it. ```javascript @@ -535,7 +551,7 @@ await inbox.subscribe('notification', async (message) => { ##### Receiving annotation summaries -To track which messages have been acknowledged, subscribe to annotation summaries. Summaries are automatically aggregated by Ably and delivered to the channel with a minimum delay of 1 second (configurable): +To track which messages have been acknowledged, subscribe to annotation summaries on the inbox channel: ```javascript @@ -561,8 +577,6 @@ await inbox.subscribe((message) => { ``` -If you need immediate notification of annotations (without the 1-second delay), you can subscribe to individual annotation events by including the `annotation-subscribe` capability and using `channel.annotations.subscribe()`. However, this approach generates more messages and should only be used when the delay is unacceptable. See the [subscribe to individual annotation events](/docs/messages/annotations#individual-annotations) documentation for details. - Annotations are automatically included when [querying history](/docs/messages/annotations#annotation-summaries), so you can see which historical messages have been marked as read without additional tracking infrastructure.