Skip to content

Intro & Defaults

Feed groups are templates that define how different feeds behave in your application. The configuration options let you create feeds that work differently depending on your use case.

By adjusting settings like ranking, aggregation, activity selectors, and processors, you can tailor each feed group to serve specific purposes in your app.

Info:

Note that any write operation to feed groups/views can take up to 30 seconds to propagate to all API nodes.

Built-in groups

There are several feed groups setup by default.

Group Description
user A feed setup for the content a user creates. Typically you add activities here when someone writes a post
timeline The timeline feed is used when you're following. So if user Charlie is following John, timeline:charlie would follow user:john
foryou A version of the timeline feed that adds popular content, and prioritizes popularity over recency
notification A notification feed. Think of the bell icon you see in most apps
story A feed set up for users to post story activities (activities with expiration data)
stories A timeline feed which can be used to follow other users' stories.

You can update the default feed group configurations or create your own feed groups.

Create feed groups

Here's how to create a feed group using the API:

use GetStream\GeneratedModels\CreateFeedGroupRequest;
use GetStream\GeneratedModels\ActivitySelectorConfig;
use GetStream\GeneratedModels\RankingConfig;
use GetStream\GeneratedModels\ActivityProcessorConfig;
use GetStream\GeneratedModels\ActivityFilterConfig;

// Create feed group request
$request = new CreateFeedGroupRequest(
    id: "myid",
    activitySelectors: [
        new ActivitySelectorConfig(type: "following")
    ],
    ranking: new RankingConfig(type: "recency"),
    activityProcessors: [
        new ActivityProcessorConfig(type: "text_interest_tags")
    ],
    activityFilter: new ActivityFilterConfig(
        excludeOwnerActivities: true
    ),
    custom: (object) [
        "description" => "My custom feed group"
    ]
);

// Create the feed group
$response = $feedsClient->createFeedGroup($request);
Info:

Applications can't have more than 100 feed groups

Info:

Alternatively you can use the getOrCreateFeedGroup endpoint to create and apply settings, or return the existing group.

Exclude owner activities

Set activity_filter.exclude_owner_activities on a feed group to hide the owner's own activities when reading that owner's feed.

Each feed has a created_by_id. When this flag is on, the filter drops activities whose user_id matches the feed's created_by_id, so the feed owner's own posts are hidden from their own feed, but still visible to followers who see the fanout in their own feeds.

  • Reading user:jimmy hides activities where user_id is jimmy.
  • Reading user:amy still shows activities fanned out from user:jimmy (and hides Amy's own).

The flag is set once at the feed group level (not per view) and applied automatically per feed. It works on the user, timeline, and foryou built-in groups, and on custom groups.

Where it's not valid: the API rejects activity_filter on:

  • notification, story, or stories built-in groups
  • feed groups that also set aggregation, notification, or stories config

For self-notifications, no configuration is needed. The Feeds API already skips creating notifications when the actor is the activity owner.

Update semantics: feed group updates use full-replacement semantics. Omitting activity_filter on an UpdateFeedGroup call clears the stored value, so include it on every update if you want to preserve it.

When flag changes take effect: changes apply on the next read. On ranked feeds (for example foryou, or groups with a ranking config), updating the feed group invalidates outstanding pagination tokens, so clients mid-pagination may need to restart from the first page.

Overview of the feed group model

FeedGroupResponse fields:

Field Description
id Feed group identifier.
default_visibility Visibility assigned to feeds created in this group (public, visible, followers, members, private). Applies at creation only.
default_follower_role Role new followers of feeds in this group receive: a built-in (feed_follower, feed_member_viewer) or a custom role. Unset behaves as feed_follower.
activity_selectors Activity source selection rules.
activity_processors Enrichment and processing pipeline configuration.
ranking Ranking formula/configuration for feed ordering.
aggregation Aggregation settings for grouped reads.
activity_filter Read-time filters, including exclude_owner_activities.
notification Notification configuration for notification feeds.
activity_marks Per-activity is_seen / is_read tracking for flat content feeds.
stories Stories configuration for stories feeds.
push_notification Push notification behavior configuration.
custom Custom metadata object.
created_at Feed group creation timestamp.
updated_at Feed group last updated timestamp.
deleted_at Deletion timestamp (present when soft-deleted).

Activity marks on content feeds

Use activity_marks when a flat timeline or other content feed needs exact per-activity is_seen / is_read values without turning the group into a notification feed:

use GetStream\GeneratedModels;

$response = $feedsClient->createFeedGroup(
    new GeneratedModels\CreateFeedGroupRequest(
        id: "ranked_timeline",
        activityMarks: new GeneratedModels\ActivityMarksConfig(
            trackSeen: true,
            trackRead: true
        ),
        activitySelectors: [
            new GeneratedModels\ActivitySelectorConfig(type: "following")
        ]
    )
);

Each activity returned from the feed includes the enabled fields. Mark activities with mark_seen or mark_read and explicit activity IDs. mark_all_seen and mark_all_read are notification-only and are rejected on content feeds, because older content can enter a personalized feed later.

$timeline = $feedsClient->feed("ranked_timeline", $userId);
$timeline->markActivity(new GeneratedModels\MarkActivityRequest(
    markSeen: [$activityId],
    userID: $userId
));

activity_marks cannot be combined with notification, aggregation, or stories configuration. A request may contain at most 100 unique activity IDs. The service retains approximately the newest 1000 marks per user and feed.

Default follower role

Every follow carries a follower_role that decides what that follower may do. By default a new follow gets feed_follower. To give followers a different tier you would otherwise have to call updateFollow after each follow, usually from a feeds.follow.created webhook, which leaves a window where the follower holds permissions you did not intend.

default_follower_role on the feed group removes that round trip. Set it once, and every new follow of a feed in the group starts with that role.

The motivating case is a follower tier you don't have to correct after the fact: a group whose followers get feed_member_viewer can read, react and vote, but not comment, from the moment the follow is accepted.

use GetStream\GeneratedModels\CreateFeedGroupRequest;

$createRequest = new CreateFeedGroupRequest(
    id: "premium",
    defaultFollowerRole: "feed_member_viewer"
);

$response = $feedsClient->createFeedGroup($createRequest);

Setting it is server-side only, like every feed group write. Allowed values are the two built-in follower roles, feed_follower and feed_member_viewer, plus any custom role you created for your app. Anything else, including reserved names such as admin or feed_member, is rejected when you write the feed group rather than being silently ignored later. Leaving it unset behaves exactly as before the field existed.

When the role is applied

The default applies once the follow is accepted, which for most visibility levels is the moment it is created.

On a followers-visibility feed a follow starts as pending, and a pending follower deliberately holds no follower permissions until the owner approves. The group default is applied at approval instead, whether the owner accepts the request directly or a visibility change auto-approves the backlog. That way an unapproved follower never receives the role early.

An explicit follower_role on acceptFollow or updateFollow always wins over the group default.

Existing accepted follows are never rewritten, so changing the default does not change the permissions of anyone who already follows a feed in the group. A follow still pending when you change it is a different case: it picks up whatever the default is at the moment it is approved, not the one in place when it was requested.

Check the visibility of the feeds in the group

feed_member_viewer is not a restriction-only control, and a group default applies to every feed in the group regardless of its visibility.

On public, visible and followers feeds it is a strict narrowing of feed_follower. On members and private feeds it widens a follower's access instead: it grants read access to the feed and its activities that feed_follower does not have. That is the subscriber-tier case, and it is deliberate: the follow itself is what admits a paying read-only audience to a restricted feed. It also means a group default of feed_member_viewer gives followers of a members or private feed in that group more than they would otherwise get.

See Follower permission tiers for the per-level breakdown.

List feed groups

To list existing feed groups and their configurations:

// List all feed groups (excluding soft-deleted ones)
$response = $feedsClient->listFeedGroups(false);

// List all feed groups including soft-deleted ones
$response = $feedsClient->listFeedGroups(true);

Activity Ranking

When ranking activities you can specify a ranking formula.

$createResponse = $feedsClient->createFeedGroup(
    new GeneratedModels\CreateFeedGroupRequest(
        id: "mytimeline",
        defaultVisibility: 'public',
        ranking: new GeneratedModels\RankingConfig(
            type: 'expression',
            score: 'decay_linear(time) * popularity'
        ),
        activitySelectors: [
            new GeneratedModels\ActivitySelectorConfig(
                type: 'following'
            )
        ]
    )
);

For you Feeds

Many apps want to have a "for you" or personalized feed. There are a couple benefits to a personalized feed:

  • Works well even if your users don't spend much time setting up follows
  • Can be a mechanism to discover new content or things to follow

Stream offers a few built-in methods to create a for you feed and gives you the API access to do more advanced customization if needed.

This next example is a bit more complicated. It uses an activity processor to add topic data to activities, activity selectors to pull in content from different sources, and ranks content you're likely to engage with higher in the feed.

use GetStream\GeneratedModels\CreateFeedGroupRequest;
use GetStream\GeneratedModels\ActivitySelectorConfig;
use GetStream\GeneratedModels\RankingConfig;
use GetStream\GeneratedModels\ActivityProcessorConfig;
use GetStream\GeneratedModels\GetOrCreateFeedRequest;

// Create feed group with activity processors, activity selectors, and ranking
$createFeedGroupRequest = new CreateFeedGroupRequest(
    id: "mytimeline",
    // Run the activity processors to analyse topics for text & images
    activityProcessors: [
        new ActivityProcessorConfig(type: "text_interest_tags"),
        new ActivityProcessorConfig(type: "image_interest_tags"),
    ],
    // Activity selectors change which activities are included in the feed
    // The default "following" selectors gets activities from the feeds you follow
    // The "popular" activity selectors includes the popular activities
    // And "interest" activities similar to activities you've engaged with in the past
    // You can use multiple selectors in 1 feed
    activitySelectors: [
        new ActivitySelectorConfig(type: "popular"),
        new ActivitySelectorConfig(type: "following"),
        new ActivitySelectorConfig(type: "interest"),
    ],
    // Rank for a user based on interest score
    // This calculates a score 0-1.0 of how well the activity matches the user's prior interest
    ranking: new RankingConfig(
        type: "interest",
        score: "decay_linear(time) * interest_score * decay_linear(popularity)"
    )
);

// Create the feed group
$response = $feedsClient->createFeedGroup($createFeedGroupRequest);

// Create and get the feed for a specific user
$forYouFeed = $feedsClient->feed("mytimeline", "thierry");
$feedRequest = new GetOrCreateFeedRequest(userID: "thierry");
$feedResponse = $forYouFeed->getOrCreateFeed($feedRequest);

Aggregation & Notification Feeds

Aggregation groups similar activities together, which is useful for notification feeds and reducing noise.

Aggregation Format

You can create your own aggregated feeds:

// Create notification feed group with aggregation and tracking
$request = new GeneratedModels\CreateFeedGroupRequest(
    id: "myid",
    defaultVisibility: 'public',
    // Group by activity type and day
    aggregation: new GeneratedModels\AggregationConfig(
        format: '{{ type }}-{{ time.strftime("%Y-%m-%d") }}'
    ),
    // Enable notification tracking
    notification: new GeneratedModels\NotificationConfig(
        trackRead: true,
        trackSeen: true
    )
);

// Create the feed group
$response = $feedsClient->createFeedGroup($request);

Notification Feed Example

The built-in notification feed comes with the necessary configurations:

use GetStream\GeneratedModels\GetOrCreateFeedRequest;

$notificationFeed = $feedsClient->feed("notification", "jane");

// Read notifications
$feedRequest = new GetOrCreateFeedRequest(
    limit: 20,
    userID: "jane"
);

$response = $notificationFeed->getOrCreateFeed($feedRequest);

// Access the aggregated activities
$notifications = $response->data->aggregatedActivities;

Marking Notifications as Read

use GetStream\GeneratedModels\MarkActivityRequest;

// Create notification feed
$notificationFeed = $feedsClient->feed("notification", "john");

// Mark all notifications as read
$markRequest = new MarkActivityRequest(
    markAllRead: true,
    userID: "john"
);

$response = $notificationFeed->markActivity($markRequest);

// Or mark only selected notifications as read
$markSelectedRequest = new MarkActivityRequest(
    markRead: [
        // group names to mark as read
    ],
    userID: "john"
);

$response = $notificationFeed->markActivity($markSelectedRequest);

Updating feed groups

It's possible to update any custom or built-in feed group:

Updates replace every field. Omitting custom, ranking, aggregation, notification, activity_marks, push_notification, stories, activity_processors or activity_filter clears the stored value, so send every field you want to keep. activity_selectors, default_visibility and default_follower_role are the exceptions: omitting any of them leaves the stored value in place. default_follower_role therefore cannot be cleared once set, only changed to another role.

Changing default_visibility does not affect feeds that already exist. Every feed stores its own visibility, copied from the group default at the moment the feed was created, so the new default only applies to feeds created from then on. To move an existing feed to a different level, change it per feed with Changing Feed Visibility, which also reconciles that feed's follows.

use GetStream\GeneratedModels\UpdateFeedGroupRequest;

// Create update request
$updateRequest = new UpdateFeedGroupRequest(
    // Fields to update
    // activityProcessors: [...],
    // activitySelectors: [...],
    // ranking: new RankingConfig(...),
    // custom: (object) [...]
);

// Update the feed group
$response = $feedsClient->updateFeedGroup("myid", $updateRequest);

Deleting feed groups

Deleting feed groups will not cascade delete associated resources. It will make feeds within the feed group unreadable but any fanout that has happened before deletion will stay in effect.

If you need a clean slate during development it is advised to create a new feed group instead.

// Delete the feed group (soft delete by default)
$response = $feedsClient->deleteFeedGroup("mytimeline", false);

// For hard delete, pass true as the second parameter
// $response = $feedsClient->deleteFeedGroup("mytimeline", true);