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:

  1. Writes go to Feeds v2. Replication mirrors them into Feeds v3. Your application is unchanged.
  2. 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.
  3. Serve reads from Feeds v3. Writes still go to Feeds v2 and are replicated.
  4. 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"]
Warning:

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, feeds and custom instead of actor, verb, to and top-level custom fields, reactions come back as reaction_groups, own_reactions and latest_reactions, and comments are their own objects (see the tables below).
  • Mapped ids. If your mapping sets id or foreign_id, the Feeds v3 activity id is the mapped value and the Feeds v2 id is in custom.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 id or foreign_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

Feeds v2
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");
Feeds v3
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

Feeds v2
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"],
});
Feeds v3
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 to user:jane, hashtag:cooking and hashtag:food. Read hashtag:cooking to 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's notification (or timeline) feed follows that feed: one follow when the user signs up, and a one-off client.feeds.getOrCreateFollows batch 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 id gets 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, type and user_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 like on an activity; adding it again does not create a second one. Pass enforce_unique: true if a user should hold only one reaction type at a time, such as switching from like to love.

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.