# Upgrade stream-chat to v10

This page lists what changed between v9 and v10 of `stream-chat`, and what to change in your app. It covers apps that call `stream-chat` directly: vanilla JavaScript, Angular, Vue and Svelte apps, custom UIs, and Node scripts or bots that connect as a user.

>
> **Note:** If your app uses `stream-chat-react` or `stream-chat-react-native`, follow [Upgrade to v15](https://getstream.io/chat/docs/sdk/react/release-guides/upgrade-to-v15/) or [Upgrade to v10](https://getstream.io/chat/docs/sdk/react-native/release-guides/upgrade-to-v10/) instead. They upgrade `stream-chat` together with the UI SDK. Use this page as well for the calls your app makes to `stream-chat` directly.
>

The complete list of changes, with every renamed type, is [stream-chat v9 to v10 breaking changes](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md) in the `stream-chat` repository. This page covers the changes most apps meet, and links to that list for the rest.

## Quick migration checklist

1. **Upgrade Node and the package.** Install with Node 22.18 or later, then `npm install stream-chat@10`. See [Install and runtime](#install-and-runtime).
2. **Move server-side code to `@stream-io/node-sdk`.** `stream-chat` 10 no longer takes an API secret. See [Server-side code](#server-side-code).
3. **Move client options under `client`.** See [Creating the client](#creating-the-client).
4. **Update how you read data.** Custom fields are under `custom`, server timestamps are nanosecond numbers, and sorts are arrays. See [Data shapes](#data-shapes).
5. **Replace `connection.changed` listeners** with the connection stores. See [Connection status](#connection-status).
6. **Pass request objects to client and channel methods,** and replace `client.queryChannels()` with `client.queryChannelsAndHydrate()` where you need `Channel` objects. See [Client methods](#client-methods).
7. **Read messages from the message paginator,** not from `channel.state`. See [Channel state and the message list](#channel-state-and-the-message-list).
8. **Rewrite channel lists** with a `ChannelPaginator` registered on `client.channelManager`. See [Channel lists](#channel-lists).
9. **Activate the channels and threads on screen,** so they recover after a reconnect. See [Activate what is on screen](#activate-what-is-on-screen).
10. **Walk [Breaks that compile](#breaks-that-compile).** Several changes compile, and plain JavaScript gets no compile errors at all.

## AI-assisted migration

The `stream-chat` repository has a migration guide written for coding agents. It lists a detection grep for every change, applies them in order, and ends with a verification step. To drive the migration with an AI assistant, use a prompt like this:

```text
Migrate this app from stream-chat v9 to v10.

Use the installed package in node_modules as the source of truth.
Before using any symbol, verify it in node_modules/stream-chat/dist/types.
Follow this migration guide:
- https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/ai-migration-v9-to-v10.md
Look up anything it does not cover in:
- https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md

Do the migration in phases:
1. preflight: record the installed versions, the Node version, and the baseline typecheck, lint and test results
2. install stream-chat 10, and move server-side code to @stream-io/node-sdk
3. run the guide's detection greps, and apply the sections that have hits
4. walk the "Breaks that compile" table
5. verify with the typecheck, lint, tests and the leftover-identifier grep

Stop after each phase and report the exact changes and remaining errors.
```

## What's new

The [state pages](https://getstream.io/chat/docs/javascript/state-overview/) describe how v10 holds data:

- [State overview](https://getstream.io/chat/docs/javascript/state-overview/): stores, subscriptions, and the paginators every list is built on.
- [Messages](https://getstream.io/chat/docs/javascript/state-messages/): the message paginator, loading older messages, and jumping to a message.
- [Channel](https://getstream.io/chat/docs/javascript/state-channel/) and [Thread](https://getstream.io/chat/docs/javascript/state-thread/): their stores, read state, and activation.
- [Channel list](https://getstream.io/chat/docs/javascript/state-channel-list/) and [Thread list](https://getstream.io/chat/docs/javascript/state-thread-list/): `ChannelPaginator`, `client.channelManager`, and `client.threads.paginator`.
- [Connection](https://getstream.io/chat/docs/javascript/state-connection/): the device's network and the WebSocket as two separate stores, and recovery after a reconnect.
- [Message composer](https://getstream.io/chat/docs/javascript/state-composer/): sending messages with an optimistic update.

## Install and runtime

`stream-chat` 10 requires Node 22.18 or later in its `engines` field. Use it wherever you install or build, including CI and Docker images, even for a browser app. Yarn 1 refuses to install on an older Node, and npm and pnpm print a warning.

v10 no longer depends on `ws`, `jsonwebtoken`, `form-data`, `base64-js` or `isomorphic-ws`. If your app imports one of them, add it to your own `package.json`. Bundler shims you added for v9, such as webpack `resolve.fallback` entries for `crypto`, `https`, `zlib` or `ws`, can be removed.

With `skipLibCheck: false` in your `tsconfig`, the v10 declarations need `@types/node` and the `dom` lib. Keep `skipLibCheck: true`, or add both.

### WebSocket on Node

Browsers and React Native are unaffected. On Node, the client uses the global `WebSocket` that Node 22 provides. To use `ws` anyway, pass it as the socket implementation:

```ts
import WS from "ws";
import { StreamChat } from "stream-chat";

const client = new StreamChat(apiKey, {
  client: {
    allowServerSideConnect: true,
    wsConnection: { webSocketImpl: WS as unknown as typeof WebSocket },
  },
});
```

### StateStore moved to its own package

`StateStore`, `MergedStateStore`, `isPatch` and the `Unsubscribe`, `Patch` and `ValueOrPatch` types are no longer exported from `stream-chat`. Import them from `@stream-io/state-store`, add that package to your dependencies with the range `stream-chat` declares, and keep one copy of it installed. A second copy makes every `StateStore` type incompatible with the ones `stream-chat` returns.

```diff
- import { StateStore, type Unsubscribe } from "stream-chat";
+ import { StateStore, type Unsubscribe } from "@stream-io/state-store";
```

## Server-side code

`stream-chat` 10 is client-side only. These are removed:

- the `secret` constructor argument, `client.secret` and `client.createToken()`
- the webhook, SQS and SNS helpers, such as `verifyWebhook`, `verifySignature`, `parseSqs` and `parseSns`
- about 90 admin methods: app settings, push providers, user administration, channel types, commands, roles and permissions, exports and imports, campaigns, segments, batch updates and retention
- the `user_id` "on behalf of" overrides on every method. The connected user is the one who acts

Move that code to [`@stream-io/node-sdk`](https://getstream.io/chat/docs/node/). Don't port it onto `stream-chat` 10, and don't keep a secret in code that imports `stream-chat`.

In v9:

```ts
const server = new StreamChat(apiKey, apiSecret);
const token = server.createToken("jane");
```

In v10:

```ts
import { StreamClient } from "@stream-io/node-sdk";

const server = new StreamClient(apiKey, apiSecret);
const token = server.generateUserToken({
  user_id: "jane",
  validity_in_seconds: 60 * 60,
});
```

The [server-only methods](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#server-only-methods-removed) entry maps each removed method to its `@stream-io/node-sdk` replacement. `client.revokeTokens()` still exists, but revokes only the connected user's tokens. App-wide revocation moved to `@stream-io/node-sdk` too.

## Creating the client

`new StreamChat(apiKey, config?)` and `StreamChat.getInstance(apiKey, config?)` take one configuration tree, the same tree `client.config.set()` takes. Most of the options v9 took at the top level now go under `client`.

In v9:

```ts
const client = StreamChat.getInstance(apiKey, {
  timeout: 6000,
  enableWSFallback: true,
  recoverStateOnReconnect: false,
  isLocalUnreadCountEnabled: true,
});
```

In v10:

```ts
import { StreamChat } from "stream-chat";

const client = StreamChat.getInstance(apiKey, {
  client: {
    axiosRequestConfig: { timeout: 6000 },
    connectionRecovery: { enabled: false },
    wsConnection: { enableWSFallback: true },
  },
  channel: { readEvents: { localUnreadCountEnabled: true } },
});
```

>
> **Warning:** In plain JavaScript, a v9 options object still runs. The client ignores its keys and logs a warning for each, so the options silently stop applying. A v9 call that passes a secret logs one warning per character of the secret.
>

Where the v9 options went:

| v9 option                                                                                     | v10 setting                                            |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `timeout`, `headers`, `withCredentials`, `httpsAgent` and the other top-level `axios` options | `client.axiosRequestConfig`                            |
| `axiosRequestConfig`                                                                          | `client.axiosRequestConfig`                            |
| `baseURL`, `browser`, `allowServerSideConnect`, `persistUserOnConnectionFailure`              | `client.<same name>`                                   |
| `enableWSFallback`                                                                            | `client.wsConnection.enableWSFallback`                 |
| `wsUrlParams`                                                                                 | `client.wsConnection.urlParams`                        |
| `recoverStateOnReconnect`                                                                     | `client.connectionRecovery.enabled`                    |
| `isLocalUnreadCountEnabled`                                                                   | `channel.readEvents.localUnreadCountEnabled`           |
| `notifications`                                                                               | `client.notificationManager`                           |
| `logger`                                                                                      | removed. See [Logging](#logging)                       |
| `device`                                                                                      | removed. Call `client.createDevice()` after connecting |
| `warmUp`, `enableInsights`, `disableCache`                                                    | removed                                                |

The client properties that mirrored options are gone too:

| v9                                           | v10                                                                                 |
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
| `client.options.x`                           | `client.config.getConfig("client")?.x`                                              |
| `client.recoverStateOnReconnect`             | `client.connectionRecovery.config.enabled`                                          |
| `client.defaultWSTimeout`                    | `client.wsConnection.config.connectTimeoutMs`                                       |
| `client.persistUserOnConnectionFailure = v`  | `client.config.setConfig("client", { persistUserOnConnectionFailure: v })`          |
| `client.userID`, `client.clientID`           | `client.userId`, `client.clientId`. The old names are deprecated                    |
| `client.setMessageComposerSetupFunction(fn)` | `client.config.setSetupFunction("messageComposer", fn)`. The old name is deprecated |

A few behaviors changed with them:

- `persistUserOnConnectionFailure: true` now keeps the socket retrying after a network failure, where v9 closed it. Call `client.closeConnection()` yourself if your app relied on the socket stopping.
- The SDK's auth headers always win over headers you set, and there is no default keep-alive agent on Node. Pass `httpsAgent` if you relied on it.
- `client.devToken()` and string tokens fail for user ids with non-ASCII characters or `>`, `?`, `~`, because the token is decoded with the global `atob`. Give those users a token provider: `client.connectUser(user, () => fetchToken())`.

## Logging

`options.logger`, `client.logger` and the `Logger` type are removed. Logging goes through `chatLoggerSystem`, exported from `stream-chat`, and configured once for the process.

The default changed: v10 logs `info` and above to the console, where v9 was silent.

```ts
import { chatLoggerSystem } from "stream-chat";

// Send warnings and errors to your own logger
chatLoggerSystem.configureLoggers({
  default: {
    level: "warn",
    sink: (level, message, ...data) => myLog(level, message, data),
  },
});

// Or keep the v9 silence
chatLoggerSystem.configureLoggers({ default: { sink: () => {} } });
```

Configure it before you create the client. `LogLevel` is `"trace" | "debug" | "info" | "warn" | "error"`, so an exhaustive `switch` over it needs the two new levels.

## Data shapes

`stream-chat` 10 types its data from the API's specification. Three changes reach almost every app.

### Custom fields are under custom

Your own fields on a message, a channel, a user, an attachment, an event or another object are no longer spread at the top level. They're in the object's `custom` field. Plain JavaScript that reads a top-level custom field gets `undefined`, with no error.

| v9                                        | v10                                                                |
| ----------------------------------------- | ------------------------------------------------------------------ |
| `message.priority`                        | `message.custom.priority`                                          |
| `channel.data?.name`                      | `channel.data?.custom?.name`                                       |
| `channel.data?.image`                     | `channel.data?.custom?.image`                                      |
| `attachment.mime_type`, `file_size`       | `attachment.custom.mime_type`, `attachment.custom.file_size`       |
| `attachment.duration`, `waveform_data`    | `attachment.custom.duration`, `attachment.custom.waveform_data`    |
| `channel.sendMessage({ text, priority })` | `channel.sendMessage({ message: { text, custom: { priority } } })` |
| `client.channel(type, id, { name })`      | `client.channel(type, id, { custom: { name } })`                   |

The channel's name and image are the reads most apps have to fix: in headers, list previews and search results.

Two places stay flat:

- **Filters.** Filter a channel's name as `name`. Filter other custom fields as `"custom.<field>"`. See [Sorts and filters](#sorts-and-filters).
- **Partial updates.** The `set` and `unset` of `client.updateUsersPartial()` and `channel.updatePartial()` take built-in and custom keys side by side. Don't wrap them in `custom`.

```ts
await channel.updatePartial({ set: { name: "General", color: "blue" } });
```

The `declare module "stream-chat"` augmentation of the `Custom*Data` interfaces still works, and now types `custom`. `stream-chat` doesn't declare a channel's `name` or `image`, so declare them on `CustomChannelData` if you read them:

```ts title="stream-custom-data.d.ts"
import "stream-chat";

declare module "stream-chat" {
  interface CustomChannelData {
    image?: string;
    name?: string;
  }

  interface CustomMessageData {
    priority?: string;
  }
}
```

`CustomCommandData` is removed: a command's `name` and `set` are plain strings. Delete its augmentation and any import of it. A leftover augmentation still compiles, so search for it.

### Timestamps are nanosecond numbers

Every date the server sends, in responses, events and state, is a `TimestampNS`: Unix time in nanoseconds, as a `number`. In v9 it was an ISO string, and formatted messages carried `Date` objects. Dates you send in a request, such as `remind_at`, are `Date` objects. In v9 `remind_at` was an ISO string, and a string no longer compiles.

`stream-chat` exports the helpers to convert them: `nsToDate`, `convertTimestampToDate`, `nsToMs`, `msToNs`, `dateToNs`, `nowNs` and `asTimestampNS`.

In v9:

```ts
const sentAt = new Date(message.created_at);
const isRecent = Date.parse(message.created_at) > Date.now() - 60_000;
```

In v10:

```ts
import { msToNs, nowNs, nsToDate } from "stream-chat";

const sentAt = nsToDate(message.created_at);
const isRecent = message.created_at > nowNs() - msToNs(60_000);
```

These fail without a type error, so check for them by hand:

- `dayjs(timestamp)` and `moment(timestamp)` read nanoseconds as milliseconds, and render dates far in the future.
- `timestamp > Date.now()` compares nanoseconds with milliseconds.
- `if (!message.created_at)` is wrong for a value of `0`.
- ISO strings or `Date` objects you stored yourself no longer compare with fresh values. Convert what you read from your own storage with `asTimestampNS()`.

`new Date(timestamp)` is marked deprecated, and fails to compile where you use the result as a `Date`. Replace it with `nsToDate(timestamp)` everywhere.

`LocalMessage` uses `undefined` instead of `null` for `deleted_at`, `pinned_at`, `quoted_message` and `error`. Replace `=== null` checks with `== null`.

### Sorts and filters

Every sort is an array of `{ field, direction }` objects, typed `SortParamRequest[]`. The object form and the 29 per-endpoint sort types, such as `ChannelSort`, `UserSort` and `MemberSort`, are removed. Keep the order of the fields: the first one sorts first.

```ts
import type { SortParamRequest } from "stream-chat";

// v9: { pinned_at: -1, last_message_at: -1 }
const sort: SortParamRequest[] = [
  { direction: -1, field: "pinned_at" },
  { direction: -1, field: "last_message_at" },
];
```

Filters are generated per endpoint, and `ChannelFilters`, `UserFilters`, `MemberFilters` and `MessageFilters` are still exported. A custom field is a `"custom.<field>"` key, and each key takes one operator. Split two operators on one key into an `$and`.

```ts
const filters: ChannelFilters = {
  "custom.color": "green",
  members: { $in: [userId] },
};
```

## Events

`Event` is a union of event types. A field exists only on the event types that carry it, so a handler gets the right type from the event name:

```ts
import type { EventPayload } from "stream-chat";

const { unsubscribe } = client.on("message.new", (event) => {
  console.log(event.message.text);
});

// A handler declared elsewhere
const onNewMessage = (event: EventPayload<"message.new">) => {
  console.log(event.message.id);
};
client.on("message.new", onNewMessage);
```

- Drop explicit `(event: Event) =>` annotations. They widen the event back to the whole union.
- `client.on()` returns `{ unsubscribe }`, and accepts only known event names. Listen to every event with `client.on(handler)` or `client.on("all", handler)`. `"*"` isn't an event name.
- `EventTypes` is `EventType`, a type only. `EVENT_MAP` is removed, so keep your own list if you iterated it.
- `channel.listeners` and `client.listeners` are a `Map` of `Set`s: `channel.listeners.get("message.new")?.size`. A handler registered twice is registered once.

These events are removed:

| v9 event                           | v10                                                                                                           |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `connection.changed`               | `client.wsConnection.state` and `client.networkConnection.state`. See [Connection status](#connection-status) |
| `transport.changed`                | the local `connection.fallback_activated` event                                                               |
| `capabilities.changed`             | `ownCapabilities` in `channel.state`                                                                          |
| `channel.muted`, `channel.unmuted` | `notification.channel_mutes_updated`                                                                          |
| `user.unread_message_reminder`     | removed                                                                                                       |

To follow a channel's capabilities, subscribe to `channel.state`. The handler runs only when the capabilities change:

```ts
channel.state.subscribeWithSelector(
  ({ ownCapabilities }) => ({ ownCapabilities }),
  ({ ownCapabilities }) => renderToolbar(ownCapabilities),
);
```

The AI state `AI_STATE_CHECKING_SOURCES` is now `AI_STATE_EXTERNAL_SOURCES`. Compare against `AIStates.ExternalSources`.

### Custom events

A custom event carries its fields in `event.custom`, and its name must be declared:

```ts
declare module "stream-chat" {
  interface CustomEventTypes {
    "friendship.request": true;
  }
}

await channel.sendEvent({
  event: { custom: { from: "jane" }, type: "friendship.request" },
});

client.on("friendship.request", (event) => console.log(event.custom));
```

In v9, `channel.sendEvent({ type, text })` sent the fields at the top level.

## Connection status

In v9, "online" meant that the WebSocket was up. `stream-chat` 10 reports two things separately: whether the device has a network, in `client.networkConnection`, and whether the socket is up, in `client.wsConnection`. [Connection](https://getstream.io/chat/docs/javascript/state-connection/) describes both.

`connection.changed` is never dispatched. In TypeScript a listener for it doesn't compile. In JavaScript it compiles and never runs, so an offline banner never shows or never hides.

In v9:

```ts
client.on("connection.changed", (event) => setOnline(!!event.online));
```

In v10:

```ts
const unsubscribe = client.wsConnection.state.subscribeWithSelector(
  ({ isHealthy }) => ({ isHealthy }),
  ({ isHealthy }) => setOnline(isHealthy),
);
```

- The store calls the handler once right away, and then on every change. v9 held a drop back for about five seconds, so a short flap didn't flash a banner. To keep that, wait `client.wsConnection.config.offlineNotificationDisplayDelayMs` yourself, and cancel when the socket comes back.
- `isOnline` in `client.networkConnection.state` is `undefined` until the network is first reported, so test it with `=== false`. Browsers get a network reporter automatically. On other hosts, register one with `client.networkConnection.setStatusReporter()`.
- `client.wsConnection` always exists, before `connectUser()` too. Check `client.wsConnection.isHealthy`, not `if (client.wsConnection)`. `client.wsFallback` is removed.
- With `enableWSFallback`, the socket waits the full `client.wsConnection.config.connectTimeoutMs`, 15 seconds by default, before it switches to long polling. Lower it to switch sooner.

REST requests go to `/api/v2/...`, and the socket connects to `/api/v2/connect` with the token in its first frame. Update proxies, CSP allowlists and request mocks keyed on paths. The exception is `client.unbanUser()`, which `channel.unbanUser()` calls too: it still sends `DELETE /moderation/ban`, without `/api/v2`.

## Errors

A failed request whose response has a Stream error code rejects with `StreamAPIError`, which has `code`, `status` and `response`. Its message is the server's message, without the `StreamChat error code N:` prefix. Anything else, such as a network error, a `502` page or a cancelled request, rejects with the raw `axios` error.

In v9:

```ts
try {
  await channel.sendMessage({ text });
} catch (err) {
  if (err instanceof ErrorFromResponse && err.code === 17) showNotAllowed();
}
```

In v10:

```ts
import { StreamAPIError } from "stream-chat";

try {
  await channel.sendMessage({ message: { text } });
} catch (err) {
  if (err instanceof StreamAPIError && err.code === 17) showNotAllowed();
  else throw err;
}
```

`ErrorFromResponse`, `client.errorFromResponse()` and `client.handleResponse()` are removed. Read `err.response?.data.status_code` instead of `StatusCode`. Code that matched on the error message text, such as `err.message.includes("StreamChat error")`, no longer matches.

When a request fails with an expired token and the user connected with a token provider, the client loads a new token and retries the request. v10 retries up to 10 times, and then rejects with the `StreamAPIError`. v9 retried with no limit.

## Client methods

### Methods take request objects

Most methods take path parameters first, then the request object, then `requestOptions`: `(pathParams?, request?, requestOptions?)`. `requestOptions` is a `StreamRequestOptions`, with `signal`, `onUploadProgress` and `timeout`, and replaces v9's `RequestOptions`. A method with no request body keeps an unused request slot, so pass `undefined` to reach `requestOptions`.

In v9:

```ts
const { users } = await client.queryUsers(
  { id: { $in: ids } },
  { last_active: -1 },
  { limit: 10 },
);
const { results } = await client.search({ cid: channel.cid }, "hello", {
  limit: 20,
});
```

In v10:

```ts
const { users } = await client.queryUsers({
  payload: {
    filter_conditions: { id: { $in: ids } },
    limit: 10,
    sort: [{ direction: -1, field: "last_active" }],
  },
});

const { results } = await client.search({
  payload: {
    filter_conditions: { cid: channel.cid },
    limit: 20,
    query: "hello",
  },
});
```

The same rule changed `queryBannedUsers`, `queryMessageFlags`, `translateMessage({ id }, { language })`, `getMessage({ id })`, `sync({ channel_cids, last_sync_at })`, `deleteChannels({ cids, hard_delete })`, `queryDrafts`, the user group methods, and more. The [generated client signatures](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#generated-client-signatures) entry lists them.

- `client.get()`, `post()`, `put()`, `patch()`, `delete()` and `doAxiosRequest()` moved to `client.api`.
- `client.createAbortControllerForNextRequest()` is removed. Pass `requestOptions.signal`.
- Responses carry the body plus `metadata`. `blocklist` is removed from responses.

### queryChannels returns the response

`client.queryChannels(request?, requestOptions?)` is now a plain request. It returns the response object, creates no `Channel` objects, stores nothing, and dispatches no `channels.queried`. In JavaScript, `const channels = await client.queryChannels(...)` now gets an object, with no `.length`.

The v9 behavior is `client.queryChannelsAndHydrate()`:

In v9:

```ts
const channels = await client.queryChannels(
  { members: { $in: [userId] } },
  { last_message_at: -1 },
  { limit: 20 },
);
```

In v10:

```ts
const channels = await client.queryChannelsAndHydrate({
  filter_conditions: { members: { $in: [userId] } },
  limit: 20,
  sort: [{ direction: -1, field: "last_message_at" }],
});
```

The state options stay the second argument, and the abort signal moves to the third. `stateOptions.skipInitialization` is removed: [activate](#activate-what-is-on-screen) the channels on screen instead. For a channel list that stays current, use a [`ChannelPaginator`](#channel-lists).

### Plain requests and AndHydrate

The same split applies to threads, reactions and message edits:

| v9                                        | v10 plain request                           | v10 that also updates local state                                       |
| ----------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
| `client.queryThreads(options)`            | `client.queryThreads(request)`              | `client.queryThreadsAndHydrate(options)`                                |
| `client.getThread(id)`                    | `client.getThread({ message_id })`          | `client.getThreadAndHydrate({ message_id })`                            |
| `client.queryReactions(id, filter, sort)` | `client.queryReactions({ id }, request)`    | `client.queryReactionsAndHydrate({ id }, request)`                      |
| `client.updateMessage(message)`           | `client.updateMessage({ id }, { message })` | `channel.messageOperations.update({ localMessage })`                    |
| `client.deleteMessage(id, hard)`          | `client.deleteMessage({ id }, { hard })`    | `channel.messageOperations.delete({ localMessage, options: { hard } })` |

The plain requests have no offline queue and change no local state.

### Renamed methods

| v9                                                                                | v10                                                                           |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `client.addDevice(id, push_provider)`                                             | `client.createDevice({ id, push_provider, push_provider_name? })`             |
| `client.getDevices()`                                                             | `client.listDevices()`                                                        |
| `client.removeDevice(id)`                                                         | `client.deleteDevice({ id })`                                                 |
| `client.setLocalDevice(device)`                                                   | removed. Call `client.createDevice()` after connecting                        |
| `client.getUnreadCount()`                                                         | `client.unreadCounts()`                                                       |
| `client.setPushPreferences(preferences)`                                          | `client.updatePushNotificationPreferences({ preferences })`                   |
| `client.partialUpdateUser(user)`, `client.partialUpdateUsers(users)`              | `client.updateUsersPartial({ users: [{ id, set, unset }] })`                  |
| `client.upsertUser(user)`, `client.upsertUsers(users)`, `client.updateUser(user)` | `client.updateUsers({ users: { [id]: user } })`                               |
| `client.unBlockUser(id)`                                                          | `client.unblockUser(id)`                                                      |
| `client.queryUserGroups(options)`                                                 | `client.listUserGroups(options)`                                              |
| `client.getSharedLocations()`                                                     | `client.getUserLiveLocations()`                                               |
| `client.updateLocation(payload)`                                                  | `client.updateLiveLocation({ message_id, latitude, longitude, end_at })`      |
| `client.markAllRead()`                                                            | `client.markChannelsRead()`                                                   |
| `client.partialUpdateMessage(id, { set, unset })`                                 | `client.updateMessagePartial({ id }, { set, unset })`                         |
| `client.ephemeralUpdateMessage(...)`                                              | `client.updateMessagePartial(...)`                                            |
| `client.partialUpdateThread(messageId, { set, unset })`                           | `client.updateThreadPartial({ message_id }, { set, unset })`                  |
| `client.enrichURL(url)`                                                           | `client.getOG({ url })`                                                       |
| `client.hydrateActiveChannels(...)`                                               | `client.hydrateChannels(...)`                                                 |
| `client.setAnonymousUser()`                                                       | `client.connectAnonymousUser()`                                               |
| `client.recoverState()`                                                           | `client.connectionRecovery.recover()`                                         |
| `client.pinMessage(message, 120)`                                                 | `client.pinMessage(message, { pin_expires: new Date(Date.now() + 120_000) })` |
| `client.unpinMessage(message, userId)`                                            | `client.unpinMessage(message)`                                                |

### Moderation

Moderation calls moved to `client.moderation`, and take request objects:

| v9                              | v10                                                           |
| ------------------------------- | ------------------------------------------------------------- |
| `client.banUser(id, options)`   | `client.moderation.ban({ target_user_id: id, ...options })`   |
| `client.shadowBan(id, options)` | `client.moderation.ban({ target_user_id: id, shadow: true })` |
| `client.removeShadowBan(id)`    | `client.unbanUser(id, { shadow: true })`                      |
| `client.muteUser(id)`           | `client.moderation.mute({ target_ids: [id] })`                |
| `client.unmuteUser(id)`         | `client.moderation.unmuteUser(id)`                            |
| `client.flagMessage(id)`        | `client.moderation.flagMessage({ message_id: id })`           |
| `client.flagUser(id)`           | `client.moderation.flagUser({ user_id: id })`                 |
| `unflagMessage`, `unflagUser`   | removed                                                       |
| `client.unblockMessage(id)`     | removed                                                       |

>
> **Warning:** Mute, unmute and flag need moderation v2 enabled for your app. v9's `client.muteUser()`, `client.unmuteUser()`, `client.flagMessage()` and `client.flagUser()` called `/moderation/mute`, `/moderation/unmute` and `/moderation/flag`. Their v10 replacements call `/api/v2/moderation/mute`, `/api/v2/moderation/unmute` and `/api/v2/moderation/flag`. Without moderation v2 these calls reject with `403` at runtime, the first time a user mutes or flags. Ask Stream support to enable it before you upgrade, and handle the rejection where your app mutes or flags.
>

### Uploads

Uploads take a request object with a `file`: a browser `File` or `Blob`, a `{ uri, name, type }` reference, or a URI string. Node `Buffer`s and streams are no longer accepted, so pass a `Blob`. There is no `contentType` argument: set `type` on the file. Upload progress, timeout and abort go in `requestOptions`.

In v9:

```ts
await channel.sendImage(file, file.name, file.type, undefined, {
  onUploadProgress,
});
await channel.deleteImage(url);
```

In v10:

```ts
await channel.uploadImage({ file }, { onUploadProgress });
await channel.deleteImage({ url });
await client.uploadFile({
  file: { name: "clip.mp4", type: "video/mp4", uri },
});
```

`channel.sendFile()` is `channel.uploadFile({ file })`, and `client.sendFile()` is removed.

## Channels

Get channels from `client.channel(type, id, data)`. Never construct one with `new Channel()`: a channel the client doesn't hold receives no events. `client.getChannelById()` and `client.getChannelByMembers()` are removed, and `client.channel()` covers both.

### watch() resolves with the channel

`channel.watch(options?, requestOptions?)` resolves with the `Channel` to use from then on, not with the server's response. That is the channel itself, or, for a channel created from its members only, the channel that superseded it.

In v9:

```ts
const response = await channel.watch();
renderMessages(response.messages);
```

In v10:

```ts
const watched = await channel.watch();
renderMessages(watched.messagePaginator.items ?? []);
```

- `channel.watch()` sends a request only when the channel isn't loaded yet, or when you pass options it wasn't watched with. Calling it again no longer refreshes a watched channel: call `channel.reload()`.
- Load other messages with the [message paginator](#channel-state-and-the-message-list), not with `channel.watch({ messages })`.
- `channel.query(options?, requestOptions?)` returns the raw response when you need it. Its v9 second argument, the message set, is removed, and neither `channel.query()` nor `channel.create()` adds messages to the list.

A request that watches or asks for presence waits for the WebSocket's connection id. That covers `channel.watch()`, `queryChannels` and `queryChannelsAndHydrate` (which watch by default), and `queryUsers` with `presence`. v9 sent it without one, the server answered `200`, and registered nothing. Call `client.connectUser()` first. Code without a socket, such as a script, passes `watch: false` and `presence: false`. With no socket open or opening, the request throws "No connection id is available".

### Activate what is on screen

After a reconnect, the client reloads the registered channel lists, and the channels and threads that are active. A channel that is watched but not active isn't refreshed, and misses the messages sent while it was offline.

Activate a channel while it's on screen. `channel.activate()` returns the function that releases it, and there is no `channel.deactivate()`:

```ts
// When the channel is shown
const release = channel.activate();

// When the user leaves it
release();
```

`client.connectionRecovery` runs the recovery and dispatches `connection.recovered` when it's done. Don't also reload active channels on `connection.recovered`, or each reconnect loads them twice. To turn recovery off, set `client.config.set({ client: { connectionRecovery: { enabled: false } } })`. `connection.recovered` still fires after every reconnect, but nothing is reloaded.

### Sending messages and reactions

`channel.sendMessage()` is a plain request with no offline queue. With an offline database, v9 queued a message that had an `id` there, and v10 no longer does. The optimistic flow, with the failed state, retries and offline replay, goes through `channel.messageComposer.send()` or `channel.messageOperations`. See [Message composer](https://getstream.io/chat/docs/javascript/state-composer/).

In v9:

```ts
await channel.sendMessage({ attachments, text }, { skip_push: true });
await channel.sendReaction(messageId, { type: "love" });
await channel.deleteReaction(messageId, "love");
```

In v10:

```ts
// Plain requests
await channel.sendMessage({ message: { attachments, text }, skip_push: true });
await channel.sendReaction({ id: messageId }, { reaction: { type: "love" } });
await channel.deleteReaction({ id: messageId, type: "love" });

// With an optimistic update
await channel.messageOperations.addReaction({
  messageId,
  reaction: { type: "love" },
});
```

`pending`, `pending_message_metadata`, `force_moderation` and `is_pending_message` aren't part of `SendMessageRequest`.

To customize the requests behind the optimistic operations, register request handlers once for the client:

```ts
client.config.setConfig("channel", {
  requestHandlers: {
    sendMessageRequest: async (params, defaultRequest) => {
      console.log("Sending to", params.localMessage.cid);
      return defaultRequest(params);
    },
  },
});
```

The handlers are `sendMessageRequest`, `retrySendMessageRequest`, `updateMessageRequest`, `deleteMessageRequest` and `markReadRequest`.

### markRead

The two mark-read methods swapped names. v9's `channel.markRead()` went through the delivery reporter. In v10 that is `channel.markReadViaReporter()`. v10's `channel.markRead(request?, requestOptions?)` sends the request directly, and replaces v9's `markAsReadRequest()`.

In v9:

```ts
await channel.markRead();
await channel.markAsReadRequest();
```

In v10:

```ts
await channel.markReadViaReporter();

if (channel.readEventsEnabled) {
  await channel.markRead({ message_id });
}
```

`channel.markRead()` and `channel.markUnread()` throw when read events are off: for the channel type, in the client configuration, or because the user lacks the `read-events` capability. v9 resolved `null`. Replace `=== null` checks with `try`/`catch`, or check `channel.readEventsEnabled` first. `channel.markReadViaReporter()` and `thread.markRead()` still resolve `null` in that case.

The `message_timestamp` of `markUnread()` is a `Date`.

### Other channel methods

| v9                                                 | v10                                                                                                           |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `channel.update(data, message)`                    | `channel.updatePartial({ set: { name } })`, or `channel.update({ data: { custom }, message })`                |
| `channel._update(payload)`                         | `channel.update(payload)`                                                                                     |
| `channel.addMembers(ids, message, options)`        | `channel.addMembers({ members: ids, message, ...options })`. The same for `removeMembers` and `inviteMembers` |
| `addModerators`, `demoteModerators`, `assignRoles` | removed. Server-side, or `addMembers({ members: [{ user_id, channel_role }] })`                               |
| `channel.partialUpdateMember(id, update)`          | `channel.updateMemberPartial({ set, unset })`, for your own membership only                                   |
| `channel.archive()`, `channel.pin()`               | resolve with `{ channel_member }`                                                                             |
| `channel.mute({ user_id })`                        | `channel.mute({ expiration })`                                                                                |
| `channel.hide(userId, clearHistory)`               | `channel.hide({ clear_history })`                                                                             |
| `channel.show(userId)`                             | `channel.show()`                                                                                              |
| `channel.shadowBan(id, options)`                   | `channel.banUser(id, { ...options, shadow: true })`                                                           |
| `channel.removeShadowBan(id)`                      | `channel.unbanUser(id, { shadow: true })`                                                                     |
| `channel.queryMembers(filter, sort, options)`      | `channel.queryMembers({ payload: { filter_conditions, sort, ...options } })`                                  |
| `channel.getPinnedMessages(options, sort)`         | `channel.getPinnedMessages({ ...options, sort })`                                                             |
| `channel.search(query, options)`                   | `client.search({ payload: { filter_conditions: { cid: channel.cid }, query } })`                              |
| `channel.getReplies(parentId, options)`            | `client.getReplies({ parent_id }, options)`, or a [thread](#threads)                                          |
| `channel.getReactions(messageId, options)`         | `client.getReactions({ id }, options)`                                                                        |
| `channel.keystroke(parentId, { user_id })`         | `channel.keystroke(parentId)`                                                                                 |
| `channel.createDraft(message)`                     | `channel.createDraft({ message })`                                                                            |
| `channel.vote(messageId, pollId, vote)`            | `channel.vote({ message_id, poll_id }, { vote })`                                                             |
| `channel.removeVote(messageId, pollId, voteId)`    | `channel.removeVote({ message_id, poll_id, vote_id })`                                                        |
| `channel.sendSharedLocation(location, userId)`     | `channel.sendSharedLocation(location)`                                                                        |
| `channel.truncate({ user_id, ... })`               | `channel.truncate({ hard_delete, message, truncated_at })`                                                    |
| `channel.disconnected`, `channel._disconnect()`    | `channel.pendingDisposal`, which is read-only, and `channel.disconnect()`                                     |
| `channel.lastMessage()`                            | `channel.messagePaginator.latestMessage`                                                                      |

`channel.update()` is a full update: `data.custom` replaces all of the channel's custom data. To change one field, use `channel.updatePartial({ set: { name } })`, or spread `channel.data?.custom` into the update.

Methods on a channel created from its members, which has no id yet, throw "Channel isn't yet created". Create it with `channel.watch()` or `channel.create()` first. That includes `channel.queryMembers()`, which in v9 queried by the channel's member list instead.

### getConfig() and ChannelConfig

`channel.getConfig()` is the `channel.serverConfig` getter, and `client.configs[cid]` is removed. `serverConfig` is the channel type's settings from the server, and is `undefined` until the channel is queried. What the client enforces is `channel.config`, the server's settings combined with yours:

In v9:

```ts
const uploadsEnabled = channel.getConfig()?.uploads;
const commands = channel.getConfig()?.commands;
```

In v10:

```ts
const uploadsEnabled = channel.serverConfig?.uploads;
const commands = channel.config.availableCommands;
const typingOn = channel.config.typingEvents.enabled;
```

The `ChannelConfig` type now means that resolved configuration, with `typingEvents`, `readEvents`, `replies`, `userMessageReminders`, `deliveryEvents`, `availableCommands` and `requestHandlers`. The v9 meaning is `ChannelConfigWithInfo`. Code typed with `ChannelConfig` can still compile and read `undefined`.

## Channel state and the message list

`channel.state` is a store, with `getLatestValue()`, `subscribe()` and `subscribeWithSelector()`. Its `members`, `read`, `watchers`, `typing` and `membership` getters still work, but changing them in place notifies no one. Assign a new object instead:

```ts
channel.state.read = { ...channel.state.read, [userId]: readState };
channel.data = { ...channel.data, frozen: true };
```

`channel.state.unreadCount` is read-only. Use `channel.markRead()` or `channel.markReadLocally()`. In `channel.state.getLatestValue()`, the watcher count is `watcherCount`.

Messages are no longer on `channel.state`. They're in `channel.messagePaginator`. See [Messages](https://getstream.io/chat/docs/javascript/state-messages/).

In v9:

```ts
const messages = channel.state.messages;
const hasOlder = channel.state.messagePagination.hasPrev;
await channel.query({ messages: { id_lt: messages[0].id, limit: 25 } });
```

In v10:

```ts
const messages = channel.messagePaginator.items ?? [];
const { hasMoreTail } = channel.messagePaginator.state.getLatestValue();
if (hasMoreTail) await channel.messagePaginator.toTail();

const unsubscribe = channel.messagePaginator.state.subscribeWithSelector(
  ({ items }) => ({ items }),
  ({ items }) => renderMessages(items ?? []),
);
```

In a message list, the tail is where older messages load, and the head is the newest message.

| v9                                        | v10                                                                                    |
| ----------------------------------------- | -------------------------------------------------------------------------------------- |
| `channel.state.messages`                  | `channel.messagePaginator.items`, the interval on screen. `undefined` means not loaded |
| `channel.state.latestMessages`            | `channel.messagePaginator.anchoredHeadItems`                                           |
| `channel.state.messagePagination.hasPrev` | `hasMoreTail` in `channel.messagePaginator.state`                                      |
| `channel.state.messagePagination.hasNext` | `hasMoreHead` in `channel.messagePaginator.state`                                      |
| `channel.state.last_message_at`           | `channel.messagePaginator.latestMessageAt`                                             |
| `channel.lastMessage()`                   | `channel.messagePaginator.latestMessage`                                               |
| `state.addMessageSorted(message)`         | `channel.messagePaginator.ingestItem(message)`                                         |
| `state.addMessagesSorted(messages)`       | `channel.messagePaginator.ingestPage({ page: messages })`                              |
| `state.removeMessage({ id })`             | `channel.messagePaginator.removeItem({ id })`                                          |
| `state.findMessage(id)`                   | `channel.messagePaginator.getItem(id)`                                                 |
| `state.loadMessageIntoState(id)`          | `channel.messagePaginator.jumpToMessage(id)`                                           |
| `state.clearMessages()`, `initMessages()` | `channel.messagePaginator.clearStateAndCache()`                                        |
| `state.isUpToDate`, `setIsUpToDate()`     | `channel.messagePaginator.isActiveIntervalAtHead`, `setViewingLive(true)`              |
| `channel.state.pinnedMessages`            | `channel.pinnedMessagesPaginator.items`                                                |
| `channel.state.threads[parentId]`         | `client.threads.get(parentId)?.messagePaginator.items`                                 |

A new message always lands in the newest interval. After a jump, `items` can be an older interval, so read the newest messages from `anchoredHeadItems` or `latestMessage`. `jumpToTheLatestMessage()` and `jumpToTheFirstUnreadMessage()` move back.

The paginators have no `next()`, `prev()`, `hasNext` or `hasPrev`. Use `toTail()`, `toHead()`, `hasMoreTail` and `hasMoreHead`. A cursor's `next` and `prev` are `tailward` and `headward`. [State overview](https://getstream.io/chat/docs/javascript/state-overview/#pagination) covers the paginator API.

## Channel lists

`client.activeChannels` is removed. The client holds its channels in `client.channelManager`:

| v9                                     | v10                              |
| -------------------------------------- | -------------------------------- |
| `client.activeChannels[cid]`           | `client.channelManager.get(cid)` |
| `Object.values(client.activeChannels)` | `client.channelManager.values()` |

`ChannelManager` was rewritten. There is one per client, `client.channelManager`, and it holds any number of lists, each a `ChannelPaginator`. `client.createChannelManager()`, `new ChannelManager()`, `manager.queryChannels()`, `loadNext()`, `setChannels()` and `ChannelManagerOptions` are removed.

In v9:

```ts
const manager = client.createChannelManager({
  options: { lockChannelOrder: true },
});
manager.registerSubscriptions();
await manager.queryChannels(
  { members: { $in: [userId] } },
  { last_message_at: -1 },
  { limit: 20 },
);
const { channels } = manager.state.getLatestValue();
```

In v10:

```ts
import { ChannelPaginator } from "stream-chat";

const paginator = new ChannelPaginator({
  client,
  filters: { members: { $in: [userId] } },
  id: "main",
  paginatorOptions: { lockItemOrder: true, pageSize: 20 },
  sort: [{ direction: -1, field: "last_message_at" }],
});

client.channelManager.insertPaginator({ paginator });
const unsubscribe = client.channelManager.registerSubscriptions();

await paginator.toTail();
const channels = paginator.items ?? [];

// Load more: await paginator.toTail()
// Reload: await paginator.reload()
// When the list goes away:
// unsubscribe();
// client.channelManager.removePaginator(paginator);
```

Register the paginator. `client.channelManager` routes events into the lists it holds, so a paginator that isn't registered shows its first page and then never adds, moves or removes a channel. [Channel list](https://getstream.io/chat/docs/javascript/state-channel-list/) covers configuring the query and the event handling.

| v9 `ChannelManager`                          | v10                                                                                                 |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `channels` in `manager.state`                | `items` in `paginator.state`. `undefined` means never queried                                       |
| `initialized`                                | `paginator.items !== undefined`                                                                     |
| `pagination.hasNext`, `pagination.isLoading` | `hasMoreTail`, `isLoading` in `paginator.state`                                                     |
| `error`                                      | `lastQueryError` in `paginator.state`                                                               |
| `setChannels(value)`                         | `paginator.setItems({ valueOrFactory: value })`                                                     |
| `setQueryChannelsRequest(fn)`                | `paginatorOptions.doRequest`                                                                        |
| `eventHandlerOverrides`                      | `client.channelManager.setEventHandlers()` and `addEventHandler()`                                  |
| `lockChannelOrder`                           | `lockItemOrder` in each paginator's `paginatorOptions`                                              |
| `promoteChannel()`                           | the position follows the sort. `paginator.boost(cid, { ttlMs })` keeps a channel on top for a while |

These change what a list shows without a compile error:

- **Order comes from the sort only.** A list no longer moves a channel to the top when a message arrives. Sort by `pinned_at` and `last_message_at` to keep that order.
- **Filters run on the client.** Every event runs the list's full filter, so a channel can leave a list it no longer matches. Teach a list a filter key it can't evaluate with `paginator.addFilterResolvers()`.
- **`client.channelManager.registerSubscriptions()` is ref-counted** and returns its unsubscribe. Pair every call with one.
- **`client.disconnectUser()` resets every list** and disposes every channel the client holds. A `Channel` you kept throws "pending disposal" after a logout or user switch. Drop your references, and get the channel again with `client.channel(type, id)` after the next `connectUser()`.

`ChannelBatchUpdater` and `client.channelBatchUpdater()` are removed. They're server-side.

## Threads

- Open a thread with `client.threads.ensure({ channel, parentMessage })`, never `new Thread()`. Fetch threads with `client.getThreadAndHydrate({ message_id })` or `client.queryThreadsAndHydrate()`.
- A thread receives events only while `client.threads` holds it and `client.threads.registerSubscriptions()` is active.
- Replies moved to `thread.messagePaginator`. `thread.state.replies` is `thread.messagePaginator.items`, `loadPrevPage()` is `toTail()`, and `loadNextPage()` is `toHead()`.
- `thread.activate()` returns the function that releases it. `thread.deactivate()` is removed.
- `thread.markAsRead()` is deprecated. Use `thread.markRead()`.
- The thread list is `client.threads.paginator`, with `items`, `isLoading`, `hasMoreTail` and `lastQueryError`. `client.threads.threadsById[id]` is `client.threads.get(id)`, and `ready` in `client.threads.state` is `client.threads.paginator.items !== undefined`.
- `client.preventThreadCleanup` has no effect. Delete the assignment.

In v9:

```ts
const thread =
  client.threads.threadsById[parentId] ?? new Thread({ client, threadData });
thread.activate();
await thread.loadPrevPage();
thread.deactivate();
```

In v10:

```ts
const thread = client.threads.ensure({ channel, parentMessage });
const release = thread.activate();
await thread.messagePaginator.toTail();
release();
```

See [Thread](https://getstream.io/chat/docs/javascript/state-thread/) and [Thread list](https://getstream.io/chat/docs/javascript/state-thread-list/).

## Types

Types are generated from the API's specification, and about 380 v9 type names were renamed or removed. For a type error on a `stream-chat` import, look the name up in the [type rename table](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#type-rename-table). The common ones:

| v9                                                                          | v10                                                                                        |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `StreamChatOptions`                                                         | `InstanceConfigTree`                                                                       |
| `Message`, `UpdatedMessage`, `MessageBase`                                  | `MessageRequest`                                                                           |
| `FormatMessageResponse`, `MessageResponseBase`                              | `LocalMessage`, `MessageResponse`                                                          |
| `ChannelData`                                                               | `ChannelInput`                                                                             |
| `ChannelAPIResponse`, `QueryChannelAPIResponse`, `QueryChannelsAPIResponse` | `ChannelStateResponseFields`, `ChannelStateResponse`, `QueryChannelsResponse`              |
| `ChannelQueryOptions`                                                       | `ChannelGetOrCreateRequest`                                                                |
| `ChannelSort`, `UserSort` and the other sort types                          | `SortParamRequest[]`                                                                       |
| `EventTypes`                                                                | `EventType`                                                                                |
| `ErrorFromResponse`, `APIErrorResponse`                                     | `StreamAPIError`, `APIError`                                                               |
| `Mute`, `MuteUserResponse`                                                  | `UserMuteResponse`, `MuteResponse`                                                         |
| `RequestOptions`                                                            | `StreamRequestOptions`                                                                     |
| `MarkReadOptions`, `MarkUnreadOptions`                                      | `MarkReadRequest`, `MarkUnreadRequest`                                                     |
| `TranslationLanguages`                                                      | `TranslationLanguage`                                                                      |
| `CommandResponse`                                                           | `Command`                                                                                  |
| `Device`, `PollVote`, `PollAnswer`, `PollOption`                            | `DeviceResponse`, `PollVoteResponseData`, `PollVoteResponseData`, `PollOptionResponseData` |

These names still exist and compile, but mean something else. Check every import of them:

| Name                                 | v10 meaning                                  | Use instead                                           |
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------------- |
| `Role`                               | a role object                                | `string` for role names                               |
| `User`                               | an unrelated `{ id, data? }` type            | `ClientUser` to connect, `UserResponse` to read       |
| `Reaction`                           | an activity reaction                         | `ReactionRequest` to send, `ReactionResponse` to read |
| `PollResponse`, `PollOptionResponse` | the endpoint responses, `{ duration, poll }` | `PollResponseData`, `PollOptionResponseData`          |
| `MuteResponse`                       | the mute endpoint's response                 | `UserMuteResponse`                                    |
| `SharedLocationResponse`             | the endpoint's response                      | `SharedLocationResponseData`                          |
| `ThreadResponse`                     | a minimal thread                             | `ThreadStateResponse`                                 |
| `TypingStartEvent`                   | the `typing.start` payload                   | `EventPayload<"typing.start">`                        |
| `ChannelConfig`                      | the resolved per-channel configuration       | `ChannelConfigWithInfo`                               |
| `VotingVisibility`                   | a type-only union                            | the string literals, such as `"public"`               |

Some fields became optional, such as `res.channel?`, and several `T | null` fields are `T | undefined`, such as `membership`, `created_by` and `pinned_by`. `MessageResponse` lost `user_id`, `status` and `moderation_details`, which is `moderation`. `UserResponse` lost `privacy_settings`, `invisible` and `push_notifications`. Your own user, an `OwnUserResponse`, has `privacy_settings` and `invisible`, and `push_preferences` in place of `push_notifications`. The utility types `UR`, `Readable`, `KnownKeys`, `PartializeKeys` and `ArrayOneOrMore` are removed.

## Composer, polls, reminders and other managers

If you drive `channel.messageComposer` directly, or use the poll, reminder, search, live location or offline managers, check the rows that apply. Each links to the full entry.

| Area                 | Change                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Composer setup       | `client.config.setSetupFunction("messageComposer", fn)`. The composer's configuration is read-only: change it with `channel.messageComposer.updateConfig()` or `client.config.setConfig("messageComposer", ...)`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#message-composer-setup-function)                                                                                                                                                                                                |
| Link previews        | On by default where the channel type enriches URLs. Turn them off with `client.config.setConfig("messageComposer", { linkPreviews: { enabled: false } })`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#link-previews-default)                                                                                                                                                                                                                                                                 |
| Commands             | The composer installs the command middlewares by default. Delete your own installs of them. A UI without a command chip removes all four. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#composer-command-middlewares-default)                                                                                                                                                                                                                                                                   |
| Composer output      | In the `localMessage` that `channel.messageComposer.compose()` returns, `deleted_at`, `pinned_at` and `reaction_groups` are `undefined` instead of `null`. The `legacy_thread` context and `applyCommandValidatorOverride` are removed. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#composer-compose-output)                                                                                                                                                                                  |
| Uploads              | Upload metadata is under `custom`. A custom `doUploadRequest` needs `customCdn: true`. `channel.messageComposer.attachmentManager.uploadAttachment()` is removed: call `channel.messageComposer.attachmentManager.uploadFile(attachment)`. Sending before uploads finish is `attachments.pendingUploadsEnabled` in the `messageComposer` configuration. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#composer-upload-gating)                                                                   |
| Poll composer        | `channel.messageComposer.pollComposer.updateFields({ data, injectedFieldErrors })`. Field errors are `PollComposerValidationError` objects, not strings. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#poll-composer-errors)                                                                                                                                                                                                                                                                    |
| Polls                | Poll methods take path objects: `client.partialUpdatePoll()` is `client.updatePollPartial({ poll_id }, ...)`, `client.castPollVote({ message_id, poll_id }, { vote })`, and `client.removePollVote()` is `client.deletePollVote({ message_id, poll_id, vote_id })`. `client.closePoll()` and `client.addPollAnswer()` are removed. `VotingVisibility` is a type only: write `"public"` instead of `VotingVisibility.public`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#poll-client-methods) |
| Reminders            | `client.reminders.createReminder({ message_id }, { remind_at })`, where `remind_at` is a `Date`. Omit it instead of passing `null`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#reminder-methods)                                                                                                                                                                                                                                                                                             |
| Live location        | Call `dispose()` on a `LiveLocationManager` when you're done with it. Locations without `end_at` are dropped. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#live-location-dispose)                                                                                                                                                                                                                                                                                                              |
| Cooldown             | `channel.cooldownTimer.ownLatestMessageDate` is `channel.cooldownTimer.ownLatestMessageTimestamp`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#cooldown-timestamp)                                                                                                                                                                                                                                                                                                                            |
| Search               | Sorts are `SortParamRequest[]`. Channels in search results aren't watched. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#search-source-sort)                                                                                                                                                                                                                                                                                                                                                    |
| Paginator subclasses | `query()` is split into `fetchQueryResponse()`, `extractQueryPage()` and `getNextQueryShape()`. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#paginator-subclass)                                                                                                                                                                                                                                                                                                                               |
| Offline storage      | The `AbstractOfflineDB` contract and the pending task payloads changed. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#offline-db-contract)                                                                                                                                                                                                                                                                                                                                                      |
| Notifications        | The configuration key `{ client: { notifications } }` configures the notification manager, and the `client.notifications` property is the `NotificationManager` itself. To use your own manager, pass `{ client: { notificationManager } }` when you create the client. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#notification-manager-config)                                                                                                                                              |
| Utilities            | `encodeBase64` and `decodeBase64` are `btoa` and `atob`. `Campaign`, `Segment` and the `permissions` module are removed. [More](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#encode-decode-base64)                                                                                                                                                                                                                                                                                                    |

## Breaks that compile

These type-check, or run untyped in JavaScript, and misbehave. Check each one that touches code you have:

| Symptom                                                             | Cause                                                       | Fix                                                                             |
| ------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Options ignored, with "unknown key" warnings                        | a v9 options object, or a secret, passed to the constructor | move the options under `client`, and drop the secret                            |
| `connectUser()` rejects "WebSocket is not defined" on Node          | `ws` is no longer bundled                                   | use Node 22 or later, or pass `wsConnection.webSocketImpl`                      |
| Console output the app never had                                    | the default logger logs `info` to the console               | `chatLoggerSystem.configureLoggers()`                                           |
| Custom fields read `undefined`, channel names are blank             | custom data is under `custom`                               | read and write `.custom.<field>`                                                |
| "Invalid Date", dates far in the future, wrong comparisons          | timestamps are nanosecond numbers                           | `nsToDate()`, and compare numbers                                               |
| A sort is ignored or rejected                                       | the object form, passed untyped                             | `[{ field, direction }]`                                                        |
| A custom-field filter matches nothing                               | custom keys need the `custom.` prefix                       | `{ "custom.<field>": value }`                                                   |
| An offline banner never shows or never hides                        | `connection.changed` is never dispatched                    | subscribe to `client.wsConnection.state`                                        |
| Error handling branches never match                                 | `ErrorFromResponse` is gone, and messages lost their prefix | `err instanceof StreamAPIError`, and match `err.code` or `err.status`           |
| `client.queryChannels()` result has no `.length`                    | it's a plain request now                                    | `client.queryChannelsAndHydrate()`                                              |
| A query hangs or throws "No connection id is available"             | watching and presence wait for the socket                   | call `connectUser()` first, or pass `watch: false` and `presence: false`        |
| `watch()` result read as a response                                 | `watch()` resolves with the channel                         | read `channel.state` and `channel.messagePaginator`, or call `query()`          |
| A repeated `watch()` doesn't refresh                                | a watched channel sends nothing for the same options        | `channel.reload()`                                                              |
| Messages missing after a reconnect                                  | only active channels reload                                 | `channel.activate()` while the channel is on screen                             |
| Each reconnect reloads twice                                        | your own reload on `connection.recovered`                   | remove it for active channels                                                   |
| Messages sent offline are no longer queued                          | `channel.sendMessage()` has no offline queue                | `channel.messageComposer.send()` or `channel.messageOperations.send()`          |
| Delivery receipts no longer clear                                   | `channel.markRead()` is the direct request                  | `channel.markReadViaReporter()`                                                 |
| `markRead()` or `markUnread()` throws instead of resolving `null`   | read events are off                                         | `try`/`catch`, or check `channel.readEventsEnabled`                             |
| Feature checks read `undefined`                                     | `getConfig()` is removed, `ChannelConfig` changed meaning   | `channel.serverConfig?.<flag>` or `channel.config.<feature>.enabled`            |
| The UI doesn't update after a state write                           | changes in place notify no one                              | assign a new object                                                             |
| The message list is empty after `channel.query()`                   | `query()` adds no messages                                  | `watch()`, or `channel.messagePaginator.toTail()`                               |
| Channel list order changed                                          | no promotion to the top, the order follows the sort         | sort by `pinned_at` and `last_message_at`, or `paginator.boost()`               |
| Channels leave or join lists unexpectedly                           | the full filter runs on the client                          | review the filters, and add `addFilterResolvers()`                              |
| A kept `Channel` throws "pending disposal" after logout             | `disconnectUser()` disposes every channel                   | drop the reference, and call `client.channel(type, id)` again                   |
| `channel.queryMembers()` throws "Channel isn't yet created"         | a channel created from its members has no id yet            | `channel.watch()` or `channel.create()` first                                   |
| A thread never updates                                              | threads get events only through `client.threads`            | `client.threads.ensure()`, with `client.threads.registerSubscriptions()` active |
| `VotingVisibility.public` fails to compile, or throws in JavaScript | the enum became a type-only union, with no runtime value    | `"public"`                                                                      |
| Link previews appear where the app had none                         | link previews are on by default                             | turn them off in the `messageComposer` configuration                            |
| Attachment size or MIME type missing                                | upload metadata moved under `custom`                        | `attachment.custom.mime_type`, `attachment.custom.file_size`                    |
| Proxy, CSP or request mocks miss requests                           | REST and WebSocket paths moved under `/api/v2`              | update the paths                                                                |
| Muting or flagging rejects with `403`                               | mute, unmute and flag call the moderation v2 endpoints      | enable moderation v2 for the app, and handle the rejection                      |
| The socket keeps retrying with `persistUserOnConnectionFailure`     | it no longer closes after a network failure                 | call `client.closeConnection()` yourself                                        |

The [behavior index](https://github.com/GetStream/stream-chat-js/blob/master/ai-docs/v10-breaking-changes.md#behaviour-index) lists every change that still compiles, including rarer ones.

## Verify the upgrade

1. Run your typecheck, lint and tests.
2. Check that one copy of `stream-chat` 10 resolves, and one copy of `@stream-io/state-store` if you import it: `npm ls stream-chat @stream-io/state-store`.
3. Run the app and check:
   - connecting a user logs no "unknown key" warnings, and the console shows the log level you expect
   - the channel list loads, keeps its order, loads more on scroll, and updates on a new message
   - a channel's messages render, older messages load, and a sent message shows right away
   - channel names and images, custom message fields, and attachment sizes render
   - dates render correctly, not as "Invalid Date" and not decades away
   - reactions, edits, deletes, read state and unread counts update
   - after dropping the network and reconnecting, the open channel catches up, and the offline banner shows and hides
   - a thread's replies load and update live
   - after logging out and in as another user, the lists and channels belong to the new user
   - image and file uploads succeed, with progress

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/javascript/upgrade-stream-chat-to-v10/](https://getstream.io/chat/docs/javascript/upgrade-stream-chat-to-v10/).