Many applications with chat functionality serve multiple customers or organizations. For example, platforms like Slack or SaaS applications like InVision need to ensure that messages remain private between different customer organizations. Stream Chat addresses this through multi-tenant mode, which organizes users into separate teams that operate independently. For more details on multitenancy setup, see our Multitenancy documentation.
This guide explains how you can provide moderation experience for your customers (tenants) by using team-level moderation policies. This will allow you to build a multitenant dashboard for your customers to manage their moderation policies and also allow them to review flagged content for their team. Basically we are providing you the APIs that Stream users to build the dashboard for their customers and you can use these APIs to build the dashboard for your customers however you want.
You can create moderation configs at the team level by setting the team field in the UpsertConfig API.
Following moderation configuration will be used for all the messages sent to messaging channel type within the team team-a.
Only team admins and moderators can manage team-level policies
Client side access to API allows you to give control to your customers (tenants) to manage their own moderation policies. This is particularly useful when:
Building a self-service moderation dashboard
Allowing teams to customize their moderation settings
The server-side query provides comprehensive access to all moderation configs:
// Get all the moderation configs for all the teamsconst { configs, next } = await client.moderation.queryModerationConfigs({ limit: 2,});// Get all the moderation configs for the team `team-a`const { configs, next } = await client.moderation.queryModerationConfigs({ filter: { team: "team-a" }, sort: [{ field: "created_at", direction: -1 }], limit: 2,});
# Get all the moderation configs for all the teamsclient.moderation().query_moderation_configs(limit=2)# Get all the moderation configs for the team `team-a`client.moderation().query_moderation_configs( filter={"team": "team-a"}, sort=[{"field": "created_at", "direction": -1}], limit=2,)
// Get all the moderation configs for all the teamsclient.Moderation().QueryModerationConfigs(ctx, &getstream.QueryModerationConfigsRequest{ Limit: getstream.PtrTo(2),})// Get all the moderation configs for the team `team-a`client.Moderation().QueryModerationConfigs(ctx, &getstream.QueryModerationConfigsRequest{ Filter: map[string]interface{}{"team": "team-a"}, Sort: []getstream.SortParam{{Field: "created_at", Direction: getstream.PtrTo(-1)}}, Limit: getstream.PtrTo(2),})
// Get all the moderation configs for all the teamsclient.moderation().queryModerationConfigs(QueryModerationConfigsRequest.builder() .limit(2) .build()) .execute();// Get all the moderation configs for the team `team-a`client.moderation().queryModerationConfigs(QueryModerationConfigsRequest.builder() .filter(Map.of("team", "team-a")) .sort(List.of(SortParam.builder().field("created_at").direction(-1).build())) .limit(2) .build()) .execute();
// Get all the moderation configs for all the teams$client->moderation()->queryModerationConfigs(new QueryModerationConfigsRequest( limit: 2,));// Get all the moderation configs for the team `team-a`$client->moderation()->queryModerationConfigs(new QueryModerationConfigsRequest( filter: ['team' => 'team-a'], sort: [new SortParam(field: 'created_at', direction: -1)], limit: 2,));
# Get all the moderation configs for all the teamsclient.moderation.query_moderation_configs(GetStream::Generated::Models::QueryModerationConfigsRequest.new( limit: 2,))# Get all the moderation configs for the team `team-a`client.moderation.query_moderation_configs(GetStream::Generated::Models::QueryModerationConfigsRequest.new( filter: { "team" => "team-a" }, sort: [GetStream::Generated::Models::SortParam.new(field: "created_at", direction: -1)], limit: 2,))
// Get all the moderation configs for all the teamsawait client.Moderation.QueryModerationConfigsAsync(new QueryModerationConfigsRequest{ Limit = 2,});// Get all the moderation configs for the team `team-a`await client.Moderation.QueryModerationConfigsAsync(new QueryModerationConfigsRequest{ Filter = new Dictionary<string, object> { { "team", "team-a" } }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 }, }, Limit = 2,});
Client-side queries are restricted to the team context of the authenticated user:
// Get all the moderation configs for the team that the user belongs toconst { configs, next } = await client.moderation.queryModerationConfigs({ limit: 2,});
# Get all the moderation configs for the team that the user belongs toclient.moderation().query_moderation_configs(limit=2)
// Get all the moderation configs for the team that the user belongs toclient.Moderation().QueryModerationConfigs(ctx, &getstream.QueryModerationConfigsRequest{ Limit: getstream.PtrTo(2),})
// Get all the moderation configs for the team that the user belongs toclient.moderation().queryModerationConfigs(QueryModerationConfigsRequest.builder() .limit(2) .build()) .execute();
// Get all the moderation configs for the team that the user belongs to$client->moderation()->queryModerationConfigs(new QueryModerationConfigsRequest( limit: 2,));
# Get all the moderation configs for the team that the user belongs toclient.moderation.query_moderation_configs(GetStream::Generated::Models::QueryModerationConfigsRequest.new( limit: 2,))
// Get all the moderation configs for the team that the user belongs toawait client.Moderation.QueryModerationConfigsAsync(new QueryModerationConfigsRequest{ Limit = 2,});
Server-side access provides complete control over the review queue:
// Get all the review queue for all the teamsconst { reviews, next } = await client.moderation.queryReviewQueue({ limit: 2,});// Get all the review queue for the team `team-a`const { reviews, next } = await client.moderation.queryReviewQueue({ filter: { teams: "team-a" }, sort: [{ field: "created_at", direction: -1 }], limit: 2,});// Get all the review queue for the team `team-a` and `team-b`const { reviews, next } = await client.moderation.queryReviewQueue({ filter: { teams: { $in: ["team-a", "team-b"] } }, sort: [{ field: "created_at", direction: -1 }], limit: 2,});
# Get all the review queue for all the teamsclient.moderation().query_review_queue(limit=2)# Get all the review queue for the team `team-a`client.moderation().query_review_queue( filter={"teams": "team-a"}, sort=[{"field": "created_at", "direction": -1}], limit=2,)# Get all the review queue for the team `team-a` and `team-b`client.moderation().query_review_queue( filter={"teams": {"$in": ["team-a", "team-b"]}}, sort=[{"field": "created_at", "direction": -1}], limit=2,)
// Get all the review queue for all the teamsclient.Moderation().QueryReviewQueue(ctx, &getstream.QueryReviewQueueRequest{ Limit: getstream.PtrTo(2),})// Get all the review queue for the team `team-a`client.Moderation().QueryReviewQueue(ctx, &getstream.QueryReviewQueueRequest{ Filter: map[string]interface{}{"teams": "team-a"}, Sort: []getstream.SortParam{{Field: "created_at", Direction: getstream.PtrTo(-1)}}, Limit: getstream.PtrTo(2),})// Get all the review queue for the team `team-a` and `team-b`client.Moderation().QueryReviewQueue(ctx, &getstream.QueryReviewQueueRequest{ Filter: map[string]interface{}{"teams": map[string]interface{}{"$in": []string{"team-a", "team-b"}}}, Sort: []getstream.SortParam{{Field: "created_at", Direction: getstream.PtrTo(-1)}}, Limit: getstream.PtrTo(2),})
// Get all the review queue for all the teamsclient.moderation().queryReviewQueue(QueryReviewQueueRequest.builder() .limit(2) .build()) .execute();// Get all the review queue for the team `team-a`client.moderation().queryReviewQueue(QueryReviewQueueRequest.builder() .filter(Map.of("teams", "team-a")) .sort(List.of(SortParam.builder().field("created_at").direction(-1).build())) .limit(2) .build()) .execute();// Get all the review queue for the team `team-a` and `team-b`client.moderation().queryReviewQueue(QueryReviewQueueRequest.builder() .filter(Map.of("teams", Map.of("$in", List.of("team-a", "team-b")))) .sort(List.of(SortParam.builder().field("created_at").direction(-1).build())) .limit(2) .build()) .execute();
// Get all the review queue for all the teams$client->moderation()->queryReviewQueue(new QueryReviewQueueRequest( limit: 2,));// Get all the review queue for the team `team-a`$client->moderation()->queryReviewQueue(new QueryReviewQueueRequest( filter: ['teams' => 'team-a'], sort: [new SortParam(field: 'created_at', direction: -1)], limit: 2,));// Get all the review queue for the team `team-a` and `team-b`$client->moderation()->queryReviewQueue(new QueryReviewQueueRequest( filter: ['teams' => ['$in' => ['team-a', 'team-b']]], sort: [new SortParam(field: 'created_at', direction: -1)], limit: 2,));
# Get all the review queue for all the teamsclient.moderation.query_review_queue(GetStream::Generated::Models::QueryReviewQueueRequest.new( limit: 2,))# Get all the review queue for the team `team-a`client.moderation.query_review_queue(GetStream::Generated::Models::QueryReviewQueueRequest.new( filter: { "teams" => "team-a" }, sort: [GetStream::Generated::Models::SortParam.new(field: "created_at", direction: -1)], limit: 2,))# Get all the review queue for the team `team-a` and `team-b`client.moderation.query_review_queue(GetStream::Generated::Models::QueryReviewQueueRequest.new( filter: { "teams" => { "$in" => ["team-a", "team-b"] } }, sort: [GetStream::Generated::Models::SortParam.new(field: "created_at", direction: -1)], limit: 2,))
// Get all the review queue for all the teamsawait client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Limit = 2,});// Get all the review queue for the team `team-a`await client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Filter = new Dictionary<string, object> { { "teams", "team-a" } }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 }, }, Limit = 2,});// Get all the review queue for the team `team-a` and `team-b`await client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Filter = new Dictionary<string, object> { { "teams", new Dictionary<string, object> { { "$in", new[] { "team-a", "team-b" } } } }, }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 }, }, Limit = 2,});
The client-side review queue access enables building custom moderation dashboards:
// Get all the review queue for all the teams that the user belongs to.const { reviews, next } = await client.moderation.queryReviewQueue({ limit: 2,});// Get all the review queue for the team `team-a`// This will return all the review queue for the team `team-a`, only if the user belongs to `team-a`const { reviews, next } = await client.moderation.queryReviewQueue({ filter: { team: "team-a" }, sort: [{ field: "created_at", direction: -1 }], limit: 2,});
# Get all the review queue for all the teams that the user belongs to.client.moderation().query_review_queue(limit=2)# Get all the review queue for the team `team-a`client.moderation().query_review_queue( filter={"team": "team-a"}, sort=[{"field": "created_at", "direction": -1}], limit=2,)
// Get all the review queue for all the teams that the user belongs to.client.Moderation().QueryReviewQueue(ctx, &getstream.QueryReviewQueueRequest{ Limit: getstream.PtrTo(2),})// Get all the review queue for the team `team-a`client.Moderation().QueryReviewQueue(ctx, &getstream.QueryReviewQueueRequest{ Filter: map[string]interface{}{"team": "team-a"}, Sort: []getstream.SortParam{{Field: "created_at", Direction: getstream.PtrTo(-1)}}, Limit: getstream.PtrTo(2),})
// Get all the review queue for all the teams that the user belongs to.client.moderation().queryReviewQueue(QueryReviewQueueRequest.builder() .limit(2) .build()) .execute();// Get all the review queue for the team `team-a`client.moderation().queryReviewQueue(QueryReviewQueueRequest.builder() .filter(Map.of("team", "team-a")) .sort(List.of(SortParam.builder().field("created_at").direction(-1).build())) .limit(2) .build()) .execute();
// Get all the review queue for all the teams that the user belongs to.$client->moderation()->queryReviewQueue(new QueryReviewQueueRequest( limit: 2,));// Get all the review queue for the team `team-a`$client->moderation()->queryReviewQueue(new QueryReviewQueueRequest( filter: ['team' => 'team-a'], sort: [new SortParam(field: 'created_at', direction: -1)], limit: 2,));
# Get all the review queue for all the teams that the user belongs to.client.moderation.query_review_queue(GetStream::Generated::Models::QueryReviewQueueRequest.new( limit: 2,))# Get all the review queue for the team `team-a`client.moderation.query_review_queue(GetStream::Generated::Models::QueryReviewQueueRequest.new( filter: { "team" => "team-a" }, sort: [GetStream::Generated::Models::SortParam.new(field: "created_at", direction: -1)], limit: 2,))
// Get all the review queue for all the teams that the user belongs to.await client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Limit = 2,});// Get all the review queue for the team `team-a`await client.Moderation.QueryReviewQueueAsync(new QueryReviewQueueRequest{ Filter = new Dictionary<string, object> { { "team", "team-a" } }, Sort = new List<SortParam> { new SortParam { Field = "created_at", Direction = -1 }, }, Limit = 2,});
Consider implementing pagination when displaying review queue items to improve performance and user experience. The next parameter can be used to fetch subsequent pages of results.
Team level blocklists are behind a feature flag and are not enabled by default. Please reach out to support if you are interested in enabling this feature.
Team level blocklists gives your customers the ability to manage blocklists for their team. These blocklists can be used in moderation policies to flag or remove messages that contain specific words or phrases.
CRUD APIs to maintain team level blocklists are accessible on client side integration. This means that you can build a custom moderation dashboard for your teams to maintain the blocklists. Its important to note that team level blocklists can only be created/updated/deleted by team admins and moderators who have the following permissions:
Maximum 20,000 blocklists per app (including global blocklists)
Maximum 200,000 words in total across all blocklists of an app
Since global (non-team) blocklists can be used in team-level moderation configs, it's recommended to maintain global blocklists for common scenarios shared across teams.
await client.CreateBlockListAsync(new CreateBlockListRequest{ Name = "myblocklist", Team = "team-a", Words = new List<string> { "bad-word-1", "bad-word-2" }, Type = "word",});
You can list both global and team-specific blocklists:
// List global blocklistsconst globalBlocklists = await client.listBlocklists();// List team specific blocklistsconst teamBlocklists = await client.listBlocklists({ team: "team-a" });
# List global blocklistsclient.list_blocklists()# List team specific blocklistsclient.list_blocklists(team="team-a")
// List global blocklistsclient.ListBlocklists(ctx, &getstream.ListBlocklistsRequest{})// List team specific blocklistsclient.ListBlocklists(ctx, &getstream.ListBlocklistsRequest{ Team: getstream.PtrTo("team-a"),})
// List global blocklistsclient.listBlocklists().execute();// List team specific blocklistsclient.listBlocklists(ListBlocklistsRequest.builder() .team("team-a") .build()) .execute();
// List global blocklists$client->listBlocklists();// List team specific blocklists$client->listBlocklists(new ListBlocklistsRequest(team: 'team-a'));
# List global blocklistsclient.list_blocklists# List team specific blocklistsclient.list_blocklists(GetStream::Generated::Models::ListBlocklistsRequest.new(team: "team-a"))
// List global blocklistsawait client.ListBlocklistsAsync();// List team specific blocklistsawait client.ListBlocklistsAsync(new ListBlocklistsRequest{ Team = "team-a",});