// Create timeline feed
timeline := client.Feeds().Feed("timeline", "john")
_, err = timeline.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error getting/creating timeline feed:", err)
}
log.Println("Timeline feed created/retrieved successfully")
// Follow a user
_, err = client.Feeds().Follow(context.Background(), &getstream.FollowRequest{
Source: "timeline:john",
Target: "user:tom",
})
if err != nil {
log.Fatal("Error following user:", err)
}
log.Println("Successfully followed user:tom")
// Follow a stock
_, err = client.Feeds().Follow(context.Background(), &getstream.FollowRequest{
Source: "timeline:john",
Target: "stock:apple",
})
if err != nil {
log.Fatal("Error following stock:", err)
}
log.Println("Successfully followed stock:apple")
// Follow with more fields
_, err = client.Feeds().Follow(context.Background(), &getstream.FollowRequest{
Source: "timeline:john",
Target: "stock:apple",
PushPreference: getstream.PtrTo("all"),
Custom: map[string]any{
"reason": "investment",
},
})
if err != nil {
log.Fatal("Error following stock with custom fields:", err)
}
log.Println("Successfully followed stock:apple with custom fields")Follow and Unfollow
Follow
The source feed should have a group that has "following" activity selector enabled, for example the built-in timeline group. The target feed should have a group that has "current" activity selector enabled, for example the built-in user group.
For most use cases, prefer getOrCreateFollow over follow. It returns the existing follow row when the pair is already followed instead of erroring, so retries and double-clicks are safe. Use follow only when you specifically want a duplicate-follow to fail.
Trying to follow a feed that is already followed will result in an error. Use getOrCreateFollow for an idempotent single-follow call, or getOrCreateFollows for the batch variant.
You do not need to call getOrCreate on the source or target feed before following. If either feed does not exist yet, the feed is created automatically when you call follow. This applies to both the single follow and batch follow endpoints.
Auto-creating users
follow, followBatch, and getOrCreateFollows support an opt-in create_users flag (default: false).
- Server-side only: client-side callers that set
create_usersare ignored and still receiveuser not foundif referenced users are missing. - Missing users only:
create_userscreates users that do not exist yet; it does not update existing users. - Best-effort behavior: missing-user creation happens before follow creation and is not transactional with the follow write. A user can be created even if a later follow validation fails.
- User ID inference: when
create_usersistrue, user IDs are derived from the feed IDs insourceandtarget(for example,timeline:aliceinfersalice,user:bobinfersbob).
For a single follow call, set create_users on the request body.
_, err = client.Feeds().Follow(context.Background(), &getstream.FollowRequest{
Source: "timeline:alice",
Target: "user:bob",
CreateUsers: getstream.PtrTo(true),
})
if err != nil {
log.Fatal("Error creating follow with create_users:", err)
}For followBatch / getOrCreateFollows, set create_users only at the top level. follows[i].create_users is rejected.
response, err := client.Feeds().GetOrCreateFollows(context.Background(), &getstream.GetOrCreateFollowsRequest{
CreateUsers: getstream.PtrTo(true),
Follows: []getstream.FollowRequest{
{
Source: "timeline:alice",
Target: "user:bob",
},
{
Source: "timeline:alice",
Target: "user:charlie",
},
},
})
if err != nil {
log.Fatal("Error creating follows with create_users:", err)
}
fmt.Printf("Created follows: %d\n", len(response.Data.Created))Unfollow
When unfollowing a feed, all previous activities of that feed are removed from the timeline.
For most use cases, prefer getOrCreateUnfollow over unfollow. It silently no-ops when the follow does not exist instead of erroring, so retries and double-clicks are safe. Use unfollow only when you specifically want unfollowing a non-existent follow to fail.
Trying to unfollow a feed that is not followed, will result in an error. You can also use the getOrCreateUnfollow endpoint for an idempotent single-unfollow call, or getOrCreateUnfollows for the batch variant.
_, err = client.Feeds().Unfollow(context.Background(), "timeline:john", "user:tom")Update follow
You can update an existing follow relationship (for example to change push preference, custom data, or on server-side the follower role):
The endpoint performs a partial update: only the fields you include in the request are changed, and each of those fields is completely overwritten.
_, err = client.Feeds().UpdateFollow(context.Background(), &getstream.UpdateFollowRequest{
Source: "timeline:" + sourceFeedId,
Target: "user:" + targetFeedId,
PushPreference: getstream.PtrTo("none"),
FollowerRole: getstream.PtrTo("my_custom_feed_follower_role"),
Custom: map[string]any{
"note": "Updated follow",
},
})Querying Follows
ctx := context.Background()
// Create timeline feed
myTimeline := client.Feeds().Feed("timeline", "john")
_, err = myTimeline.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error creating timeline feed:", err)
}
// Query follows to check if we follow a list of feeds
response, err := client.Feeds().QueryFollows(ctx, &getstream.QueryFollowsRequest{
Filter: map[string]any{
"source_feed": "timeline:john",
"target_feed": map[string]any{
"$in": []string{"user:sara", "user:adam"},
},
},
})
if err != nil {
log.Fatal("Error querying follows:", err)
}
log.Printf("Follows: %+v", response.Data.Follows)
// Create user feed
userFeed := client.Feeds().Feed("user", "john")
_, err = userFeed.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error creating user feed:", err)
}
// Paginating through followers for a feed - first page
firstPage, err := client.Feeds().QueryFollows(ctx, &getstream.QueryFollowsRequest{
Filter: map[string]any{
"target_feed": "user:john",
},
Limit: getstream.PtrTo(20),
})
if err != nil {
log.Fatal("Error querying first page of follows:", err)
}
// Next page
secondPage, err := client.Feeds().QueryFollows(ctx, &getstream.QueryFollowsRequest{
Filter: map[string]any{
"target_feed": "user:john",
},
Limit: getstream.PtrTo(20),
Next: firstPage.Data.Next,
})
if err != nil {
log.Fatal("Error querying second page of follows:", err)
}
log.Printf("First page follows: %+v", firstPage.Data.Follows)
log.Printf("Second page follows: %+v", secondPage.Data.Follows)
// Filter by source - feeds that I follow
sourceFollows, err := client.Feeds().QueryFollows(ctx, &getstream.QueryFollowsRequest{
Filter: map[string]any{
"source_feed": "timeline:john",
},
Limit: getstream.PtrTo(20),
})
if err != nil {
log.Fatal("Error querying source follows:", err)
}
log.Printf("Source follows: %+v", sourceFollows.Data.Follows)Follows Queryable Built-In Fields
| name | type | description | supported operations | example |
|---|---|---|---|---|
source_feed | string or list of strings | The feed ID that is following | $in, $eq | { source_feed: { $eq: 'messaging:general' } } |
target_feed | string or list of strings | The feed ID being followed | $in, $eq | { target_feed: { $in: [ 'sports:news', 'tech:updates' ] } } |
status | string or list of strings | The follow status | $in, $eq | { status: { $in: [ 'accepted', 'pending', 'rejected' ] } } |
created_at | string, must be formatted as an RFC3339 timestamp | The time the follow relationship was created | $eq, $gt, $gte, $lt, $lte | { created_at: { $gte: '2023-12-04T09:30:20.45Z' } } |
Follow Requests
Some apps require the user's approval for following them.
ctx := context.Background()
saraFeed := client.Feeds().Feed("user", "sara")
_, err = saraFeed.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
Data: &getstream.FeedInput{
Visibility: getstream.PtrTo("followers"),
},
UserID: getstream.PtrTo("sara"),
})
if err != nil {
log.Fatal("Error creating sara feed:", err)
}
adamTimeline := client.Feeds().Feed("timeline", "adam")
_, err = adamTimeline.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
UserID: getstream.PtrTo("adam"),
})
if err != nil {
log.Fatal("Error creating adam timeline feed:", err)
}
// Create follow request from adamTimeline to saraFeed
followRequest, err := client.Feeds().Follow(ctx, &getstream.FollowRequest{
Source: "timeline:adam",
Target: "user:sara",
})
if err != nil {
log.Fatal("Error creating follow request:", err)
}
fmt.Printf("Follow request status: %s\n", followRequest.Data.Follow.Status)
// Accept follow request and set follower's role
_, err = client.Feeds().AcceptFollow(ctx, &getstream.AcceptFollowRequest{
Source: "timeline:adam",
Target: "user:sara",
FollowerRole: getstream.PtrTo("feed_member"),
})
if err != nil {
log.Fatal("Error accepting follow request:", err)
}
// Reject follow request
_, err = client.Feeds().RejectFollow(ctx, &getstream.RejectFollowRequest{
Source: "timeline:adam",
Target: "user:sara",
})
if err != nil {
log.Fatal("Error rejecting follow request:", err)
}Push Preferences on Follow
Understanding the difference between push_preference, skip_push and create_notification_activity:
When following a feed, you can set push_preference to control push notifications for future activities from that feed:
all- Receive push notifications for all activities from the followed feednone(default) - Don't receive push notifications for activities from the followed feed
The skip_push controls whether the follow action itself triggers a notification.
The create_notification_activity controls whether the follow action creates an activity on the source feed author's notification feed.
Note: You usually don't want to set skip_push and create_notification_activity true at the same time, for more information see the Push Overview page
// Scenario 1: Follow a user and receive notifications for their future activities
await timeline.follow("user:alice", {
push_preference: "all", // You'll get push notifications for Alice's future posts
});
// Scenario 2: Follow a user but don't get notifications for their activities
await timeline.follow("user:bob", {
push_preference: "none", // You won't get push notifications for Bob's future posts
});
// Scenario 3: Follow a user silently
await timeline.follow("user:charlie", {
skip_push: true, // Charlie won't get a "you have a new follower" notification
push_preference: "all", // But you'll still get notifications for Charlie's future posts
});
// Scenario 4: Silent follow with no future notifications
await timeline.follow("user:diana", {
skip_push: true, // Diana won't know you followed her
push_preference: "none", // And you won't get notifications for her posts
});
// Scenario 5: Follow a user and create notification activity for Charile
await timeline.follow("user:charlie", {
skip_push: true, // Charlie won't get a "you have a new follower" notification
create_notification_activity: true, // Charlie's notification feed will have a new activity
push_preference: "all", // But you'll still get notifications for Charlie's future posts
});Built-in fields of follows
FollowResponse
| Name | Type | Description | Constraints |
|---|---|---|---|
created_at | number | When the follow relationship was created | Required |
custom | object | Custom data for the follow relationship | - |
follower_role | string | Role of the follower (source user) in the follow relationship | Required |
push_preference | string (all, none) | Push preference for notifications. One of: all, none | Required |
request_accepted_at | number | When the follow request was accepted | - |
request_rejected_at | number | When the follow request was rejected | - |
source_feed | FeedResponse | Source feed object | Required |
status | string (accepted, pending, rejected) | Status of the follow relationship. One of: accepted, pending, rejected | Required |
target_feed | FeedResponse | Target feed object | Required |
updated_at | number | When the follow relationship was last updated | Required |
Follow Suggestions
Stream provides intelligent follow suggestions to help users discover feeds they might want to follow based on their activity and social graph.
Note: The maximum limit for follow suggestions is 50. If a higher limit is requested, it will be automatically capped at 50.
// Get follow suggestions for a user
suggestions, err := client.Feeds().GetFollowSuggestions(context.Background(), &getstream.GetFollowSuggestionsRequest{
FeedGroupId: "user",
Limit: getstream.PtrTo(10),
UserId: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error getting follow suggestions:", err)
}
fmt.Printf("Algorithm used: %s\n", suggestions.Data.AlgorithmUsed)
fmt.Printf("Duration: %s\n", suggestions.Data.Duration)
for _, suggestion := range suggestions.Data.Suggestions {
fmt.Printf("Suggested feed: %s\n", suggestion.Fid)
fmt.Printf("Name: %s\n", suggestion.Name)
fmt.Printf("Description: %s\n", suggestion.Description)
fmt.Printf("Follower count: %d\n", suggestion.FollowerCount)
fmt.Printf("Recommendation score: %.2f\n", suggestion.RecommendationScore)
fmt.Printf("Reason: %s\n", suggestion.Reason)
fmt.Printf("Algorithm scores: %+v\n", suggestion.AlgorithmScores)
}Response Fields
The follow suggestions response includes:
suggestions: Array of suggested feeds to followfeed: Feed identifiername: Feed namedescription: Feed descriptionvisibility: Feed visibility settingmember_count: Number of membersfollower_count: Number of followersfollowing_count: Number of feeds this feed followscreated_at: When the feed was createdupdated_at: When the feed was last updatedrecommendation_score: Combined recommendation score (0-1)reason: Human-readable reason for the suggestionalgorithm_scores: Individual algorithm scores
algorithm_used: The algorithm used to generate suggestionsduration: Request processing time
Algorithm Types
Stream's follow suggestions use a sophisticated multi-algorithm approach with weighted scoring:
-
popularity(Weight: 0.3): Based on follower count and engagement- Calculates normalized follower count relative to the most popular feed in your app
- Score = min(follower_count / max_follower_count, 1.0)
- Helps surface trending and popular content
-
friend-of-friend(Weight: 0.7): Based on social connections and mutual follows- Analyzes how many of your followed feeds also follow the suggested feed
- Score = mutual_follows / your_total_follows
- Leverages social proof and network effects
-
combined: Uses multiple algorithms with weighted scoring- Final score = (popularity_score × 0.3 + friend_of_friend_score × 0.7) / total_weight
- Provides balanced recommendations combining popularity and social relevance
Note: Additional algorithms will be added in future releases to provide even more sophisticated recommendations.
Scoring System
The recommendation system uses a sophisticated scoring mechanism:
- Individual Algorithm Scores: Each algorithm calculates a score from 0.0 to 1.0
- Weighted Combination: Scores are combined using configurable weights
- Normalization: Final scores are normalized to ensure fair comparison
- Filtering: Only feeds with positive combined scores are included
- Ranking: Results are sorted by combined score in descending order
Features
- Excludes feeds already followed by the user
- Excludes user's own feeds
- Sophisticated algorithm to find feeds to follow
Idempotent follow & unfollow
getOrCreateFollow / getOrCreateUnfollow are the idempotent single-pair variants of follow / unfollow. Calling them on a pair that is already followed (or already not followed) does not error — the response's created / deleted boolean tells you whether this call actually changed state.
getOrCreateFollows / getOrCreateUnfollows are the batch variants, accepting up to 100 pairs per call. Their response includes a created list with the subset of follows newly inserted by this call.
// Idempotent follow: returns the existing follow if one exists, otherwise creates it.
const { follow, created } = await client.feeds.getOrCreateFollow({
source: "timeline:alice",
target: "user:bob",
});
console.log(created ? "newly created" : "already existed", follow);
// Idempotent unfollow: no error if the follow does not exist.
const { follow: removed, deleted } = await client.feeds.getOrCreateUnfollow({
source: "timeline:alice",
target: "user:bob",
});
console.log(deleted ? "removed" : "was not following", removed);Batch follow & unfollow
getOrCreateFollows/getOrCreateUnfollows endpoints allow creating a maximum of 100 follow/unfollow at once.
These are idempotent endpoints (as opposed to follow and unfollow), trying to follow/unfollow a feed that's already/not yet followed won't cause errors.
ctx := context.Background()
// Batch create follows
response, err := client.Feeds().GetOrCreateFollows(ctx, &getstream.GetOrCreateFollowsRequest{
Follows: []getstream.FollowRequest{
{
Source: "timeline:john",
Target: "user:tom",
// Optional
PushPreference: getstream.PtrTo("all"),
Custom: map[string]any{
"reason": "investment",
},
},
{
Source: "timeline:john",
Target: "stock:apple",
},
},
})
if err != nil {
log.Fatal("Error creating follows:", err)
}
fmt.Printf("Created follows: %d\n", len(response.Data.Created))
fmt.Printf("Total follows: %d\n", len(response.Data.Follows))
// Batch remove follows
unfollowResponse, err := client.Feeds().GetOrCreateUnfollows(ctx, &getstream.GetOrCreateUnfollowsRequest{
Follows: []getstream.FollowPair{
{
Source: "timeline:john",
Target: "user:tom",
},
},
})
if err != nil {
log.Fatal("Error removing follows:", err)
}
fmt.Printf("Follows that were removed: %d\n", len(unfollowResponse.Data.Follows))