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.

// 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")

Trying to follow a feed that is already followed will result in an error. You can also use the getOrCreateFollows endpoint that provides an idempotent batch follow mechanism.

Unfollow

When unfollowing a feed, all previous activities of that feed are removed from the timeline.

Trying to unfollow a feed that is not followed, will result in an error. You can also use the getOrCreateUnfollows endpoint that provides an idempotent batch unfollow mechanism.

_, err = client.Feeds().Unfollow(context.Background(), "timeline:john", "user:tom")

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

nametypedescriptionsupported operationsexample
source_feedstring or list of stringsThe feed ID that is following$in, $eq{ source_feed: { $eq: 'messaging:general' } }
target_feedstring or list of stringsThe feed ID being followed$in, $eq{ target_feed: { $in: [ 'sports:news', 'tech:updates' ] } }
statusstring or list of stringsThe follow status$in, $eq{ status: { $in: [ 'accepted', 'pending', 'rejected' ] } }
created_atstring, must be formatted as an RFC3339 timestampThe 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 feed
  • none (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

NameTypeDescriptionConstraints
created_atnumberWhen the follow relationship was createdRequired
customobjectCustom data for the follow relationship-
follower_rolestringRole of the follower (source user) in the follow relationshipRequired
push_preferencestring (all, none)Push preference for notificationsRequired
request_accepted_atnumberWhen the follow request was accepted-
request_rejected_atnumberWhen the follow request was rejected-
source_feedFeedResponseSource feed objectRequired
statusstring (accepted, pending, rejected)Status of the follow relationshipRequired
target_feedFeedResponseTarget feed objectRequired
updated_atnumberWhen the follow relationship was last updatedRequired

Follow Suggestions

Stream provides intelligent follow suggestions to help users discover feeds they might want to follow based on their activity and social graph.

// 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 follow
    • feed: Feed identifier
    • name: Feed name
    • description: Feed description
    • visibility: Feed visibility setting
    • member_count: Number of members
    • follower_count: Number of followers
    • following_count: Number of feeds this feed follows
    • created_at: When the feed was created
    • updated_at: When the feed was last updated
    • recommendation_score: Combined recommendation score (0-1)
    • reason: Human-readable reason for the suggestion
    • algorithm_scores: Individual algorithm scores
  • algorithm_used: The algorithm used to generate suggestions
  • duration: 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:

  1. Individual Algorithm Scores: Each algorithm calculates a score from 0.0 to 1.0
  2. Weighted Combination: Scores are combined using configurable weights
  3. Normalization: Final scores are normalized to ensure fair comparison
  4. Filtering: Only feeds with positive combined scores are included
  5. 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

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.FollowInput{
    {
      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.FollowInput{
    {
      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))