Notification Feeds

Notification feeds let you notify users about relevant interactions, for example

  • someone started to follow them
  • someone liked their post
  • someone left a comment on their post

Creating notification feeds

The built-in notification feed comes with the necessary configurations, but it's also possible to create your own notification feed group:

const myNotificationGrpup = await serverClient.feeds.createFeedGroup({
  id: "myid",
  // Group by activity type and day
  aggregation: { format: '{{ type }}-{{ time.strftime("%Y-%m-%d") }}' },
  // Enable notification tracking
  notification: {
    track_read: true,
    track_seen: true,
  },
});

Notification configuration

The notification config includes several settings that control how notifications are tracked and deduplicated:

  • TrackSeen: When enabled, tracks which notifications have been seen by the user
  • TrackRead: When enabled, tracks which notifications have been read by the user
  • DeduplicationWindow: Controls how duplicate notifications are handled (only available for the built-in notification feed group)
    • Empty string ("") = always deduplicate (default behavior)
    • Duration string (e.g., "24h", "7d") = time-based deduplication window

Note: Comments are not deduplicated. Each comment will create a separate notification activity, regardless of the deduplication window setting.

The built-in notification feed group has deduplication enabled by default (always deduplicate). To change the deduplication window, you need to update the notification feed group.

Aggregation format

The built-in notification feed uses the following aggregation format: "{{ target_id }}_{ type }}_{{ time.strftime('%Y-%m-%d') }}". You can change this syntax by updating the notification feed group.

You can see all supported fields and syntax for aggregation in the Aggregation guide.

It's possible to turn off aggregation, but still enable notification tracking. In that case every new activity will increase unread/unseen count. When you have aggregation turned on, unread/unseen will refer to the number of aggregated groups.

Adding notification activities

The built-in notification groups can automatically create notifications for the most common interactions (see Built-in notification feed section).

If you want to extend that, or create your own notification feed, you can add notification activities using server-side integration. You can add webhook handlers for the relevant events to create notifications without API calls from your client-side application to your server-side application.

It's important to note that target_id is only defined if a notification is created by Stream API. If you want to extend or replace this behavior by adding notification activities from your own application, you most likely need to extend the default aggregation format. You can see all supported fields and syntax for aggregation in the Aggregation guide.

Manual Activity Addition to Notification Feeds

You can directly add activities to notification feeds for complete control over notifications (only available server-side):

// Add a custom notification directly to a user's notification feed
await serverClient.feeds.addActivity({
  feeds: ["notification:user-123"], // Target user's notification feed
  type: "milestone", // Custom activity type
  text: "You've reached 1000 followers!",
  user_id: "user_id",
  extra_data: {
    milestone_type: "followers",
    count: 1000,
  },
});

// Add activity to custom notification feed group
await serverClient.feeds.addActivity({
  feeds: ["alerts:user-123"], // Custom notification feed group
  type: "system_alert",
  text: "Your subscription expires in 3 days",
  user_id: "user_id",
});

Important: When adding activities directly to notification feeds, ensure the activity type is included in the feed group's push_types configuration to trigger push notifications.

Built-in notification feed

Creating notification activities

The built-in notification feed allows you to automatically create notification activities. The following actions are supported and will automatically create notification activities for the target user depending on the action.

ActionTrigger UserTarget User (Recipient)Notification TypeNotification TextDeduplicated?Notes
React to ActivityUser who reacts (e.g., Bob)Activity author (e.g., Alice)reaction{user} reacted to your activity✅ YesMultiple reactions from the same user on the same activity are deduplicated within the deduplication window
React to CommentUser who reacts (e.g., Charlie)Comment author (e.g., Bob)comment_reaction{user} reacted to your comment✅ YesMultiple reactions from the same user on the same comment are deduplicated within the deduplication window. The activity author does NOT receive a notification for reactions
Comment on ActivityUser who comments (e.g., Bob)Activity author (e.g., Alice)comment{user} commented on your activity❌ NoEach comment creates a new notification; not deduplicated
Reply to CommentUser who replies (e.g., Charlie)Comment author (e.g., Bob)comment_reply{user} replied to your comment❌ NoEach reply creates a new notification; not deduplicated. The activity author does NOT receive a notification for replies
Follow UserUser who follows (e.g., Bob)User being followed (e.g., Alice)follow{user} started following you✅ YesMultiple follows/unfollows from the same user are deduplicated within the deduplication window
Mention in ActivityUser who creates activity with mention (e.g., Alice)Mentioned user (e.g., Bob)mention{user} mentioned you in an activity✅ YesMultiple mentions from the same user on the same activity are deduplicated within the deduplication window
Mention in CommentUser who creates comment with mention (e.g., Charlie)Mentioned user (e.g., Bob)comment_mention{user} mentioned you in a comment✅ YesMultiple mentions from the same user on the same comment are deduplicated within the deduplication window. The activity author does NOT receive a notification for mentions (they already get a comment notification)
Update Activity (add mentions)User who updates activity (e.g., Alice)Mentioned user (e.g., Bob)mention{user} mentioned you in an activity✅ YesWhen mentions are added via UpdateActivity or UpdateActivityPartial with handle_mention_notifications=true, mention notifications are automatically created for newly mentioned users. Multiple mentions from the same user on the same activity are deduplicated within the deduplication window
Update Comment (add mentions)User who updates comment (e.g., Charlie)Mentioned user (e.g., Bob)comment_mention{user} mentioned you in a comment✅ YesWhen mentions are added via UpdateComment with handle_mention_notifications=true, comment_mention notifications are automatically created for newly mentioned users. Multiple mentions from the same user on the same comment are deduplicated within the deduplication window. The activity author does NOT receive a notification for mentions (they already get a comment notification)

Adding notifications with the create notification activity flag only works if the target user (the one who should receive the notification) has a feed with group notification, and id <user id>.

// Eric follows Jane
await ericTimeline.follow(janeFeed, {
  // When true Jane's notification feed will be updated with follow activity
  create_notification_activity: true,
});

// Eric comments on Jane's activity
await ericClient.addComment({
  comment: "Agree!",
  object_id: janeActivity.id,
  object_type: "activity",
  // When true Jane's notification feed will be updated with comment activity
  create_notification_activity: true,
});

// Eric reacts to Jane's activity
await ericClient.addReaction({
  activity_id: janeActivity.id,
  // When true Jane's notification feed will be updated with reaction activity
  type: "like",
  create_notification_activity: true,
});

// Eric reacts to a comment posted to Jane's activity by Sara
await ericClient.addCommentReaction({
  id: saraComment.id,
  type: "like",
  // When true Sara's notification feed will be updated with comment reaction activity
  create_notification_activity: true,
});

Updating mentions in activities and comments

You can also create or remove mention notifications when updating activities or comments. This flag defaults to false for all endpoints it's supported in.

// Alice updates her activity to mention Bob
await ericClient.updateActivity({
  id: activityId,
  text: "Hey @Bob check this out!",
  mentioned_user_ids: ["bob"],
  handle_mention_notifications: true, // When true, Bob will receive a mention notification
});

// Alice updates her comment to mention Charlie
await ericClient.updateComment({
  id: commentId,
  comment: "Hey @Charlie!",
  mentioned_user_ids: ["charlie"],
  handle_mention_notifications: true, // When true, Charlie will receive a comment_mention notification
});

// Alice removes mentions from her activity
await ericClient.updateActivity({
  id: activityId,
  text: "Updated text without mentions",
  mentioned_user_ids: [],
  handle_mention_notifications: true, // When true, mention notifications for removed users are deleted
});

Commenting and reacting to your own activities and comments will not create notification activities.

Deleting notification activities

When you add notification activities with create_notification_activity you can also have the API automatically remove these when the corresponding trigger entity is deleted. This is done by using the flag delete_notification_activity which defaults to false for all endpoints it's supported in.

ActionNotification Type DeletedNotes
Remove Activity ReactionreactionOnly the notification activity created by the user removing the reaction is deleted. Other users' reaction notifications remain unaffected.
Remove Comment Reactioncomment_reactionOnly the notification activity created by the user removing the reaction is deleted. Other users' comment reaction notifications remain unaffected.
Delete CommentcommentDeletes the comment notification for the activity author.
Delete Commentcomment_mentionWhen a comment with mentions is deleted, comment_mention notifications are deleted for all mentioned users if delete_notification_activity=true.
Unfollow UserfollowOnly the notification activity created by the user performing the unfollow is deleted.
Delete ActivitymentionWhen an activity with mentions is deleted, mention notifications are deleted for all mentioned users if delete_notification_activity=true.
Delete Activities (batch)mentionWhen multiple activities with mentions are deleted via DeleteActivities with delete_notification_activity=true, mention notifications are deleted for all mentioned users across all deleted activities.
Update Activity (remove mentions)mentionWhen mentions are removed via UpdateActivity or UpdateActivityPartial with handle_mention_notifications=true, mention notifications are automatically removed for users no longer mentioned.
Update Comment (remove mentions)comment_mentionWhen mentions are removed via UpdateComment with handle_mention_notifications=true, comment_mention notifications are automatically removed for users no longer mentioned.

Deletion Scope: The deletion behavior differs depending on the action:

  • Reactions and Follows: Only the notification activity created by the specific user performing the deletion is removed. Other users' notifications remain unaffected.
  • Activities and Comments with Mentions: When an activity or comment containing mentions is deleted, notification activities are removed for all mentioned users if delete_notification_activity=true.
// Eric unfollows Jane
await ericTimeline.unfollow(janeFeed, {
  // When true the corresponding notification activity will be removed from Jane's notification feed
  delete_notification_activity: true,
});

// Eric removes his comment
await ericClient.deleteComment({
  id: commentId,
  // When true the corresponding notification activity will be removed from Jane's notification feed
  delete_notification_activity: true,
});

// Eric removes his activity reaction
await ericClient.deleteActivityReaction({
  activity_id: janeActivity.id,
  type: "like",
  // When true the corresponding notification activity will be removed from Jane's notification feed
  delete_notification_activity: true,
});

// Eric removes his comment reaction
await ericClient.deleteCommentReaction({
  id: saraComment.id,
  type: "like",
  // When true the corresponding notification activity will be removed from Jane's notification feed
  delete_notification_activity: true,
});

// Eric deletes his activity with mentions
await ericClient.deleteActivity({
  id: activityId,
  // When true, mention notifications for all mentioned users will be removed
  delete_notification_activity: true,
});

// Eric deletes multiple activities with mentions (batch operation)
await ericClient.deleteActivities({
  ids: [activityId1, activityId2],
  // When true, mention notifications for all mentioned users will be removed
  delete_notification_activity: true,
});

Trigger and Target

The trigger is the entity that triggered the creation of the notification activity. This can be a follow, a reaction or a comment. When the trigger is a comment the comment data is available on the trigger object, this allows for deep linking back to the comment that triggered the notification.

The target is the receiver of the trigger action. This can be a feed (e.g., a user's feed), an activity or a comment. When the target is an activity the activity data will be present in the target object. When the target is a comment the parent activity data and comment data will be present in the target object.

Example: A reply to a comment will include the data of the reply comment in the trigger and the parent comment and activity in the target.

{
  "target": {
    "id": "<activity_id>",
    "type": "<activity_type>",
    "user_id": "<activity_user_id>",
    "comment": {
      "id": "<parent_comment_id>",
      "comment": "this is the parent comment",
      "user_id": "<parent_comment_user_id>"
    }
  },
  "trigger": {
    "text": "<comment_user_id> replied to your comment",
    "type": "comment_reply",
    "comment": {
      "id": "<comment_id>",
      "user_id": "<comment_user_id>",
      "comment": "this is the reply comment"
    }
  }
}

Aggregating on comments and activities

By default the built-in notification feed aggregates on activities only with the aggregation format {{ target_id }}-{{ type }}-{{ time.strftime('%Y-%m-%d') }}. If you want to aggregate on comments and activities alike you need to update the aggregation format to

{% if comment_id %}
  {{ comment_id }}_{{ type }}_{{ time.strftime('%Y-%m-%d') }}
{% else %}
  {{ target_id }}_{{ type }}_{{ time.strftime('%Y-%m-%d') }}
{% endif %}

This lets you show notifications like

  • Your comment has 5 new likes or
  • Your comment has 3 new replies

Reading notification activities

// Read notifications
await notificationFeed.getOrCreate({
  limit: 20,
});
console.log(notificationFeed.currentState.is_loading_activities);
console.log(notificationFeed.currentState.aggregated_activities);

await notificationFeed.getNextPage();

This is what Jane's notification feed looks like after the above interactions (only relevant fields shown):

  • Three aggregated activity groups:
    • <activity id>-comment-2025-08-04
    • <activity id>-reaction-2025-08-04
    • <feed id>-follow-2025-08-04
  • notification_context has information about the activity/action that triggered the notification
    • Please note that notification_context field is only defined if you're using the built-in notification feed and create_notification_activity flag

When reading notifications, every aggregated activity group contains at most 100 activities (can be configured with aggregation group limit). The user_count field is also computed from the last n activities, defined by aggregation group limit.

If a group has more activities than the limit, user_count_truncated will be set to true, signaling that user_count may not be accurate. This enables creating notifications like "100+ people commented on your post". The activity_count field is always accurate, even if the group has more activities than the limit.

Example API response:

{
  aggregated_activities: [
    {
      activity_count: 1,
      user_count: 1,
      user_count_truncated: false,
      is_seen: true,
      is_read: false,
      group: "activity123-comment-2025-08-04",
      activities: [
        {
          type: "comment",
          user: {
            id: "eric",
            name: "Eric",
            // other User fields
          },
          notification_context: {
            trigger: {
              text: "Eric commented on your activity",
              type: "comment",
            },
            target: {
              user_id: "jane",
              type: "post",
              text: "As earnestly shameless elsewhere defective estimable fulfilled of",
              id: "a0668408-0eb9-4906-a1cf-be79f988051d",
              attachments: [
                {
                  type: "image",
                  image_url: "https://...",
                },
              ],
            },
          },
          // Other activity fields
        },
      ],
    },
    {
      activity_count: 1,
      user_count: 1,
      is_seen: true,
      is_read: true,
      group: "activity123-reaction-2025-08-04",
      activities: [
        {
          type: "reaction",
          user: {
            id: "eric",
            name: "Eric",
          },
          notification_context: {
            target: {
              id: "8966090a-30bf-4fe2-b8bc-b0fe36200e56",
              user_id: "jane",
              type: "post",
              text: "Ask too matter formed county wicket oppose talent",
            },
            trigger: {
              type: "reaction",
              text: "Eric reacted to your activity",
            },
          },
        },
      ],
    },
    {
      activity_count: 1,
      user_count: 1,
      is_seen: false,
      is_read: false,
      group: "jane-follow-2025-08-04",
      activities: [
        {
          type: "follow",
          user: {
            id: "eric",
            name: "Eric",
          },
          notification_context: {
            target: {
              id: "jane",
              name: "Jane",
            },
            trigger: {
              type: "follow",
              text: "Eric started following you",
            },
          },
        },
      ],
    },
    {
      activity_count: 1,
      user_count: 1,
      is_seen: false,
      is_read: false,
      group: "comment456-comment_reply-2025-08-04",
      activities: [
        {
          type: "comment_reply",
          user: {
            id: "charlie",
            name: "Charlie"
          },
          notification_context: {
            target: {
              id: "8966090a-30bf-4fe2-b8bc-b0fe36200e56",
              user_id: "alice",
              type: "post",
              text: "Ask too matter formed county wicket oppose talent",
              comment: {
                id: "comment456",
                user_id: "bob",
                comment: "Great post! I totally agree with this."
              }
            },
            trigger: {
              type: "comment_reply",
              text: "Charlie replied to your comment"
            }
          }
        }
      ]
    },
  ];
}

Push Notifications

For information on configuring push notifications, see Feed Group Push Configuration.

Notification status

If notification tracking is turned on for the feed group (track_seen / track_read), the server stamps is_seen and is_read directly on each aggregated activity group. This means you no longer need to compute read/seen status client-side — just use the boolean fields from the response.

For example, take the notification system on Facebook. If you click the notification icon, all notifications get marked as seen. However, an individual notification only gets marked as read when you click on it.

Server-side is_seen / is_read

Each aggregated activity group includes is_seen and is_read boolean fields when tracking is enabled:

{
  "aggregated_activities": [
    {
      "group": "activity123-comment-2025-08-04",
      "is_seen": true,
      "is_read": false,
      "activity_count": 1,
      "activities": [...]
    }
  ]
}

The is_seen and is_read fields are only present when track_seen / track_read are enabled on the feed group. When tracking is disabled, these fields are omitted from the response.

To check if a notification group is read or seen, simply read the fields:

const isRead = group.is_read;
const isSeen = group.is_seen;

The server uses a hybrid algorithm to determine these values:

  1. Check if the group ID is in the seen/read ID lists
  2. Fall back to timestamp comparison (updated_at < last_seen_at) for entries beyond the list cap

This makes the server-side fields more reliable than client-side computation, especially for feeds with many notifications. Client-side SDKs will update is_seen and is_read flags from notification WebSocket events.

Non-aggregated feeds: If aggregation is turned off but notification tracking is still enabled, is_seen and is_read are stamped on each individual activity. When aggregation is on, activities inside a group inherit the group's is_seen / is_read value.

Unread/unseen counts

The notification_status in the response also includes unread and unseen counts. These are computed from the last 1000 activities, aggregated into a maximum of 100 groups. This means unread/unseen counts will never exceed 100.

{
  "notification_status": {
    "unread": 12,
    "unseen": 0,
    "last_seen_at": "2025-08-04T12:00:00Z",
    "last_read_at": "2025-08-04T11:30:00Z",
    "seen_activities": [], // deprecated — use is_seen on each group instead
    "read_activities": ["activity123-reaction-2025-08-04"] // deprecated — use is_read on each group instead
  }
}

You can access notification_status (including unread and unseen) as follows:

const status = notificationFeed.state.getLatestValue().notification_status;
const unread = status?.unread ?? 0;
const unseen = status?.unseen ?? 0;

Legacy: read_activities / seen_activities arrays

Deprecated: The read_activities and seen_activities arrays in the notification_status response are deprecated. Use the is_read and is_seen fields on each aggregated activity group instead.

For backward compatibility, the notification_status response still includes read_activities and seen_activities ID lists. However, these lists are capped at 100 entries, which means they may be incomplete for feeds with many notifications. The server-side is_seen / is_read fields do not have this limitation.

Legacy: Client-side computation

Deprecated: The following client-side pattern is deprecated. Use the server-side is_read / is_seen fields instead.

Older SDK versions required computing read/seen status client-side using timestamps and ID lists:

// Deprecated — use group.is_read / group.is_seen instead
const isRead =
  (lastReadAt && group.updated_at.getTime() < lastReadAt.getTime()) ||
  readActivities.includes(group.group);
const isSeen =
  (lastSeenAt && group.updated_at.getTime() < lastSeenAt.getTime()) ||
  seenActivities.includes(group.group);

Marking notifications as seen

await notificationFeed.markActivity({
  // Mark all notifications as seen...
  mark_all_seen: true,
  // ...or only selected ones
  mark_seen: [
    /* group names to mark as seen */
  ],
});

Marking notifications as read

await notificationFeed.markActivity({
  // Mark all notifications as read...
  mark_all_read: true,
  // ...or only selected ones
  mark_read: [
    /* group names to mark as read */
  ],
});

Realtime events

Two events are emitted when mark operations are performed:

  • feeds.activity.marked — contains the IDs that were marked and any mark_all_* flags
  • feeds.notification_feed.updated — contains the updated feed with stamped is_seen / is_read values

Clients can subscribe to these realtime events to update the UI without re-fetching the feed.

Pagination

Pagination for notification (aggregated) feeds work the same way as it works for any other feed:

const feed = client.feed("user", "jack");

// First page
await feed.getOrCreate({
  limit: 10,
});

// Second page
await feed.getNextPage();

console.log(feed.state.getLatestValue().is_loading_activities);
// Truthy if feed has next page
console.log(feed.state.getLatestValue().next);
console.log(feed.state.getLatestValue().activities);
// Only if feed group has aggregation turned on
console.log(feed.state.getLatestValue().aggregated_activities);