Stream Video can be configured for teams/multi-tenant operation, allowing users to be organized into distinct, isolated teams. This configuration is essential for applications like Zoom or Disqus, where it's crucial that users within one team remain completely separated from others to ensure privacy and security.
Stream Video has the concept of teams for users and calls. The purpose of teams is to provide a simple way to separate different groups of users and calls 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 calls 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.
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.
In order to use Teams, your application must have multi-tenant mode enabled. You can enable multi-tenant from the dashboard (Overview screen) or using the API.
// shows the current statusconst appSettings = await client.getApp();console.log(appSettings.app.multi_tenant_enabled);// enables teamsclient.updateApp({ multi_tenant_enabled: true,});
# shows the current statusprint(client.get_app().data.app.multi_tenant_enabled)# enables teamsclient.update_app(multi_tenant_enabled=True)
// shows the current statusvar app = client.getApp().execute();System.out.println(app.getData().getApp().getMultiTenantEnabled());// enables teamsclient.updateApp(pdateAppRequest.builder().multiTenantEnabled(true).build()).execute();
Please keep in mind that enabling/disabling multi-tenant changes permission checking for your users. Do not change this on a production app without testing that your integration supports it correctly.
Calls can be associated with a team. Users can create calls 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.
Call teams allow you to ensure proper permission checking for a multi tenant application. Keep in mind that you will still need to enforce that call IDs are unique.
There are two common way to address this: generate random IDs using a random UUID, prefix the team name to the id of the call.
By default client-side user search will only return results from teams that authenticated user is a part of. API injects filter {team: {$in: [<user_teams>]}} 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 and you can search as usual and also filter by teams.
// search for users by name and teamclient.queryUsers({ payload: { filter_conditions: { name: "Nick", teams: { $in: ["red", "blue"] }, }, },});// search for users that are not part of any teamclient.queryUsers({ payload: { filter_conditions: { name: "Tom", teams: null, }, },});
# search for users by name and teamresponse = client.query_users( QueryUsersPayload( filter_conditions={ "name": {"$eq": "Nick"}, "teams": {"$in": ["red", "blue"]}, } ))# search for users that are not part of any teamresponse = client.query_users( QueryUsersPayload( filter_conditions={ "teams": None, } ))
response, err := client.QueryUsers(ctx, &getstream.QueryUsersRequest{ Payload: &getstream.QueryUsersPayload{ FilterConditions: map[string]interface{}{ "name": "Nick", "teams": map[string]interface{}{"$in": []string{"red", "blue"}}, }, }, })// search for users that are not part of any teamresponse, err = client.QueryUsers(ctx, &getstream.QueryUsersRequest{ Payload: &getstream.QueryUsersPayload{ FilterConditions: map[string]interface{}{ "teams": nil, }, },})
client.queryUsers( QueryUsersRequest.builder() .Payload( QueryUsersPayload.builder() .filterConditions( Map.of("name", "Nick", "teams", Map.of("$in", List.of("red", "blue")) ) .build() ) .build()).execute();// search for users that are not part of any teamclient.queryUsers( QueryUsersRequest.builder() .Payload( QueryUsersPayload.builder() .filterConditions(Map.of( "teams", null )) .build() ) .build()).execute();
# Search for users by name and teamPAYLOAD='{"filter_conditions": {"name": "Nick", "teams": { "$in": ["red", "blue"] }}}';ENCODED_PAYLOAD=$(echo ${PAYLOAD} | perl -MURI::Escape -lne 'print uri_escape($_)')curl -X GET "https://video.stream-io-api.com/api/v2/users?api_key=${API_KEY}&payload=${ENCODED_PAYLOAD}" \ -H "Authorization: ${TOKEN}" \ -H "stream-auth-type: jwt"Search for users that are not part of any teamPAYLOAD='{"filter_conditions": {"name": "Tom", "teams": null}}';ENCODED_PAYLOAD=$(echo ${PAYLOAD} | perl -MURI::Escape -lne 'print uri_escape($_)')curl -X GET "https://video.stream-io-api.com/api/v2/users?api_key=${API_KEY}&payload=${ENCODED_PAYLOAD}" \ -H "Authorization: ${TOKEN}" \ -H "stream-auth-type: jwt"
When using multi-tenant, the client-side query calls endpoint will only return calls that match the query and are on the same team as the authenticated user. The 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 and you can search as usual and also filter by teams.
// All callsclient.video.queryCalls();// Calls without teamclient.video.queryCalls({ filter_conditions: { team: null, },});// Calls with specific teamclient.video.queryCalls({ filter_conditions: { team: "blue", },});
# query calls by team fieldresponse = client.video.query_calls( filter_conditions={"id": call_id, "team": {"$eq": "blue"}})# retrieve calls without a teamresponse = client.video.query_calls( filter_conditions={"id": call_id, "team": {"$eq": "blue"}})