const updateResponse = await chatClient.upsertUser({
id: userID,
role: "admin",
book: "dune",
});Managing Users
The Stream user object is central to the chat system and appears in many API responses, effectively following the user throughout the platform. Only an id is required to create a user but you can store additional custom data. We recommend only storing what is necessary for Chat such as a username and image URL.
Client-side User Creation
The connectUser method automatically creates and updates the user. If you are looking to onboard your userbase lazily, this is typically a perfectly viable option.
However, it is also common to add your users to Stream before going Live and keep properties of your user base in sync with Stream. For this you’ll want to use the upsertUsers function server-side and send users in bulk.
Creating and Updating Users Server-Side
The upsertUser method creates or updates a user, replacing its existing data with the new payload (see below for partial updates). To create or update users in batches of up to 100, use the upsertUsers or partialUpdateUsers APIs, which accept an array of user objects.
Depending on the permission configuration of your application, you may also allow users to update their own user objects client-side.
client.upsert_users([{"id": user_id, "role": "admin", "book": "dune"}])client.UpsertUsers(ctx, &User{
ID: userID, Role: "admin", ExtraData: map[string]interface{}{"book": "dune"}})var user = new UserRequest
{
Id = "bob-1",
Role = Role.Admin,
};
user.SetData("book", "dune");
await userClient.UpsertManyAsync(new[] { user });await chatClient.upsertUser(user);const updateResponse = await serverClient.upsertUser({
id: userID,
role: "admin",
book: "dune",
});# require 'stream-chat'
# user object is now {id: userID, role: 'admin', book: 'dune'}
serverClient.upsert_users([{:id => user_id, :role => "admin", :book => "dune"}])var user = UserRequestObject.builder().id(userId1).role("admin").build();
User.upsert().user(user).request();Client->UpsertUsers({User});// Only Id field is required, the rest is optional
var createOrUpdateUser = new StreamUserUpsertRequest
{
Id = "my-user-id",
// BanExpires = DateTimeOffset.Now.AddDays(7),
// Banned = true,
// Invisible = true,
// Role = "user",
// Name = "David",
// Image = "image-url", // You can upload image to Stream CDN or your own
// CustomData = new StreamCustomDataRequest
//{
// { "Age", 24 },
// { "Passions", new string[] { "Tennis", "Football", "Basketball" } }
//}
};
// Upsert means: update user with a given ID or create a new one if it doesn't exist
var users = await Client.UpsertUsers(new[] { createOrUpdateUser });And for a batch of users, simply add additional entries (up to 100) into the array you pass to upsertUsers :
const updateResponse = await serverClient.upsertUsers([
{ id: userID1, role: "admin", book: "dune" },
{ id: userID2, role: "user", book: "1984" },
{ id: userID3, role: "admin", book: "Fahrenheit 451" },
]);
// each user object is updated accordinglyclient.upsert_users([
{"id": user_id1, "role": "admin", "book": "dune"},
{"id": user_id2, "role": "user", "book": "1984"},
{"id": user_id3, "role": "admin", "book": "Fahrenheit 451"},
])client.UpsertUsers(ctx,
&User{ID: userID1, Role: "admin", ExtraData: map[string]interface{}{"book": "dune"}},
&User{ID: userID2, Role: "user", ExtraData: map[string]interface{}{"book": "1984"}},
&User{ID: userID3, Role: "admin", ExtraData: map[string]interface{}{"book": "Fahrenheit 451"}},
)await userClient.UpsertManyAsync(new[] {
new UserRequest
{
Id = userId1,
Role = Role.Admin,
},
new UserRequest
{
Id = userId2,
Role = Role.User,
},
new UserRequest
{
Id = userId3,
Role = Role.Admin,
}
});client.upsert_users([
{:id => user_id1, :role => "admin", :book => "dune"},
{:id => user_id2, :role => "user", :book => "1984"},
{:id => user_id3, :role => "admin", :book => "Fahrenheit 451"},
])var user1 = UserRequestObject.builder().id(userId1).role("admin").build();
var user2 = UserRequestObject.builder().id(userId1).role("admin").build();
var user3 = UserRequestObject.builder().id(userId1).role("admin").build();
User.upsert().user(user1).user(user2).user(user3).request();var usersToCreateOrUpdate = new[]
{
new StreamUserUpsertRequest
{
Id = "my-user-id",
Role = "user",
},
new StreamUserUpsertRequest
{
Id = "my-user-id-2",
// BanExpires = DateTimeOffset.Now.AddDays(7),
// Banned = true,
// Invisible = true,
// Role = "user",
// Name = "David",
// Image = "image-url", // You can upload image to Stream CDN or your own
// CustomData = new StreamCustomDataRequest
//{
// { "Age", 24 },
// { "Passions", new string[] { "Tennis", "Football", "Basketball" } }
//}
},
};
// Upsert means: update user with a given ID or create a new one if it doesn't exist
var users = await Client.UpsertUsers(usersToCreateOrUpdate);If any user in a batch of users contains an error, the entire batch will fail, and the first error encountered will be returned.
Server-side Partial Updates
If you need to update a subset of properties for a user(s), you can use a partial update method. Both set and unset parameters can be provided to add, modify, or remove attributes to or from the target user(s). The set and unset parameters can be used separately or combined.
// partial update for a single user setting and unsetting multiple fields
const update = {
id: "userID",
set: {
role: "admin",
field: {
text: "value",
},
"field2.subfield": "test",
},
unset: ["field.unset"],
};
const response = await client.partialUpdateUser(update);
// partial update for multiple users
const updates = [
{
id: "userID",
set: {
field: "value",
},
},
{
id: "userID2",
unset: ["field.value"],
},
];
const response = await client.partialUpdateUsers(updates);# make partial update call for userID
# it set's user.role to "admin", sets user.field = {'text': 'value'}
# and user.field2.subfield = 'test'.
# NOTE:
# changing role is available only for server-side auth.
# field name should not contain dots or spaces, as dot is used as path separator.
update = {
"id": "userID",
"set": {
"role": "admin",
"field": {
"text": 'value'
},
'field2.subfield': 'test',
},
"unset": ['field.unset'],
};
# response will contain user object with updated users info
client.update_user_partial(update);
# partial update for multiple users
updates = [
{
"id": "userID",
"set": {"field": "value"}
},
{
"id": "userID2",
"unset": ["field.value"]
}
]
# response will contain object {userID => user} with updated users info
client.update_users_partial(updates)# make partial update call for userID
# it set's user.role to "admin", sets user.field = {'text': 'value'}
# and user.field2.subfield = 'test'.
# NOTE:
# changing role is available only for server-side auth.
# field name should not contain dots or spaces, as dot is used as path separator.
update = {
"id" => "userID",
"set"=> {
"role"=> "admin",
"field"=> {
"text"=> 'value'
},
'field2.subfield'=> 'test',
},
"unset"=> ['field.unset'],
};
# response will contain user object with updated users info
client.update_user_partial(update);
# partial update for multiple users
updates = [
{
"id"=> "userID",
"set"=> {"field"=> "value"}
},
{
"id"=> "userID2",
"unset"=> ["field.value"]
}
]
# response will contain object {userID => user} with updated users info
client.update_users_partial(updates)$client->partialUpdateUser([
"id" => $user["id"],
"set" => ["role" => "admin", "field2.subfield" => "test"]
]);var updateObj = UserPartialUpdateRequestObject.builder()
.id(user.getId())
.setValue("role", "admin")
.setValue("field2.subfield", "test")
.build();
User.partialUpdate().users(Arrays.asList(updateObj)).request();update := PartialUserUpdate{
ID: user.ID,
Set: map[string]interface{}{
"role": "admin",
"field": map[string]interface{}{
"text": "value",
},
},
}
resp, err := client.PartialUpdateUsers(ctx, []PartialUserUpdate{update})// Partial update is not supported in Swift SDKvar updates = new Dictionary<string, object>
{
{ "role", "admin" },
{ "field2.subfield", "test" }
};
var resp = await userClient.UpdatePartialAsync(new UserPartialRequest
{
Id = user.Id,
Set = updates,
});Partial updates support batch requests, similar to the upsertUser endpoint.
Unique Usernames
Clients can set a username, by setting the name custom field. The field is optional and by default has no uniqueness constraints applied to it, however this is configurable by setting the enforce_unique_username to either app or team.
When checking for uniqueness, the name is normalized, by removing any white-space or other special characters, and finally transforming it to lowercase. So “John Doe” is considered a duplicate of “john doe”, “john.doe”, etc.
With the setting at app, creating or updating a user fails if the username already exists anywhere in the app. With team, it only fails if the username exists within the same team.
// Enable uniqueness constraints on App level
await client.updateAppSettings({
enforce_unique_usernames: "app",
});
// Enable uniqueness constraints on Team level
await client.updateAppSettings({
enforce_unique_usernames: "team",
});# Enable uniqueness constraints on App level
client.update_app_settings(enforce_unique_usernames: 'app')
# Enable uniqueness constraints on Team level
client.update_app_settings(enforce_unique_usernames: 'team')# Enable uniqueness constraints on App level
client.update_app_settings(enforce_unique_usernames="app")
# Enable uniqueness constraints on Team level
client.update_app_settings(enforce_unique_usernames="team")// Enable uniqueness constraints on App level
App.update().enforceUniqueUsernames("app").request();
// Enable uniqueness constraints on Team level
App.update().enforceUniqueUsernames("team").request();// Enable uniqueness constraints on App level
await _appClient.UpdateAppSettingsAsync(new AppSettingsRequest
{
EnforceUniqueUsernames = UniqueUsernameEnforcementPolicy.App,
});// Enable uniqueness constraints on App level
settings := &AppSettings{EnforceUniqueUsernames: "app"}
resp, err = client.UpdateAppSettings(ctx, settings)
// Enable uniqueness constraints on Team level
settings := &AppSettings{EnforceUniqueUsernames: "team"}
resp, err = client.UpdateAppSettings(ctx, settings)// Enable uniqueness constraints on App level
$client->updateAppSettings(["enforce_unique_usernames" => "app"]);
// Enable uniqueness constraints on Team level
$client->updateAppSettings(["enforce_unique_usernames" => "team"]);// This can be set in https://dashboard.getstream.io/ -> Open your application -> Overview -> AuthenticationEnabling this setting will only enforce the constraint going forward and will not try to validate existing usernames.
Deactivate a User
To deactivate a user, Stream Chat exposes a server-side deactivateUser method. A deactivated user cannot connect to Stream Chat but will be present in user queries and channel history.
const deactivate = await client.deactivateUser(userID);
const deactivate = await client.deactivateUser(userID, {
mark_messages_deleted: true,
created_by_id: "joe",
});response = client.deactivate_user(user_id)
response = client.deactivate_user(user_id,
mark_messages_deleted=True,
created_by_id="joe")response = client.deactivate_user(user_id)
response = client.deactivate_user(user_id,
mark_messages_deleted: true,
created_by_id: "joe")$response = $this->client->deactivateUser($user["id"]);
$response = $this->client->deactivateUser($user["id"], [
"mark_messages_deleted" => true,
"created_by_id" => "joe",
]);err := client.DeactivateUser(userID, nil)
err := client.DeactivateUser(userID, map[string]interface{}{
"mark_messages_deleted": true,
"created_by_id": "joe",
})await userClient.DeactivateAsync(userId);
await userClient.DeactivateAsync(userId, true, "joe");User.deactivate(userId).createdById(userId2).request();// This is a server-side only feature, choose any of our server-side SDKs to use itDeactivate Many Users
Many users (up to 100) can be deactivated and reactivated with a single call. The operation runs asynchronously, and the response contains a task_id which can be polled using the getTask endpoint to check the status of the operation.
let resp = await serverClient.deactivateUsers([userIDs, ...], {
created_by_id: userID, // optional: define who deactivated the users
mark_messages_deleted: true // optional: the messages will be marked as deleted
})
// This is a server-side only feature, choose any of our server-side SDKs to use it| Name | Type | Description | Default | Optional |
|---|---|---|---|---|
| mark_messages_deleted | boolean | Soft deletes all of the messages sent by the user | false | ✓ |
Reactivate a User
To reinstate the user as active, use the reactivateUser method by passing the users ID as a parameter:
const reactivate = await client.reactivateUser(userID);
const reactivate = await client.reactivateUser(userID, {
restore_messages: true,
name: "I am back",
created_by_id: "joe",
});response = client.reactivate_user(user_id)
response = client.reactivate_user(user_id,
restore_messages=True,
name="I am back",
created_by_id="joe")response = client.reactivate_user(user_id)
response = client.reactivate_user(user_id,
restore_messages: true,
name: "I am back",
created_by_id: "joe")$response = $this->client->reactivateUser($user["id"]);
$response = $this->client->reactivateUser($user["id"], [
"restore_messages" => true,
"name" => "I am back",
"created_by_id" => "joe",
]);err := client.ReactivateUser(userID, nil)
err := client.ReactivateUser(userID, map[string]interface{}{
"restore_messages": true,
"name": "I am back",
"created_by_id": "joe",
})await userClient.ReactivateAsync(userId);
await userClient.ReactivateAsync(user2.Id, restoreMessages: true, name: "I am back", createdById: "joe");User.reactivate(userId).createdById(userId2).request();// This is a server-side only feature, choose any of our server-side SDKs to use itDeleting Many Users
You can delete up to 100 users and optionally all of their channels and messages using this method. First the users are marked deleted synchronously so the user will not be directly visible in the API. Then the process deletes the user and related objects asynchronously.
let response = await serverClient.deleteUsers(['userID1', 'userID2'], {
user: 'soft',
messages: 'hard',
});
response = await serverClient.getTask(response['task_id']);
if(response['status] === 'completed') {
// success!
}response = client.delete_users(
['userID1', 'userID2'],
"soft",
messages="hard"
)
response = client.get_task(response["task_id"])
if response['status'] == 'completed':
# success!
passtaskID, err := client.DeleteUsers([]string{"userID1", "userID2"}, DeleteUserOptions{
User: SoftDelete,
Messages: HardDelete,
})
if err != nil {
return err
}
resp, err := client.GetTask(taskID)
if err != nil {
return err
}
if resp.Status == "completed" {
// success!
}$response = $client->deleteUsers(
["userID1", "userID2"],
["user" => "soft", "messages" => "hard"]
);
$response = $client->getTask($response["task_id"]);
if ($response["status"] == "completed") {
// success!
};response = client.delete_users(
['userID1', 'userID2'],
user: StreamChat::SOFT_DELETE
)
response = client.get_task(response["task_id"])
if response['status'] == 'completed'
# success!var taskId = User.deleteMany(List.of(userID1, userID2))
.deleteUserStrategy(DeleteStrategy.SOFT)
.deleteMessagesStrategy(DeleteStrategy.HARD)
.request().getTaskId();
var taskStatusResponse = TaskStatus.get(taskId).request();
// "completed".equals(taskStatusResponse.status);var resp = await userClient.DeleteManyAsync(
new DeleteUsersRequest()
.WithUserIds(user1.Id, user2.Id)
.WithUserDeletionStrategy(DeletionStrategy.Hard)
.WithMessagesDeletionStrategy(DeletionStrategy.Hard)
.WithConversationsDeletionStrategy(DeletionStrategy.Hard));
var status = await taskClient.GetTaskStatusAsync(resp.TaskId);
var finished = status.Status == AsyncTaskStatus.Completed;// This is a server-side only feature, choose any of our server-side SDKs to use itThe deleteUsers method is an asynchronous API where the response contains a task_id which can be polled using the getTask endpoint to check the status of the deletions.
These are the request parameters which determine what user data is deleted:
| name | type | description | default | optional |
|---|---|---|---|---|
| user_ids | array | List of users who will be deleted | - | |
| user | enum (soft, pruning, hard) | Soft: marks user as deleted and retains all user data. Pruning: marks user as deleted and nullifies user information. Hard: deletes user completely - this requires hard option for messages and conversation as well. | - | ✓ |
| conversations | enum (soft, hard) | Soft: marks all conversation channels as deleted (same effect as Delete Channels with ‘hard’ option disabled). Hard: deletes channel and all its data completely including messages (same effect as Delete Channels with ‘hard’ option enabled). | ✓ | |
| messages | enum (soft, pruning, hard) | Soft: marks all user messages as deleted without removing any related message data. Pruning: marks all user messages as deleted, nullifies message information and removes some message data such as reactions and flags. Hard: deletes messages completely with all related information. | - | ✓ |
| new_channel_owner_id | string | Channels owned by hard-deleted users will be transferred to this userID. | - | ✓ |
When deleting a user, if you wish to transfer ownership of their channels to another user, provide that user’s ID in the new_channel_owner_id field. Otherwise, the channel owner will be updated to a system generated ID like delete-user-8219f6578a7395g
Restoring deleted users
If users are soft deleted, they can be restored using the server-side client. However, only the user’s metadata is restored; memberships, messages, reactions, etc. are not restored.
You can restore up to 100 users per call:
await client.restoreUsers(["userID1", "userID2"]);client.restore_users(['userID1', 'userID2'])// This is a server-side only feature, choose any of our server-side SDKs to use itQuerying Users
The Query Users method lets you search for users, though in many cases it’s more practical to query your own user database instead. Like other Stream query APIs, it accepts filter, sort, and options parameters.
val request = QueryUsersRequest(
filter = Filters.`in`("id", listOf("john", "jack", "jessie")),
querySort = QuerySortByField.descByName("last_active"),
offset = 0,
limit = 10,
)
client.queryUsers(request).enqueue { result ->
if (result is Result.Success) {
val users: List<User> = result.value
} else {
// Handle Result.Failure
}
}const response = await client.queryUsers(
{ id: { $in: ["john", "jack", "jessie"] } },
{ last_active: -1 },
{ limit: 10, offset: 0 },
);FilterObject filter = Filters.in("id", Arrays.asList("john", "jack", "jessie"));
QuerySorter<User> sort = QuerySortByField.descByName("last_active");
int offset = 0;
int limit = 10;
QueryUsersRequest request = new QueryUsersRequest(filter, offset, limit, sort);
client.queryUsers(request).enqueue(result -> {
if (result.isSuccess()) {
List<User> users = result.data();
} else {
// Handle result.error()
}
});let controller = chatClient.userListController(
query: .init(
filter: .in(.id, values: ["john", "jack", "jessie"]),
sort: [.init(key: .lastActivityAt, isAscending: false)],
pageSize: 10
)
)
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// access users
print(controller.users)
}
}$response = $client->queryUsers(
[ 'id' => ['$in' => ['john', 'jack', 'jessie'] ] ],
[ 'last_active' => -1 ],
[ 'limit' => 10, 'offset' => 0 ]
);final result = client.queryUsers(
filter: Filter.in_('id', ['john', 'jack', 'jessie']),
sort: [SortOption('last_active', direction: SortOption.DESC)],
pagination: PaginationParams(limit: 10, offset: 0),
);Client->QueryUsers(
FFilter::In(TEXT("id"), {TEXT("john"), TEXT("jack"), TEXT("jessie")}),
{FUserSortOption{EUserSortField::LastActive}},
false, // Presence
{{10, 0}}, // Pagination: limit, offset
[](const TArray<FUserRef>& Users)
{
// Do something with Users
});var query = QueryUserOptions.Default.WithFilter(new Dictionary<string, object>
{
{ "id", new Dictionary<string, object> { { "$in", new[]{ "john", "jack", "jessie" } } } },
});
query.Limit = 10;
query.Offset = 0;
var resp = await userClient.QueryAsync(query);client.query_users(
{"id": {"$in": ["john", "jack", "jessie"]}},
{"last_active": -1},
limit=10,
offset=0
)var filters = new IFieldFilterRule[]
{
UserFilter.Id.In("john", "jack", "jessie")
};
var sort = UsersSort.OrderByDescending(UserSortField.LastActive);
var limit = 10;
var offset = 0;
// Returns collection of IStreamUser
var users = await Client.QueryUsersAsync(filters, sort, offset, limit);client.query_users(
{"id": {"$in": ["john", "jack", "jessie"]}},
{"last_active": -1},
limit: 10,
offset: 0
)All filters use a Mongoose-style syntax; however, we do not run MongoDB on the backend, so only a subset of Mongoose queries are supported. The supported filters are described below.
Supported Filters
| Name | Type | Description | Allowed Operators |
|---|---|---|---|
| id | string | ID of the user | $eq, $gt, $gte, $lt, $lte, $in, $autocomplete |
| role | string | Role of the user | $eq, $gt, $gte, $lt, $lte, $in |
| banned | boolean | Whether the user is banned | $eq |
| shadow_banned | boolean | Whether the user is shadow banned | $eq |
| created_at | string, must be formatted as an RFC3339 timestamp | Time when the user was created | $eq, $gt, $gte, $lt, $lte, $in |
| updated_at | string, must be formatted as an RFC3339 timestamp | Time when the user was updated | $eq, $gt, $gte, $lt, $lte, $in |
| last_active | string, must be formatted as an RFC3339 timestamp | Time when the user was last active | $eq, $gt, $gte, $lt, $lte, $in, $exists |
| teams | string | Teams the user belongs to | $eq, $contains |
| name | string | Name of the user | $eq, $autocomplete |
| username | string | Username of the user | $eq, $autocomplete |
| custom properties | string, boolean, int | Custom user properties | $eq, $gt, $gte, $lt, $lte, $in |
Supported Sort
You can sort results by specifying a field and direction (1 for ascending, -1 for descending).
| Name | Description |
|---|---|
| id | User ID |
| created_at | Time when the user was created |
| updated_at | Time when the user was updated |
| last_active | Time when the user was last active |
| role | Role of the user |
Supported Options
The options for the queryUsers method are primarily used for pagination.
| Name | Type | Description | Default | Optional |
|---|---|---|---|---|
| limit | integer | Number of users to return | 30 | ✓ |
| offset | integer | Offset for pagination | 0 | ✓ |
| id_gt | string | ID-based pagination. Return IDs greater than this ID. If this is not empty, the default sort order will be [{id: -1}]. | - | ✓ |
| id_gte | string | ID-based pagination. Return IDs greater than or equal to this ID. If this is not empty, the default sort order will be [{id: -1}]. | - | ✓ |
| id_lt | string | ID-based pagination. Return IDs less than this ID. If this is not empty, the default sort order will be [{id: -1}]. | - | ✓ |
| id_lte | string | ID-based pagination. Return IDs less than or equal to this ID. If this is not empty, the default sort order will be [{id: -1}]. | - | ✓ |
| include_deactivated_users | boolean | Include deactivated users in the response | - | ✓ |
The maximum offset value is 1000.
Querying with Autocomplete
You can use the $autocomplete operator to search for users by name or ID with partial matching.
val request = QueryUsersRequest(
filter = Filters.autocomplete("name", "ro"),
offset = 0,
limit = 10,
)
client.queryUsers(request).enqueue { /* ... */ }const response = await client.queryUsers({
name: { $autocomplete: "ro" },
});$response = $client->queryUsers(
[
'name' => ['$autocomplete' => 'ro']
]);let controller = chatClient.userListController(
query: .init(filter: .autocomplete(.name, text: "ro"))
)
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// access users
print(controller.users)
}
}FilterObject filter = Filters.autocomplete("name", "ro");
int offset = 0;
int limit = 10;
QueryUsersRequest request = new QueryUsersRequest(filter, offset, limit);
client.queryUsers(request).enqueue(result -> { /* ... */ });final _result = client.queryUsers(
filter: Filter.autoComplete('name', 'ro'),
);Client->QueryUsers(
FFilter::Autocomplete(TEXT("name"), TEXT("ro")),
{}, // Sort
true, // Presence
{}, // Pagination options
[](const TArray<FUserRef>& Users)
{
// Do something with Users
});var resp = await _userClient.QueryAsync(QueryUserOptions.Default.WithFilter(new Dictionary<string, object>
{
{ "name", new Dictionary<string, object> { { "$autocomplete", "ro" } } },
}));client.query_users({"name": {"$autocomplete": "ro"}})client.query_users({"name": {"$autocomplete": "ro"}})var filters = new IFieldFilterRule[]
{
UserFilter.Name.Autocomplete("Ro")
};
// Returns collection of IStreamUser
var users = await Client.QueryUsersAsync(filters);Querying Inactive Users
You can use the last_active field with the $exists operator to find users who have never connected. Use $exists: false for users who have never been active, or $exists: true for users who have connected at least once.
const response = await client.queryUsers({
id: { $in: [activeUser, neverActiveUser] },
last_active: { $exists: false },
});users = client.query_users({"last_active": {"$exists": False}})