const client = new StreamChat('{{ api_key }}', '{{ api_secret }}');
// Enable multi-tenant in app settings.
await client.updateAppSettings({
multi_tenant_enabled: true,
});
Multi-Tenant & Teams
Many apps that add chat have customers of their own. If you’re building something like Slack, or a SaaS application like InVision you want to make sure that one customer can’t read the messages of another customer. Stream Chat can be configured in multi-tenant mode so that users are organized in separated teams that cannot interact with each other.
Teams
Stream Chat has the concept of teams for users and channels. The purpose of teams is to provide a simple way to separate different groups of users and channels within a single application.
If a user belongs to a team, the API will ensure that such user will only be able to connect to channels from the same team. Features such as user search are limited so that a user can only search for users from the same team by default.
In legacy permission system users can never access users nor channels from other teams. In Permissions V2 it is possible to alter this behavior using multi-tenant permissions.
When enabling multi-tenant mode all user requests will always ensure that the request applies to a team the user belongs to. For instance, if a user from team “blue” tries to delete a message that was created on a channel from team “red” the API will return an error. If user doesn’t have team set, it will only have access to users and channels that don’t have team.
Enable Teams for your application
In order to use Teams, your application must have multi-tenant mode enabled. You can ensure your app is in multi-tenant mode by calling the Application Settings endpoint.
App.update().multiTenantEnabled(true).request();
client = StreamChat("{{ api_key }}", "{{ api_secret }}")
client.update_app_settings(multi_tenant_enabled=True)
client.update_app_settings(multi_tenant_enabled: true)
client->updateAppSettings(["multi_tenant_enabled" => true]);
appClient.UpdateAppSettingsAsync(new AppSettingsRequest { MultiTenantEnabled = true });
client.UpdateAppSettings(ctx, NewAppSettings().SetMultiTenant(true))
You only need to activate multi-tenant once per application.
Make sure to activate multi-tenant before using teams.
Do not turn off multi-tenant on an application without very careful consideration as this will turn off teams checking which gives users the ability to access all channels and messages across all teams.
User teams
When using teams, users must be created from your back-end and specify which teams they are a member of.
// creates or updates a user from backend to be part of the "red" and "blue" teams
client.upsertUser({id, teams: ["red", "blue"]});
// Partial updating a user to be part of red and blue team
var updateObj =
UserPartialUpdateRequestObject.builder()
.id(user.getId())
.setValue("teams", List.of("red", "blue"))
.build();
User.partialUpdate()
.user(updateObj)
.request();
client.update_user_partial(
{"id": user_id, "set": {"teams": ["red", "blue"]}}
)
client.update_user_partial({
id: user_id,
set: { teams: ['red', 'blue'] }
})
client->partialUpdateUser(["id" => user_id, "set" => ["teams" => ["red", "blue"]]]);
var updates = new Dictionary<string, object>
{
{ "teams", new[] { "red", "blue" } },
};
await userClient.UpdatePartialAsync(new UserPartialRequest
{
Id = user.Id,
Set = updates,
});
update := PartialUserUpdate{
ID: user.ID,
Set: map[string]interface{}{
"teams": []string { "red", "blue" },
},
}
resp, err := c.PartialUpdateUsers(ctx, []PartialUserUpdate{update})
A user can be a member of a maximum of 250 teams. Team name is limited to 100 bytes
User teams are included in all User object payloads. We recommend to have short team names to reduce response payload sizes
In Permissions v1, user teams can only be changed using server-side auth. This ensures users can’t change their own team membership. In Permissions v2 it is possible to update user teams from client-side if UpdateUserTeam
action is granted to the user
Channel team
Channels can be associated with a team. Users can create channels client-side but if their user is part of a team, they will have to specify a team or the request will be rejected with an error.
// Creates the red-general channel for the red team
client.createChannel(
channelType = "messaging",
channelId = "red-general",
extraData = mapOf("team" to "red")
).enqueue { result ->
if (result.isSuccess) {
val channel = result.data()
} else {
// Handle result.error()
}
}
// Creates the channel red-general for team red
client.channel("messaging", "red-general", {team: "red"}).create()
import StreamChat
/// 1: Create a `ChannelId` that represents the channel you want to create.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Use the `ChatClient` to create a `ChatChannelController` with the `ChannelId`.
let channelController = try chatClient.channelController(
createChannelWithId: channelId,
name: "Channel Name",
imageURL: nil,
team: "red",
extraData: .defaultValue
)
/// 3: Call `ChatChannelController.synchronize` to create the channel.
channelController.synchronize { error in
if let error = error {
/// 4: Handle possible errors
print(error)
}
}
// Creates the channel red-general for team red
final channel = await client.channel(
'messaging',
id: 'red-general',
extraData: {'team': 'red'},
).create();
channel = client.channel("messaging", "red-general", {"team": "red"})
channel.create()
const FChannelProperties Properties{
TEXT("messaging"), // Type
TEXT("red-general"), // Id
TEXT("red"), // Team
};
Client->CreateChannel(
Properties,
[](UChatChannel* Channel)
{
// Channel created
});
channel = client.channel('messaging', channel_id: 'red-general', data: { 'team' => 'red' })
channel.create(user_id)
$channel = $client->Channel(
"messaging",
"red-general",
["team" => "red"]
);
$channel->create(userId);
var chanData = new ChannelRequest
{
Team = "red",
CreatedBy = new UserRequest { Id = user.Id },
};
await _channelClient.GetOrCreateAsync("messaging", "red-general", new ChannelGetRequest
{
Data = chanData,
});
resp, err := client.CreateChannel(ctx, "messaging", "red-general", owner.ID, map[string]interface{}{
"team": "red",
})
// Creates the red-general channel for the red team
Map<String, Object> extraData = new HashMap<>();
List<String> memberIds = new LinkedList<>();
extraData.put("team", "red");
client.createChannel("messaging", "red-general", memberIds, extraData).enqueue(result -> {
if (result.isSuccess()) {
Channel channel = result.data();
} else {
// Handle result.error()
}
});
Channel teams allows you to ensure proper permission checking for a multi tenant application. Keep in mind that you will still need to enforce that channel IDs are unique. A very effective approach is to include the team name as a prefix to avoid collisions. (ie. “red-general” and “blue-general” instead of just “general”)
User Search
By default the user search will only return results from teams that user is a part of. API injects filter {teams: {$in: ["red", "blue"]}}
for every request that doesn’t already contain filter for teams
field. If you want to query users from all teams, you have to provide empty filter like this: {teams:{}}
. For server-side requests, this filter does not apply.
// Search for users with the name Jordan that are part of the red team
val filter = Filters.and(
Filters.eq("name", "Jordan"),
Filters.contains("teams", "red")
)
client.queryUsers(QueryUsersRequest(filter, offset = 0, limit = 1)).enqueue { result ->
if (result.isSuccess) {
val users: List<User> = result.data()
} else {
// Handle result.error()
}
}
// search for users with name Jordan that are part of the same team as authorized user
client.queryUsers({
name: { $eq: "Jordan" }
});
// search for users with name Nick in all teams
client.queryUsers({
$and: [
{ name: { $eq: "Nick" } },
{ teams: {} }
],
});
// search for users with name Dan in subset of teams
client.queryUsers({
$and: [
{ name: { $eq: "Nick" } },
{ teams: { $in: ["red", "blue"] } }
],
});
// search for users with name Tom that don't have any team assigned
client.queryUsers({
$and: [
{ name: { $eq: "Tom" } },
{ teams: { $eq: null } }
],
});
import StreamChat
// search for users with name Jordan that are part of the same team as authorized user
let controller = chatClient.userListController(
query: .init(filter: .eq("name", value: "Jordan"))
)
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// access users
print(controller.users)
}
}
// search for users with name Jordan that are part of the same team as authorized user
await client.queryUsers(
filter: Filter.equal('name', 'Jordan'),
);
// search for users with name Nick in all teams
await client.queryUsers(
filter: Filter.and([
Filter.equal('name', 'Nick'),
Filter.raw(value: {'teams': {}}),
]),
);
// search for users with name Dan in subset of teams
await client.queryUsers(
filter: Filter.and([
Filter.equal('name', 'Nick'),
Filter.in_('teams', ['red', 'blue']),
]),
);
// search for users with name Tom that don't have any team assigned
await client.queryUsers(
filter: Filter.and([
Filter.equal('name', 'Tom'),
Filter.raw(value: {
'teams': {'\$eq': null},
}),
]),
);
// search for users with name Jordan that are part of the same team as the authorized user
Client->QueryUsers(FFilter::Equal(TEXT("name"), TEXT("Jordan")));
// search for users with name Nick in all teams
Client->QueryUsers(FFilter::And({
FFilter::Equal(TEXT("name"), TEXT("Nick")),
FFilter::Empty(TEXT("teams")),
}));
// search for users with name Dan in subset of teams
Client->QueryUsers(FFilter::And({
FFilter::Equal(TEXT("name"), TEXT("Nick")),
FFilter::In(TEXT("teams"), {TEXT("red"), TEXT("blue")}),
}));
// search for users with name Tom that don't have any team assigned
Client->QueryUsers(FFilter::And({
FFilter::Equal(TEXT("name"), TEXT("Tom")),
FFilter::Equal(TEXT("teams"), nullptr),
}));
# Server side usage, it searches all teams implicitly
client.query_users({ 'name' => { '$eq' => 'Nick' } })
// Server side usage, it searches all teams implicitly
client.queryUsers(["name" => ['$eq' => "Nick"]])
// Server side usage, it searches all teams implicitly
await userClient.QueryAsync(QueryUserOptions.Default.WithFilter(new Dictionary<string, object>
{
{ "name", "Nick" },
}));
// Server side usage, it searches all teams implicitly
resp, err := c.QueryUsers(ctx, &QueryOption{
Filter: map[string]interface{}{
"name": map[string]string{"$eq": "Nick"},
},
})
// Search for users with the name Jordan that are part of the red team
FilterObject filter = Filters.and(
Filters.eq("name", "Jordan"),
Filters.contains("teams", "red")
);
int offset = 0;
int limit = 1;
client.queryUsers(new QueryUsersRequest(filter, offset, limit)).enqueue(result -> {
if (result.isSuccess()) {
List<User> users = result.data();
} else {
// Handle result.error()
}
});
Users that cannot be displayed to the current user due to lack of permissions will be omitted from response.
Query Channels
When using multi-tenant, the query channels endpoint will only return channels that match the query and are on the same team as the user. API injects filter {team: {$in: [<user_teams>]}}
for every request that doesn’t already contain filter for team
field. If you want to query channels from all teams, you have to provide empty filter like this: {team:{}}
. For server-side requests, this filter does not apply.
// query all channels from teams that user is a part of
await client.queryChannels({})
// query all channels from all teams
await client.queryChannels({team:{}})
// query all channels with no teams
await client.queryChannels({team:{$eq:null}})
// query all channels from teams that user is a part of
await client.queryChannels(filter: Filter.empty());
// query all channels from all teams
await client.queryChannels(filter: Filter.custom(key: 'team', value: {}));
// query all channels from teams that user is a part of
var channels = Channel.list().request();
// query all channels from all teams
var channels = Channel.list()
.filterCondition("team", "")
.request();
// query all channels from teams that user is a part of
Client->QueryChannels();
// query all channels from all teams
Client->QueryChannels(FFilter::Empty(TEXT("team")));
# query all channels from teams that user is a part of
client.query_channels({ 'filter_conditions' => {}})
# query all channels from all teams
client.query_channels({ 'filter_conditions' => { 'team' => {} }})
// query all channels from teams that user is a part of
client.queryChannels([ "filter_conditions" => [] ])
// query all channels from all teams
client.queryChannels([ "filter_conditions" => [ "team" => [] ]])
// query all channels from teams that user is a part of
await _channelClient.QueryChannelsAsync(QueryChannelsOptions.Default.WithFilter(new Dictionary<string, object>()));
// query all channels from all teams
await _channelClient.QueryChannelsAsync(QueryChannelsOptions.Default.WithFilter(new Dictionary<string, object>
{
{ "team", new Dictionary<string, string>() },
}));
queryChannResp, err := c.QueryChannels(ctx, &QueryOption{Filter: make(map[string]interface{})})
// query all channels from all teams
queryChannResp, err := c.QueryChannels(ctx, &QueryOption{
Filter: map[string]interface{}{
"team": make(map[string]string),
},
})
In case if response contains channels that user cannot access, an access error will be returned.
Multi-Tenant Permissions
In tables below you will find default permission grants for builtin roles that designed for multi-tenant applications. They are useful for multi-tenant applications only.
By default, for multi-tenant applications, all objects (users, channels, and messages) must belong to the same team to be able to interact. These multi-tenant permissions enable overriding that behavior, so that certain users can have permissions to interact with objects on any team
Scope video:livestream
Permission ID |
---|
Scope video:development
Permission ID |
---|
Scope .app
Permission ID | global_moderator | global_admin |
---|---|---|
flag-user-any-team | ✅ | ✅ |
mute-user-any-team | ✅ | ✅ |
read-flag-reports-any-team | ✅ | ✅ |
search-user-any-team | ✅ | ✅ |
update-flag-report-any-team | ✅ | ✅ |
update-user-owner | ✅ | ✅ |
Scope video:audio_room
Permission ID |
---|
Scope video:default
Permission ID |
---|
Scope messaging
Permission ID | global_moderator | global_admin |
---|---|---|
add-links-any-team | ✅ | ✅ |
ban-channel-member-any-team | ✅ | ✅ |
ban-user-any-team | ✅ | ✅ |
create-call-any-team | ✅ | ✅ |
create-channel-any-team | ✅ | ✅ |
create-message-any-team | ✅ | ✅ |
create-attachment-any-team | ✅ | ✅ |
create-mention-any-team | ✅ | ✅ |
create-reaction-any-team | ✅ | ✅ |
create-system-message-any-team | ✅ | ✅ |
delete-attachment-any-team | ✅ | ✅ |
delete-channel-any-team | ✖️ | ✅ |
delete-channel-owner-any-team | ✅ | ✖️ |
delete-message-any-team | ✅ | ✅ |
delete-reaction-any-team | ✅ | ✅ |
flag-message-any-team | ✅ | ✅ |
join-call-any-team | ✅ | ✅ |
mute-channel-any-team | ✅ | ✅ |
pin-message-any-team | ✅ | ✅ |
read-channel-any-team | ✅ | ✅ |
read-channel-members-any-team | ✅ | ✅ |
read-message-flags-any-team | ✅ | ✅ |
recreate-channel-any-team | ✖️ | ✅ |
recreate-channel-owner-any-team | ✅ | ✖️ |
remove-own-channel-membership-any-team | ✅ | ✅ |
run-message-action-any-team | ✅ | ✅ |
send-custom-event-any-team | ✅ | ✅ |
skip-channel-cooldown-any-team | ✅ | ✅ |
skip-message-moderation-any-team | ✅ | ✅ |
truncate-channel-any-team | ✖️ | ✅ |
truncate-channel-owner-any-team | ✅ | ✖️ |
unblock-message-any-team | ✅ | ✅ |
update-channel-any-team | ✅ | ✅ |
update-channel-cooldown-any-team | ✅ | ✅ |
update-channel-frozen-any-team | ✅ | ✅ |
update-channel-members-any-team | ✅ | ✅ |
update-message-any-team | ✅ | ✅ |
upload-attachment-any-team | ✅ | ✅ |
Scope livestream
Permission ID | global_moderator | global_admin |
---|---|---|
add-links-any-team | ✅ | ✅ |
ban-channel-member-any-team | ✅ | ✅ |
ban-user-any-team | ✅ | ✅ |
create-call-any-team | ✅ | ✅ |
create-channel-any-team | ✅ | ✅ |
create-message-any-team | ✅ | ✅ |
create-attachment-any-team | ✅ | ✅ |
create-mention-any-team | ✅ | ✅ |
create-reaction-any-team | ✅ | ✅ |
create-system-message-any-team | ✅ | ✅ |
delete-attachment-any-team | ✅ | ✅ |
delete-channel-any-team | ✖️ | ✅ |
delete-message-any-team | ✅ | ✅ |
delete-reaction-any-team | ✅ | ✅ |
flag-message-any-team | ✅ | ✅ |
join-call-any-team | ✅ | ✅ |
mute-channel-any-team | ✅ | ✅ |
pin-message-any-team | ✅ | ✅ |
read-channel-any-team | ✅ | ✅ |
read-channel-members-any-team | ✅ | ✅ |
read-message-flags-any-team | ✅ | ✅ |
recreate-channel-any-team | ✖️ | ✅ |
remove-own-channel-membership-any-team | ✖️ | ✅ |
run-message-action-any-team | ✅ | ✅ |
send-custom-event-any-team | ✅ | ✅ |
skip-channel-cooldown-any-team | ✅ | ✅ |
skip-message-moderation-any-team | ✅ | ✅ |
truncate-channel-any-team | ✖️ | ✅ |
unblock-message-any-team | ✅ | ✅ |
update-channel-any-team | ✖️ | ✅ |
update-channel-cooldown-any-team | ✅ | ✅ |
update-channel-frozen-any-team | ✅ | ✅ |
update-channel-members-any-team | ✖️ | ✅ |
update-message-any-team | ✅ | ✅ |
upload-attachment-any-team | ✅ | ✅ |
Scope team
Permission ID | global_moderator | global_admin |
---|---|---|
add-links-any-team | ✅ | ✅ |
ban-channel-member-any-team | ✅ | ✅ |
ban-user-any-team | ✅ | ✅ |
create-call-any-team | ✅ | ✅ |
create-channel-any-team | ✅ | ✅ |
create-message-any-team | ✅ | ✅ |
create-attachment-any-team | ✅ | ✅ |
create-mention-any-team | ✅ | ✅ |
create-reaction-any-team | ✅ | ✅ |
create-system-message-any-team | ✅ | ✅ |
delete-attachment-any-team | ✅ | ✅ |
delete-channel-any-team | ✖️ | ✅ |
delete-channel-owner-any-team | ✅ | ✖️ |
delete-message-any-team | ✅ | ✅ |
delete-reaction-any-team | ✅ | ✅ |
flag-message-any-team | ✅ | ✅ |
join-call-any-team | ✅ | ✅ |
mute-channel-any-team | ✅ | ✅ |
pin-message-any-team | ✅ | ✅ |
read-channel-any-team | ✅ | ✅ |
read-channel-members-any-team | ✅ | ✅ |
read-message-flags-any-team | ✅ | ✅ |
recreate-channel-any-team | ✖️ | ✅ |
recreate-channel-owner-any-team | ✅ | ✖️ |
remove-own-channel-membership-any-team | ✅ | ✅ |
run-message-action-any-team | ✅ | ✅ |
send-custom-event-any-team | ✅ | ✅ |
skip-channel-cooldown-any-team | ✅ | ✅ |
skip-message-moderation-any-team | ✅ | ✅ |
truncate-channel-any-team | ✖️ | ✅ |
truncate-channel-owner-any-team | ✅ | ✖️ |
unblock-message-any-team | ✅ | ✅ |
update-channel-any-team | ✅ | ✅ |
update-channel-cooldown-any-team | ✅ | ✅ |
update-channel-frozen-any-team | ✅ | ✅ |
update-channel-members-any-team | ✅ | ✅ |
update-message-any-team | ✅ | ✅ |
upload-attachment-any-team | ✅ | ✅ |
Scope commerce
Permission ID | global_moderator | global_admin |
---|---|---|
add-links-any-team | ✅ | ✅ |
ban-channel-member-any-team | ✅ | ✅ |
ban-user-any-team | ✅ | ✅ |
create-call-any-team | ✅ | ✅ |
create-channel-any-team | ✅ | ✅ |
create-message-any-team | ✅ | ✅ |
create-attachment-any-team | ✅ | ✅ |
create-mention-any-team | ✅ | ✅ |
create-reaction-any-team | ✅ | ✅ |
create-system-message-any-team | ✅ | ✅ |
delete-attachment-any-team | ✅ | ✅ |
delete-channel-any-team | ✖️ | ✅ |
delete-message-any-team | ✅ | ✅ |
delete-reaction-any-team | ✅ | ✅ |
flag-message-any-team | ✅ | ✅ |
join-call-any-team | ✅ | ✅ |
mute-channel-any-team | ✅ | ✅ |
pin-message-any-team | ✅ | ✅ |
read-channel-any-team | ✅ | ✅ |
read-channel-members-any-team | ✅ | ✅ |
read-message-flags-any-team | ✅ | ✅ |
recreate-channel-any-team | ✖️ | ✅ |
remove-own-channel-membership-any-team | ✅ | ✅ |
run-message-action-any-team | ✅ | ✅ |
send-custom-event-any-team | ✅ | ✅ |
skip-channel-cooldown-any-team | ✅ | ✅ |
skip-message-moderation-any-team | ✅ | ✅ |
truncate-channel-any-team | ✖️ | ✅ |
unblock-message-any-team | ✅ | ✅ |
update-channel-any-team | ✅ | ✅ |
update-channel-cooldown-any-team | ✅ | ✅ |
update-channel-frozen-any-team | ✅ | ✅ |
update-channel-members-any-team | ✅ | ✅ |
update-message-any-team | ✅ | ✅ |
upload-attachment-any-team | ✅ | ✅ |
Scope gaming
Permission ID | global_moderator | global_admin |
---|---|---|
add-links-any-team | ✅ | ✅ |
ban-channel-member-any-team | ✅ | ✅ |
ban-user-any-team | ✅ | ✅ |
create-call-any-team | ✅ | ✅ |
create-channel-any-team | ✖️ | ✅ |
create-message-any-team | ✅ | ✅ |
create-attachment-any-team | ✅ | ✅ |
create-mention-any-team | ✅ | ✅ |
create-reaction-any-team | ✅ | ✅ |
create-system-message-any-team | ✅ | ✅ |
delete-attachment-any-team | ✅ | ✅ |
delete-channel-any-team | ✖️ | ✅ |
delete-message-any-team | ✅ | ✅ |
delete-reaction-any-team | ✅ | ✅ |
flag-message-any-team | ✅ | ✅ |
join-call-any-team | ✅ | ✅ |
mute-channel-any-team | ✅ | ✅ |
pin-message-any-team | ✅ | ✅ |
read-channel-any-team | ✅ | ✅ |
read-channel-members-any-team | ✅ | ✅ |
read-message-flags-any-team | ✅ | ✅ |
recreate-channel-any-team | ✖️ | ✅ |
remove-own-channel-membership-any-team | ✅ | ✅ |
run-message-action-any-team | ✅ | ✅ |
send-custom-event-any-team | ✅ | ✅ |
skip-channel-cooldown-any-team | ✅ | ✅ |
skip-message-moderation-any-team | ✅ | ✅ |
truncate-channel-any-team | ✖️ | ✅ |
unblock-message-any-team | ✅ | ✅ |
update-channel-any-team | ✖️ | ✅ |
update-channel-cooldown-any-team | ✅ | ✅ |
update-channel-frozen-any-team | ✅ | ✅ |
update-channel-members-any-team | ✖️ | ✅ |
update-message-any-team | ✅ | ✅ |
upload-attachment-any-team | ✅ | ✅ |