V2 to V3 Code Migration
The V2 to V3 Migration guide moves your data: live traffic replication mirrors every Feeds v2 write into Feeds v3, and the backfill copies your history. This guide covers the other half, moving your code: what each Feeds v2 call becomes in Feeds v3, and the order in which to switch your application over without losing or duplicating writes.
The examples use the Node.js SDKs: getstream for Feeds v2 and @stream-io/node-sdk for Feeds v3. The Feeds v3 server SDKs share method and field names, so the same mapping applies to the other languages; see each SDK's pages in the sidebar.
Switch over in four steps
Live traffic replication keeps running until you complete the migration. The safe order is therefore to move reads first and writes last, all at once:
- Writes go to Feeds v2. Replication mirrors them into Feeds v3. Your application is unchanged.
- Shadow reads from Feeds v3. Your application still serves Feeds v2 responses, and in the background also reads the same data from Feeds v3 and compares the two. This is where you find mapping problems and read-path bugs before any user sees them.
- Serve reads from Feeds v3. Writes still go to Feeds v2 and are replicated.
- Switch writes to Feeds v3 and stop writing to Feeds v2, in the same deploy. Your application now runs entirely on Feeds v3. Complete the migration in the Dashboard.
flowchart LR
A["1. Write v2<br/>read v2"] --> B["2. Write v2<br/>read v2, shadow-read v3"]
B --> C["3. Write v2<br/>read v3"]
C --> D["4. Write v3<br/>read v3"]Never write to both versions at the same time. A write sent to Feeds v2 is replicated into Feeds v3, so the same write sent to Feeds v3 as well is applied twice. Port every write path, then switch them together in step 4, never one feature at a time.
Put the ported read paths behind one flag (steps 2 and 3) and the ported write paths behind another (step 4). Nothing changes for your users until you flip a flag, and flipping it back is your rollback.
What to expect at each step
Step 2, shadow reads. Expect these differences; they are not bugs:
- Response shape. Feeds v3 returns
user,type,feedsandcustominstead ofactor,verb,toand top-level custom fields, reactions come back asreaction_groups,own_reactionsandlatest_reactions, and comments are their own objects (see the tables below). - Mapped ids. If your mapping sets
idorforeign_id, the Feeds v3 activity id is the mapped value and the Feeds v2 id is incustom.original_id. Without that mapping, activities and comments keep their Feeds v2 ids. - Older data formats. Fields your mapping does not cover arrive in
custom. If your app changed its activity format over time, older activities keep their old fields there. - Repeat reactions. Feeds v3 holds one reaction of each type per user on an activity, so a count can be lower where a user reacted twice in Feeds v2.
Step 3, reads from Feeds v3.
- Your stored ids keep working. Activity and comment ids carry over (unless your mapping sets
idorforeign_id, in which case look activities up by the mapped id). - Replication is not instant. A user who writes and immediately reloads reads from Feeds v3 before the replicated write has landed. Show a user's own new post or comment optimistically in the UI instead of re-reading it.
Step 4, writes to Feeds v3.
- New activities get Feeds v3 ids, while existing ones keep theirs, so code that parses or compares ids must accept both.
- A Feeds v2 write made just before the switch can still be on its way through replication and land shortly after your first Feeds v3 writes. Activities are upserted, so the worst case is one late edit on an object edited in that same moment.
Client and users
import stream from "getstream";
const client = stream.connect(apiKey, apiSecret, appId);
await client
.user("jane")
.getOrCreate({ name: "Jane", profileImage: "https://..." });
const token = client.createUserToken("jane");import { StreamClient } from "@stream-io/node-sdk";
const client = new StreamClient(apiKey, apiSecret);
await client.upsertUsers([{ id: "jane", name: "Jane", image: "https://..." }]);
const token = client.generateUserToken({ user_id: "jane" });upsertUsers replaces the whole user. To change some fields, as user(id).update did in Feeds v2, use client.updateUsersPartial({ users: [{ id, set: { name } }] }).
Users in Feeds v3 are shared with Stream Chat and Video and have first-class name and image fields; anything else goes in custom. See Users and Authentication.
Feeds v2 recommended a backend-only integration. Feeds v3 also works from the client with a user token, so you can move reads into your apps later; nothing in this guide requires it.
Feed groups
Feed groups are created with client.feeds.createFeedGroup. Aggregated and notification feeds are no longer separate feed types: aggregation and notification tracking are settings on a feed group. See Feed Groups, Aggregation and Notification Feeds.
In Feeds v2 you addressed a feed with client.feed(group, id). In Feeds v3 most writes name feeds by id ("user:jane") in the request, and client.feeds.feed(group, id) returns a feed object for reads.
Activities
Fields
| Feeds v2 | Feeds v3 |
|---|---|
actor: "SU:jane" |
user_id: "jane" |
verb |
type |
object |
no equivalent: keep the value in custom, or reference a collection with collection_refs |
foreign_id |
id (set your own activity id) |
time |
set by Feeds v3 when the activity is created |
to: ["hashtag:hiking", "notification:bob"] |
feeds: ["user:jane", "hashtag:hiking", "notification:bob"] |
| custom fields at the top level | custom: { ... } |
| images in a custom field | attachments: [{ type: "image", image_url }] |
object: "SO:trail:abc" (collection reference) |
collection_refs: ["trail:abc"] |
object: "SA:<activity id>" (reshare) |
parent_id: "<activity id>" |
mentions as SU: references |
mentioned_user_ids: ["bob"] |
Adding an activity
await client.feed("user", "jane").addActivity({
actor: client.user("jane").ref(),
verb: "post",
object: "SO:trail:abc",
foreign_id: "post:42",
time: new Date().toISOString(),
text: "Made it to the hut #sunrise @bob",
attachments: { images: ["https://.../1.jpg"] },
to: ["hashtag:sunrise", "notification:bob"],
});await client.feeds.addActivity({
id: "post:42",
user_id: "jane",
type: "post",
feeds: ["user:jane", "hashtag:sunrise", "notification:bob"],
text: "Made it to the hut #sunrise @bob",
attachments: [{ type: "image", image_url: "https://.../1.jpg" }],
collection_refs: ["trail:abc"],
mentioned_user_ids: ["bob"],
});addToMany becomes the same call: list every target in feeds. See Activities.
One activity in several feeds, or a follow
Feeds v2 apps often copied one activity into many feeds with to or addToMany. Which Feeds v3 model fits depends on what decides the list of feeds:
- The content decides it (a few feeds per activity): list them in
feeds. A post "I love cooking! #cooking #food" goes touser:jane,hashtag:cookingandhashtag:food. Readhashtag:cookingto see every cooking post, or have a timeline follow it. - The audience decides it (the list grows with your user count): write the activity once and let follows deliver it. An announcement goes to one feed, such as
announcement:all, and each user'snotification(ortimeline) feed follows that feed: one follow when the user signs up, and a one-offclient.feeds.getOrCreateFollowsbatch for existing users. Never list every user's feed on the activity.
An activity can be in at most 25 feeds by default, which the second case would quickly exceed. If your app lets a post carry many hashtags, cap them, since the author's own feed counts too.
Feeds that do not exist yet
Adding an activity (and following) creates any listed feed that does not exist yet, as long as its feed group exists and the part after the colon is an existing user's id: notification:bob is created for user bob. A feed whose id is not a user, such as hashtag:cooking or announcement:all, cannot be created this way, and the write fails with a "user not found" error. Create those feeds before the first write, with an owner:
await client.feeds.feed("hashtag", "cooking").getOrCreate({ user_id: "jane" });
// or many at once (up to 1000 per call)
await client.feeds.createFeedsBatch({
feeds: [
{ feed_group_id: "hashtag", feed_id: "cooking", created_by_id: "jane" },
],
});Feeds that already held data in Feeds v2 are created by the migration. For feeds your app creates later, such as a new hashtag, create the feed the first time your app sees it, or create the missing feeds when a write fails this way and retry the write. Do not call createFeedsBatch for every write: on a feed that already exists it overwrites the name, description, custom data and owner. Updating an activity's feeds never creates feeds: a missing one returns an error.
Limits
- An activity can be in at most 25 feeds by default (see above). Ask support if your app needs more.
- An activity can reference at most 10 collections.
- An activity or comment created without an
idgets a random UUID (v4).
If your v2 app used foreign_id as its own id, set id to that value in Feeds v3, and use the foreign_id mapping when you migrate so existing activities get the same ids.
Updating, removing and resharing
| Feeds v2 | Feeds v3 |
|---|---|
client.activityPartialUpdate({ foreign_id, time, set, unset }) |
client.feeds.updateActivityPartial({ id, set, unset, user_id }) |
client.updateActivities([...]) |
client.feeds.updateActivity({ id, ... }) (full replace) |
feed.removeActivity(id) or { foreign_id }, when the activity is in no other feed |
client.feeds.deleteActivity({ id }) |
feed.removeActivity(...), when the activity stays in other feeds |
client.feeds.updateActivityPartial({ id, set: { feeds: [...remaining] } }) |
addActivity({ verb: "share", object: "SA:<id>" }) |
client.feeds.addActivity({ type: "share", parent_id: "<id>", feeds, user_id }) |
In Feeds v2, an activity sent to to targets was copied into each feed, and removeActivity removed one copy. In Feeds v3 one activity belongs to all of its feeds, so decide what the Feeds v2 call meant: if it deleted a post (the usual case), use deleteActivity, which removes it from every feed; if it only took the activity out of one feed, update feeds.
Feeds v2 updated activities by foreign_id plus time. Feeds v3 updates them by id alone, so you no longer need to store the creation time to edit an activity.
Reactions, comments and bookmarks
Feeds v2 stored likes, comments, replies and bookmarks all as reactions of different kinds. Feeds v3 gives each its own API.
| Feeds v2 | Feeds v3 |
|---|---|
reactions.add("like", activityId, data, { userId }) |
client.feeds.addActivityReaction({ activity_id, type: "like", user_id, custom }) |
reactions.add("like", ..., { targetFeeds }) |
the same, with target_feeds (writes a reference activity into each feed, as in Feeds v2), or create_notification_activity: true to use the notification feed |
reactions.delete(reactionId) (a like) |
client.feeds.deleteActivityReaction({ activity_id, type, user_id }) |
reactions.add("comment", activityId, { text }, { userId }) |
client.feeds.addComment({ object_id: activityId, object_type: "activity", comment: text, user_id }) |
reactions.add or addChild("comment", ..., { targetFeeds }) (comment or reply) |
the same, with create_notification_activity: true |
reactions.addChild("comment", commentId, { text }) (reply) |
client.feeds.addComment({ parent_id: commentId, comment: text, user_id }) |
reactions.addChild("like", commentId) (like on a comment) |
client.feeds.addCommentReaction({ id: commentId, type: "like", user_id }) |
reactions.update(commentId, { text }) |
client.feeds.updateComment({ id, comment }) |
reactions.delete(commentId) |
client.feeds.deleteComment({ id }) |
reactions.delete(reactionId) (a like on a comment) |
client.feeds.deleteCommentReaction({ id: commentId, type: "like", user_id }) |
reactions.get(commentId) |
client.feeds.getComment({ id }) |
reactions.add("bookmark", activityId) (or your own kind name) |
client.feeds.addBookmark({ activity_id, user_id }) |
reactions.delete(bookmarkId) |
client.feeds.deleteBookmark({ activity_id, user_id }) |
reactions.filter({ user_id, kind: "bookmark" }) |
client.feeds.queryBookmarks({ filter: { user_id } }) |
reactions.filter({ activity_id, kind: "comment" }) |
client.feeds.getComments({ object_id, object_type: "activity", depth }) |
Two differences to plan for:
- Reactions are keyed by user and type. You delete a Feeds v3 activity reaction by
activity_id,typeanduser_id, not by a reaction id. If your app stored Feeds v2 reaction ids to delete likes, use the activity id and the user instead. - One reaction of a type per user. A Feeds v3 user holds at most one
likeon an activity; adding it again does not create a second one. Passenforce_unique: trueif a user should hold only one reaction type at a time, such as switching fromliketolove.
Comment ids carry over from Feeds v2 (the reaction id becomes the comment id), so stored comment ids keep working.
Notifications created by Feeds v3 (create_notification_activity, mentions and follows) are never created for a user's own action, so code that skipped notifying users about themselves can drop that check. target_feeds is different: it writes an ordinary activity into the listed feeds, including the user's own notification feed if you list it. See Reactions, Comments and Bookmarks.
Follows
| Feeds v2 | Feeds v3 |
|---|---|
client.feed("timeline", "jane").follow("user", "bob") |
client.feeds.follow({ source: "timeline:jane", target: "user:bob" }) |
feed.unfollow("user", "bob") |
client.feeds.unfollow({ source: "timeline:jane", target: "user:bob" }) |
feed.unfollow("user", "bob", { keepHistory: true }) |
client.feeds.unfollow({ source, target, keep_history: true }) |
client.followMany([{ source, target }]) |
client.feeds.getOrCreateFollows({ follows: [{ source, target }] }) |
client.unfollowMany([{ source, target }]) |
client.feeds.getOrCreateUnfollows({ follows: [{ source, target }] }) |
As in Feeds v2, unfollowing removes the target's activities from the source feed unless you keep history. See Follows.
Collections
| Feeds v2 | Feeds v3 |
|---|---|
client.collections.add("trail", "abc", data) |
client.feeds.createCollections({ collections: [{ name: "trail", id: "abc", custom: data }] }) |
client.collections.update("trail", "abc", data) |
client.feeds.updateCollections({ collections: [...] }) or upsertCollections |
object: client.collections.entry("trail", "abc").ref() |
collection_refs: ["trail:abc"] on the activity |
On read, referenced collections are returned in activity.collections. See Collections.
Reading feeds
| Feeds v2 | Feeds v3 |
|---|---|
client.feed("timeline", "jane").get({ limit, id_lt, enrich: true, withOwnReactions: true, withReactionCounts: true, withRecentReactions: true, user_id }) |
client.feeds.feed("timeline", "jane").getOrCreate({ limit, next, user_id }) |
id_lt / id_gt pagination |
next / prev cursors from the previous response |
activity.reaction_counts.like |
activity.reaction_groups.like.count |
activity.own_reactions.like |
activity.own_reactions (filter by type) |
activity.own_reactions.<your bookmark kind> |
activity.own_bookmarks |
activity.latest_reactions.comment |
activity.comment_count, and client.feeds.getComments(...) for the comments |
reaction children_counts, latest_children (replies and likes on a comment) |
comment.reply_count, comment.reaction_groups, and client.feeds.getCommentReplies({ id }) for the replies |
activity.actor.data.name (enriched user) |
activity.user.name |
activity.object (enriched collection or reshared activity) |
activity.collections / activity.parent |
notification feed get({ mark_seen: true }) |
getOrCreate({ user_id }) then markActivity({ mark_all_seen: true, user_id }) |
notification groups results[i].activities, is_seen |
aggregated_activities[i].activities, is_seen, and notification_status.unseen |
See Feeds and Notification Feeds.
Checklist
- Every Feeds v2 write path is listed, with the feed groups, fields, reaction kinds and follows it uses, and so is every read path.
- The migration mapping covers the fields the ported code reads (including older formats your app still renders).
- Ported read paths are behind a read flag and have been compared in shadow mode.
- Ported write paths are behind a single write flag.
- Code that stores or compares ids accepts both Feeds v2 and Feeds v3 ids.
- Your UI shows a user's own new writes optimistically while reads come from Feeds v3.
- After switching writes, the migration is completed in the Dashboard.