Multi-Tenant & Teams

Last Edit: Aug 08 2020

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 can be configured so that a user can only search for users from the same team.

User teams and channel team can only be changed using server-side auth. This ensures users can't change their own team membership.

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.

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.


const client = new StreamChat('YOUR_API_KEY', 'YOUR_API_SECRET');

// switch your application to v2 permissions
await client.updateAppSettings({
	multi_tenant_enabled: true,
});
                    
You only need to activate multi-tenant once per application. Make sure to do this before using teams.

User teams

When using teams, users must be created from your back-end and specify which teams they are a member of. (Note this is only allowed server side)


// creates or updates a user from backend to be part of the "red" and "blue" teams
client.updateUser({id, teams: ["red", "blue"]});
                    

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.


// watches the channel red-general from team red
client.channel("messaging", "red-general", {team: "red"}).create()
                    

// watches the channel red-general from team red

Map<String, Object> extraData = new HashMap<>();
extraData.put("team", "red");
client.createChannel("messaging", "red-general", extraData).enqueue(result -> {

    if (result.isSuccess()) {
        Channel channel = result.data();
    } else {
        ChatError error = result.error();
    }

    return Unit.INSTANCE;
});
                    

// watches the channel red-general from team red

val extraData = mutableMapOf<String, Any>()
extraData["team"] = "red"
client.createChannel("messaging", "red-general", extraData)
    .enqueue { result ->
        if (result.isSuccess) {
            val channel = result.data()
        } else {
            val error = result.error()
        }
    }
                    

let channel = Client.shared.channel(type: .messaging, id: "red-general", team: "red")
channel.create { (result) in
    // handle result
}
                    
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")

By default the user search endpoint allows users to search for any other user, applications in multi-tenant mode are required to specify a team filter when performing user searches client-side.

Because users can belong to multiple teams, you need to use the $contains operator for this match.


// search for users with name Jordan that are part of the red team
client.queryUsers({
   $and: [
      { name: { $eq: "Jordan" } },
      { teams: { $contains: "red" } }
   ],
});
                    

Client.shared.queryUsers(filter: .contains("teams", "blue")) { (result) in
   // handle result
}
                    

// search for users with name Jordan that are part of the red team

FilterObject filter = and(
        eq("name", "Jordan"),
        eq("teams", Filters.contains("red"))
);

int offset = 0;
int limit = 1;
QuerySort sort = null;
boolean presence = false;

client.queryUsers(new QueryUsersRequest(filter, offset, limit, sort, presence)).enqueue(result -> {
    if (result.isSuccess()) {
        List<User> users = result.data();
    } else {
        ChatError error = result.error();
    }
    return Unit.INSTANCE;
});
                    

// search for users with name Jordan that are part of the red team

val filter = and(
    eq("name", "Jordan"),
    eq("teams", contains("red"))
)
val offset = 0
val limit = 1
client.queryUsers(QueryUsersRequest(filter, offset, limit))
    .enqueue { result ->
        if (result.isSuccess) {
            val users = result.data()
        } else {
            val error = result.error()
        }
    }
                    
If a user tries to search for users from other teams it will receive an error from the Chat API endpoint.

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. This happens automatically on Stream Chat side and cannot be circumvented by users.

Server-side you can use query channels to get channels from any team and you can filter them using the team field as well.


// server-side you can query channels from any team
const otherFilters = {};
client.queryChannels({team: "red-team", ...otherFilters}, sort, options);