# 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 Follower

- `Resource` - an item that the Subject attempts to perform an Action against. It can be a Feed, Activity, or another User

- `Action` - the exact action that is being performed. For example `ReadFeed`, `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 perform

- `Role` - assigned to a User or to a user's relationship with a feed (as member or follower), and is used to check their permissions

- `Grants` - 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:

<Tabs>

```js label="Node"
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'],
    },
  ],
});
```

```go label="Go"
// 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)
```

```java label="Java"
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();
```

```php label="php"
// 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']
        ]
    ]
));
```

```csharp label="C#"
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);
```

```python label="Python"
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)
```

```ruby label="Ruby"
# 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)
```

</Tabs>


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:

<Tabs>

```js label="Node"
await client.feeds.feed("user", "community_id").updateFeedMembers({
  operation: "upsert",
  members: [{ user_id: "james_bond", role: "feed_moderator" }],
});
```

```php label="PHP"
$feed = $feedsClient->feed('user', 'community_id');
$feed->updateFeedMembers(
    new GeneratedModels\UpdateFeedMembersRequest(
        operation: 'upsert',
        members: [
            new GeneratedModels\FeedMemberRequest(
                userID: 'james_bond',
                role: 'feed_moderator'
            ),
        ]
    )
);
```

```python label="Python"
client.feeds.update_feed_members(
    feed_group="user",
    feed_id="community_id",
    operation="upsert",
    members=[
        {"user_id": "james_bond", "role": "feed_moderator"},
    ],
)
```

```java label="Java"
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();
```

```csharp label="C#"
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"
            }
        }
    }
);
```

```ruby label="Ruby"
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"
      )
    ]
  )
)
```

```go label="Go"
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")},
	},
})
```

</Tabs>

You can also update a **follower's role** on an existing follow:

<Tabs>

```js label="Node"
await client.feeds.updateFollow({
  source: `timeline:${sourceFeedId}`,
  target: `user:${targetFeedId}`,
  push_preference: "none",
  role: "my_custom_feed_follower_role",
  custom: {
    note: "Updated follow",
  },
});
```

```go label="Go"
_, 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",
  },
})
```

```java label="Java"
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();
```

```php label="PHP"
$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']
    )
);
```

```csharp label="C#"
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" },
});
```

```python label="Python"
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"},
)
```

```ruby label="Ruby"
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' }
)
```

</Tabs>

<admonition type="info">

Changing roles is not allowed client-side. Use server-side SDKs for these operations.

</admonition>

## 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                                                                     |
| feed_moderator     | Feed  | Feed member role to perform moderation                                                                                        |

<admonition type="info">

You cannot use user-level roles as feed-level roles and vice versa. This restriction only applies to built-in roles.

</admonition>

## 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:

1. **Feed** - owned by its creator

2. **Activity** - owned by its creator (actor)

3. **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:

<Tabs>

```js label="Node"
await client.createRole({
  name: "special_agent",
});
```

```php label="PHP"
$client->createRole(['name' => 'special_agent']);
```

```python label="Python"
client.create_role(name="special_agent")
```

```java label="Java"
Role.create()
    .name("special_agent")
    .request();
```

```csharp label="C#"
await _permissionClient.CreateRoleAsync(new CreateRoleRequest
{
    Name = "special_agent",
});
```

```ruby label="Ruby"
client.create_role(name: 'special_agent')
```

```go label="Go"
_, err := client.Permissions().CreateRole(ctx, &getstream.CreateRoleRequest{
	Name: "special_agent",
})
```

</Tabs>

To delete a previously created role you can use the DeleteRole API endpoint:

<Tabs>

```js label="Node"
await client.deleteRole({
  name: "agent_006",
});
```

```php label="PHP"
$client->deleteRole(['name' => 'agent_006']);
```

```python label="Python"
client.delete_role(name="agent_006")
```

```java label="Java"
Role.delete("agent_006").request();
```

```csharp label="C#"
await _permissionClient.DeleteRoleAsync(new DeleteRoleRequest
{
    Name = "agent_006",
});
```

```ruby label="Ruby"
client.delete_role(name: 'agent_006')
```

```go label="Go"
_, err := client.Permissions().DeleteRole(ctx, &getstream.DeleteRoleRequest{
	Name: "agent_006",
})
```

</Tabs>

<admonition type="info">

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.

</admonition>

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 `.app` scope. These permissions apply to operations that occur outside of feed groups, such as [modifying other users](/activity-feeds/docs/node/user-management/) or [moderation features](/activity-feeds/docs/node/moderation/).

- **Feed visibility permissions**: These apply to all feeds that have a given visibility.

To list all available permissions:

<Tabs>

```js label="Node"
const { permissions } = await client.listPermissions(); // List of Permission objects
```

```php label="PHP"
$response = $client->listPermissions();
var_dump($response['permissions']); // List of Permission objects
```

```python label="Python"
response = client.list_permissions()
```

```java label="Java"
var response = Permission.list().request();
```

```csharp label="C#"
var response = await _permissionClient.ListPermissionsAsync();
```

```ruby label="Ruby"
response = client.list_permissions
```

```go label="Go"
resp, err := p.ListPermissions(ctx)
```

</Tabs>

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`:

<Tabs>

```js label="Node"
// 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",
    ],
  },
});
```

```php label="PHP"
// 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',
        ],
    ],
]);
```

```python label="Python"
# 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",
        ]
    },
)
```

```java label="Java"
// 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();
```

```csharp label="C#"
// 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",
            }
        },
    }
});
```

```ruby label="Ruby"
# 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',
    ],
  }
)
```

```go label="Go"
// 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",
		},
	},
})
```

</Tabs>

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:

<Tabs>

```js label="Node"
await client.feeds.updateFeedVisibility({
  name: "visible",
  grants: {
    guest: [],
    anonymous: [],
  },
});
```

```php label="PHP"
$client->feeds->updateFeedVisibility([
    'name' => 'visible',
    'grants' => [
        'guest' => [],
        'anonymous' => [],
    ],
]);
```

```python label="Python"
client.feeds.update_feed_visibility(
    name="visible",
    grants={"guest": [], "anonymous": []},
)
```

```java label="Java"
var request = UpdateFeedVisibilityRequest.builder()
    .name("visible")
    .grants(Map.of(
        "guest", List.of(),
        "anonymous", List.of()
    ))
    .build();
feedsClient.updateFeedVisibility(request).execute();
```

```csharp label="C#"
await _feedsClient.UpdateFeedVisibilityAsync(new UpdateFeedVisibilityRequest
{
    Name = "visible",
    Grants = new Dictionary<string, List<string>>
    {
        { "guest", new List<string>() },
        { "anonymous", new List<string>() },
    }
});
```

```ruby label="Ruby"
client.feeds.update_feed_visibility(
  name: 'visible',
  grants: { 'guest' => [], 'anonymous' => [] }
)
```

```go label="Go"
_, err = client.Feeds().UpdateFeedVisibility(ctx, &getstream.UpdateFeedVisibilityRequest{
	Name: "visible",
	Grants: map[string][]string{
		"guest":     {},
		"anonymous": {},
	},
})
```

</Tabs>

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:

<Tabs>

```js label="Node"
await client.updateAppSettings({
  grants: {
    anonymous: [],
    guest: [],
    user: ["search-user", "mute-user"],
    admin: ["search-user", "mute-user", "ban-user"],
  },
});
```

```php label="PHP"
$client->updateAppSettings([
    'grants' => [
        'anonymous' => [],
        'guest' => [],
        'user' => ['search-user', 'mute-user'],
        'admin' => ['search-user', 'mute-user', 'ban-user'],
    ],
]);
```

```python label="Python"
client.update_app_settings(grants={
    "anonymous": [],
    "guest": [],
    "user": ["search-user", "mute-user"],
    "admin": ["search-user", "mute-user", "ban-user"],
})
```

```java label="Java"
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();
```

```csharp label="C#"
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" } },
    }
});
```

```ruby label="Ruby"
client.update_app_settings(
  grants: {
    'anonymous' => [],
    'guest' => [],
    'user' => ['search-user', 'mute-user'],
    'admin' => ['search-user', 'mute-user', 'ban-user'],
  }
)
```

```go label="Go"
_, err = client.UpdateAppSettings(ctx, &getstream.AppSettingsRequest{
	Grants: map[string][]string{
		"anonymous": {},
		"guest":     {},
		"user":      {"search-user"},
		"admin":     {"search-user", "ban-user"},
	},
})
```

</Tabs>

## 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.

<Tabs>

```js label="JavaScript"
// 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
  },
);
```

```ts label="React"
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);
```

```ts label="React Native"
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);
```

</Tabs>


---

This page was last updated at 2026-05-08T12:56:52.316Z.

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/node/user-permissions/](https://getstream.io/activity-feeds/docs/node/user-permissions/).