const user: UserRequest = {
id: 'userid',
role: 'user',
custom: {
color: 'blue',
},
name: 'This is a test user',
image: 'link/to/profile/image',
};
client.upsertUsers([user]);
// or
client.updateUsersPartial({
users: [
{
id: user.id,
set: {
'new-field': 'value',
},
unset: ['name'],
},
],
});User permissions
Stream Activity Feeds ships with a configurable permission system that allows high resolution control over what users are permitted to do.
Getting Started
There are multiple important terms to understand when it comes to permission management. Each permission check comes down to three things:
Subject- an actor which attempts to perform a certain Action. It can be represented by a User, a FeedMember, or a FollowerResource- an item that the Subject attempts to perform an Action against. It can be a Feed, Activity, or another UserAction- the exact action that is being performed. For exampleReadFeed,AddActivity,DeleteActivity
The purpose of the permission system is to answer the question: is Subject A allowed to perform Action B on Resource C?
Stream Activity Feeds provides several concepts which help to control which actions are available to whom:
Permission- an object which represents actions a subject is allowed to performRole- assigned to a User or to a user's relationship with a feed (as member or follower), and is used to check their permissionsGrants- the way permissions are assigned to roles, applicable across the entire application or specific to a feed group (visibility scope).
Also important to know: permission checking only happens on client-side calls. Server-side calls allow everything so long as a valid API key and secret are provided.
Role Management
To make it easy to get started, all Stream applications come with several roles already built in with permissions that represent the most common use cases. These roles can be customized if needed, and new roles can be created specific to your application.
This is the process of assigning a role to users so they can be granted permissions. This represents Subject A in the permissions question. Users have one role which grants them permissions for the entire application. Additionally, users can have feed-level roles when they interact with a specific feed—as a member or as a follower.
By default all users have the built-in role user assigned. Updating a user's role can be done via upsertUsers or updateUsersPartial endpoints:
// Example 1: Upsert users
user := getstream.UserRequest{
ID: "userid",
Role: getstream.PtrTo("user"),
Custom: map[string]any{
"color": "blue",
},
Name: getstream.PtrTo("This is a test user"),
Image: getstream.PtrTo("link/to/profile/image"),
}
upsertRequest := &getstream.UpdateUsersRequest{
Users: map[string]getstream.UserRequest{
user.ID: user,
},
}
upsertResponse, err := client.UpdateUsers(context.Background(), upsertRequest)
if err != nil {
log.Fatal("Error upserting users:", err)
}
log.Printf("Users upserted successfully: %+v\n", upsertResponse)
// Example 2: Partial update users
partialUpdateRequest := &getstream.UpdateUsersPartialRequest{
Users: []getstream.UpdateUserPartialRequest{
{
ID: user.ID,
Set: map[string]any{
"new-field": "value",
},
Unset: []string{"name"},
},
},
}
partialResponse, err := client.UpdateUsersPartial(context.Background(), partialUpdateRequest)
if err != nil {
log.Fatal("Error partially updating users:", err)
}
log.Printf("Users partially updated successfully: %+v\n", partialResponse)Map<String, UserRequest> usersMap = new HashMap<>();
usersMap.put(
testUserId,
UserRequest.builder().id(testUserId).name("Test User 1").role("user").build());
usersMap.put(
testUserId2,
UserRequest.builder().id(testUserId2).name("Test User 2").role("user").build());
UpdateUsersRequest updateUsersRequest = UpdateUsersRequest.builder().users(usersMap).build();
client.updateUsers(updateUsersRequest).execute();// Example 1: Upsert users (replace update)
$client->updateUsers(new GeneratedModels\UpdateUsersRequest(
users: [
'userid' => [
'id' => 'userid',
'name' => 'This is a test user',
'role' => 'user',
'custom' => (object)[
'color' => 'blue'
],
'image' => 'link/to/profile/image'
]
]
));
// Example 2: Partial update users
$client->updateUsersPartial(new GeneratedModels\UpdateUsersPartialRequest(
users: [
[
'id' => 'userid',
'set' => (object)[
'new-field' => 'value'
],
'unset' => ['name']
]
]
));var updateUsersRequest = new UpdateUsersRequest
{
Users = new Dictionary<string, UserRequest>
{
[_testUserId] = new UserRequest
{
ID = _testUserId,
Name = "Test User 1",
Role = "user"
},
[_testUserId2] = new UserRequest
{
ID = _testUserId2,
Name = "Test User 2",
Role = "user"
},
[_testUserId3] = new UserRequest
{
ID = _testUserId3,
Name = "Test User 3",
Role = "user"
}
}
};
var userResponse = await _client.UpdateUsersAsync(updateUsersRequest);users = {
self.test_user_id: UserRequest(
id=self.test_user_id, name="Test User 1", role="user"
),
self.test_user_id_2: UserRequest(
id=self.test_user_id_2, name="Test User 2", role="user"
),
}
response = self.client.update_users(users=users)# Partially update user
partial_request = GetStream::Generated::Models::UpdateUsersPartialRequest.new(
users: [
{
'id' => 'userid',
'set' => {
'name' => 'Updated Name',
},
},
],
)
response = client.common.update_users_partial(partial_request)When you add a user as a member of a feed, the feed_member role is assigned by default. You can set a different role when adding or updating members:
await client.feeds.feed("user", "community_id").updateFeedMembers({
operation: "upsert",
members: [{ user_id: "james_bond", role: "feed_moderator" }],
});$feed = $feedsClient->feed('user', 'community_id');
$feed->updateFeedMembers(
new GeneratedModels\UpdateFeedMembersRequest(
operation: 'upsert',
members: [
new GeneratedModels\FeedMemberRequest(
userID: 'james_bond',
role: 'feed_moderator'
),
]
)
);client.feeds.update_feed_members(
feed_group="user",
feed_id="community_id",
operation="upsert",
members=[
{"user_id": "james_bond", "role": "feed_moderator"},
],
)UpdateFeedMembersRequest request = UpdateFeedMembersRequest.builder()
.operation("upsert")
.members(List.of(
FeedMemberRequest.builder()
.userID("james_bond")
.role("feed_moderator")
.build()
))
.build();
feeds.updateFeedMembers("user", "community_id", request).execute();await _feedsV3Client.UpdateFeedMembersAsync(
FeedGroupID: "user",
FeedID: "community_id",
request: new UpdateFeedMembersRequest
{
Operation = "upsert",
Members = new List<FeedMemberRequest>
{
new FeedMemberRequest
{
UserID = "james_bond",
Role = "feed_moderator"
}
}
}
);client.feeds.update_feed_members(
"user",
"community_id",
GetStream::Generated::Models::UpdateFeedMembersRequest.new(
operation: "upsert",
members: [
GetStream::Generated::Models::FeedMemberRequest.new(
user_id: "james_bond",
role: "feed_moderator"
)
]
)
)feed := client.Feeds().Feed("user", "community_id")
_, err = feed.UpdateFeedMembers(ctx, &getstream.UpdateFeedMembersRequest{
Operation: "upsert",
Members: []getstream.FeedMemberRequest{
{UserID: "james_bond", Role: getstream.PtrTo("feed_moderator")},
},
})You can also update a follower's role on an existing follow:
await client.feeds.updateFollow({
source: `timeline:${sourceFeedId}`,
target: `user:${targetFeedId}`,
push_preference: "none",
role: "my_custom_feed_follower_role",
custom: {
note: "Updated follow",
},
});_, 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",
},
})UpdateFollowRequest request = UpdateFollowRequest.builder()
.source("timeline:" + sourceFeedId)
.target("user:" + targetFeedId)
.pushPreference("none")
.followerRole("my_custom_feed_follower_role")
.custom(Map.of("note", "Updated follow"))
.build();
feeds.updateFollow(request).execute();$response = $feedsClient->updateFollow(
new GeneratedModels\UpdateFollowRequest(
source: 'timeline:' . $sourceFeedId,
target: 'user:' . $targetFeedId,
pushPreference: 'none',
followerRole: 'my_custom_feed_follower_role',
custom: (object)['note' => 'Updated follow']
)
);await _feedsV3Client.UpdateFollowAsync(new UpdateFollowRequest
{
Source = $"timeline:{sourceFeedId}",
Target = $"user:{targetFeedId}",
PushPreference = "none",
FollowerRole = "my_custom_feed_follower_role",
Custom = new Dictionary<string, object> { ["note"] = "Updated follow" },
});client.feeds.update_follow(
source=f"timeline:{source_feed_id}",
target=f"user:{target_feed_id}",
push_preference="none",
follower_role="my_custom_feed_follower_role",
custom={"note": "Updated follow"},
)client.feeds.update_follow(
source: "timeline:#{source_feed_id}",
target: "user:#{target_feed_id}",
push_preference: 'none',
follower_role: 'my_custom_feed_follower_role',
custom: { note: 'Updated follow' }
)Changing roles is not allowed client-side. Use server-side SDKs for these operations.
Subject
Subject can be represented by a User, a FeedMember, or a Follower. FeedMember and Follower subjects are used when a user interacts with a feed they are a member or follower of. Both the user-level role and the feed-level role (member or follower role) are taken into account when checking permissions.
Built-in roles
There are some built-in roles in Stream Activity Feeds that cover basic scenarios:
| Role | Level | Description |
|---|---|---|
| user | User | Default user role |
| guest | User | Guests are short-lived temporary users that could be created without a token |
| anonymous | User | Anonymous users are not allowed to perform any actions that write data. You should treat them as unauthenticated clients |
| admin | User | Role for users that perform administrative tasks with elevated permissions |
| moderator | User | Role for users that perform moderation tasks |
| feed_member | Feed | Default role when a user is added as a member of a feed; has elevated permissions to perform administrative tasks on the feed |
| feed_member_viewer | Feed | A read-only feed member role |
| feed_follower | Feed | Default role assigned when a user starts following a feed |
You cannot use user-level roles as feed-level roles and vice versa. This restriction only applies to built-in roles.
Ownership
Some Stream Activity Feeds entities have an owner, and ownership can be considered when configuring access permissions. Ownership is supported in these entity types:
Feed - owned by its creator
Activity - owned by its creator (actor)
User - the authenticated user owns themselves
Using the ownership concept, permissions can be set up so that entity owners are allowed to perform certain actions. For example:
Update Own Activity - allows activity authors to edit their activities
Update Own User - allows users to change their own properties (except role)
Add Activity in Own Feed - allows feed owners to add activities to feeds they created even if they are not members
Custom Roles
In more sophisticated scenarios custom roles can be used. A Stream application can have up to 25 custom roles. Roles are simple and require only a name to be created. They do nothing until permissions are assigned to the role. To create a new custom role you can use the CreateRole API endpoint with the activity-feeds server-side SDK:
await client.createRole("special_agent");$client->createRole('special_agent');client.create_role("special_agent")Role.create().name("special_agent").request();await _permissionClient.CreateRoleAsync("special_agent");client.create_role('special_agent')p := client.Permissions()
p.CreateRole(ctx, "special_agent")To delete a previously created role you can use the DeleteRole API endpoint:
await client.deleteRole("agent_006");$client->deleteRole('agent_006');client.delete_role("agent_006")Role.delete("agent_006").request();await _permissionClient.DeleteRoleAsync("special_agent");client.delete_role('agent_006')p := client.Permissions()
p.DeleteRole(ctx, "agent_006")To delete a role, you must remove all permission grants for that role and ensure no non-deleted users have this role assigned. Feed-level roles can be deleted without reassigning them first, although some users may lose access on feeds where that role was used.
Once you have created a role you can start granting permissions to it. You can also grant or remove permissions for built-in roles.
Granting permissions
User access in Activity Feeds is split across multiple scopes.
Application permissions: You can grant these using the
.appscope. These permissions apply to operations that occur outside of feed groups, such as modifying other users or moderation features.Feed visibility permissions: These apply to all feeds that have a given visibility.
To list all available permissions:
const { permissions } = await client.listPermissions(); // List of Permission objects$response = $client->listPermissions();
var_dump($response['permissions']); // List of Permission objectsresponse = client.list_permissions()var response = Permission.list().request();var response = await _permissionClient.ListPermissionsAsync();response = client.list_permissionsresp, err := p.ListPermissions(ctx)Each permission object contains these fields:
| Field | Type | Description | Example |
|---|---|---|---|
| id | string | Unique permission ID | add-activity-owner |
| name | string | Human-readable permission name | Add Activity in Owned Feed |
| description | string | Human-readable permission description | Grants AddActivity when… |
| action | string | Action that this permission grants | AddActivity |
| owner | boolean | If true, Subject must be the owner of the Resource | true |
| same_team | boolean | If true, Subject must be part of the same team as the Resource | true |
To configure granted permissions for a feed visibility (e.g. public, visible, followers, members, private), use the feed visibility API. First get the current configuration with getFeedVisibility, then update role grants with updateFeedVisibility:
// Get current grants for a feed visibility
const response = await client.feeds.getFeedVisibility({ name: "visible" });
// Update "feed_member" role grants for this feed visibility
await client.feeds.updateFeedVisibility({
name: "visible",
grants: {
feed_member: [
"read-feed",
"add-activity",
"update-activity-owner",
"delete-activity-owner",
],
},
});// Get current grants for a feed visibility
$response = $client->feeds->getFeedVisibility(['name' => 'visible']);
// Update "feed_member" role grants for this feed visibility
$client->feeds->updateFeedVisibility([
'name' => 'visible',
'grants' => [
'feed_member' => [
'read-feed',
'add-activity',
'update-activity-owner',
'delete-activity-owner',
],
],
]);# Get current grants for a feed visibility
response = client.feeds.get_feed_visibility(name="visible")
# Update "feed_member" role grants for this feed visibility
client.feeds.update_feed_visibility(
name="visible",
grants={
"feed_member": [
"read-feed",
"add-activity",
"update-activity-owner",
"delete-activity-owner",
]
},
)// Get current grants for a feed visibility
var response = feedsClient.getFeedVisibility("visible").execute();
// Update "feed_member" role grants for this feed visibility
var request = UpdateFeedVisibilityRequest.builder()
.name("visible")
.grants(Map.of("feed_member", List.of(
"read-feed",
"add-activity",
"update-activity-owner",
"delete-activity-owner"
)))
.build();
feedsClient.updateFeedVisibility(request).execute();// Get current grants for a feed visibility
var response = await _feedsClient.GetFeedVisibilityAsync("visible");
// Update "feed_member" role grants for this feed visibility
await _feedsClient.UpdateFeedVisibilityAsync(new UpdateFeedVisibilityRequest
{
Name = "visible",
Grants = new Dictionary<string, List<string>>
{
{
"feed_member",
new List<string>
{
"read-feed",
"add-activity",
"update-activity-owner",
"delete-activity-owner",
}
},
}
});# Get current grants for a feed visibility
response = client.feeds.get_feed_visibility(name: 'visible')
# Update "feed_member" role grants for this feed visibility
client.feeds.update_feed_visibility(
name: 'visible',
grants: {
'feed_member' => [
'read-feed',
'add-activity',
'update-activity-owner',
'delete-activity-owner',
],
}
)// Get current grants for a feed visibility
resp, err := client.Feeds().GetFeedVisibility(ctx, "visible")
// Update "feed_member" role grants for this feed visibility
_, err = client.Feeds().UpdateFeedVisibility(ctx, &getstream.UpdateFeedVisibilityRequest{
Name: "visible",
Grants: map[string][]string{
"feed_member": {
"read-feed",
"add-activity",
"update-activity-owner",
"delete-activity-owner",
},
},
})This call only changes grants for roles that are mentioned in the request. You can remove all grants for a role by providing an empty array ([]) as the list of permissions:
await client.feeds.updateFeedVisibility({
name: "visible",
grants: {
guest: [],
anonymous: [],
},
});$client->feeds->updateFeedVisibility([
'name' => 'visible',
'grants' => [
'guest' => [],
'anonymous' => [],
],
]);client.feeds.update_feed_visibility(
name="visible",
grants={"guest": [], "anonymous": []},
)var request = UpdateFeedVisibilityRequest.builder()
.name("visible")
.grants(Map.of(
"guest", List.of(),
"anonymous", List.of()
))
.build();
feedsClient.updateFeedVisibility(request).execute();await _feedsClient.UpdateFeedVisibilityAsync(new UpdateFeedVisibilityRequest
{
Name = "visible",
Grants = new Dictionary<string, List<string>>
{
{ "guest", new List<string>() },
{ "anonymous", new List<string>() },
}
});client.feeds.update_feed_visibility(
name: 'visible',
grants: { 'guest' => [], 'anonymous' => [] }
)_, err = client.Feeds().UpdateFeedVisibility(ctx, &getstream.UpdateFeedVisibilityRequest{
Name: "visible",
Grants: map[string][]string{
"guest": {},
"anonymous": {},
},
})To list all feed visibility configurations use listFeedVisibilities(). If you want to reset a feed visibility to default settings, pass null for the grants field (where supported by the SDK) when calling updateFeedVisibility.
You can manage .app scope grants using the Update App Settings API endpoint in the same way:
await client.updateAppSettings({
grants: {
anonymous: [],
guest: [],
user: ["search-user", "mute-user"],
admin: ["search-user", "mute-user", "ban-user"],
},
});$client->updateAppSettings([
'grants' => [
'anonymous' => [],
'guest' => [],
'user' => ['search-user', 'mute-user'],
'admin' => ['search-user', 'mute-user', 'ban-user'],
],
]);client.update_app_settings(grants={
"anonymous": [],
"guest": [],
"user": ["search-user", "mute-user"],
"admin": ["search-user", "mute-user", "ban-user"],
})var request = UpdateAppSettingsRequest.builder()
.grants(Map.of(
"anonymous", List.of(),
"guest", List.of(),
"user", List.of("search-user", "mute-user"),
"admin", List.of("search-user", "mute-user", "ban-user")
))
.build();
client.updateAppSettings(request).execute();await _client.UpdateAppSettingsAsync(new UpdateAppSettingsRequest
{
Grants = new Dictionary<string, List<string>>
{
{ "anonymous", new List<string>() },
{ "guest", new List<string>() },
{ "user", new List<string> { "search-user", "mute-user" } },
{ "admin", new List<string> { "search-user", "mute-user", "ban-user" } },
}
});client.update_app_settings(
grants: {
'anonymous' => [],
'guest' => [],
'user' => ['search-user', 'mute-user'],
'admin' => ['search-user', 'mute-user', 'ban-user'],
}
)_, err = client.UpdateAppSettings(ctx, &getstream.AppSettingsRequest{
Grants: map[string][]string{
"anonymous": {},
"guest": {},
"user": {"search-user"},
"admin": {"search-user", "ban-user"},
},
})UI for configuring permissions
Stream Dashboard provides a user interface to edit permission grants.
Client-side permission checks
The own_capabilities array contains the actions a user is allowed to perform in a given feed (for example add-activity). You can use this data to show/hide different parts of the UI based on a user's permissions.
// Get feed from activity context (e.g. current_feed)
const [group, id] = activity.current_feed?.feed.split(":") ?? [];
const feed = group && id ? client.feed(group, id) : undefined;
const unsubscribe = feed?.state.subscribeWithSelector(
(state) => state.own_capabilities,
(ownCapabilities) => {
// Use ownCapabilities for UI or permission checks
},
);import { useOwnCapabilities } from "@stream-io/feeds-react-sdk";
const [group, id] = activity.current_feed?.feed.split(":") ?? [];
const feed = group && id ? client?.feed(group, id) : undefined;
const ownCapabilities = useOwnCapabilities(feed);import { useOwnCapabilities } from "@stream-io/feeds-react-native-sdk";
const [group, id] = activity.current_feed?.feed.split(":") ?? [];
const feed = group && id ? client?.feed(group, id) : undefined;
const ownCapabilities = useOwnCapabilities(feed);