feat: add instance configuration service - #1831
Merged
Merged
Conversation
… event cid fallback
… and delete the legacy manager
…manager # Conflicts: # src/client.ts
# Conflicts: # CLAUDE.md # src/ChannelManager.ts # src/client.ts # src/pagination/paginators/ChannelPaginator.ts # test/unit/ChannelManager.test.ts # test/unit/pagination/paginators/ChannelPaginator.test.ts # v9-to-v10-migration-guide-methods.md
Channel-type flags were only readable from the raw server config, so any consumer combining them with registered configuration had to do it itself — and mostly didn't, offering features the client had already disabled. They now resolve into the instance's own configuration, which becomes the whole answer: uploads, polls and url_enrichment on MessageComposer; typing_events, read_events, replies, user_message_reminders, delivery_events and the command list on Channel. Channel gains a ConfigController to do it, re-deriving when the server's answer arrives after construction. Also fixes setters skipping a write when the server was masking the field, which let a stale earlier request win once the server relented. BREAKING CHANGE: `channel.getConfig()` is removed; use the `channel.serverConfig` getter, or `channel.config` for the six flags that now have a resolved counterpart. `ChannelInstanceConfig` -> `ChannelConfig`, `ThreadInstanceConfig` -> `ThreadConfig`, `InstanceConfigurationService` -> `InstanceConfigurationRegistry`. `MessageComposerConfig` gains required `polls`; `AttachmentManagerConfig` gains required `enabled` and `customCdn`. `linkPreviews.enabled` now defaults to `true`. See v9-to-v10-migration-guide-other.md.
# Conflicts: # src/channel.ts # src/client.ts # src/thread.ts # v9-to-v10-migration-guide-other.md # v9-to-v10-migration-guide-type-renames.md
MartinCupela
requested review from
isekovanic,
oliverlaz,
santhoshvai,
szuperaz and
vishalnarkhede
as code owners
August 18, 2026 15:25
isekovanic
reviewed
Aug 19, 2026
A channel's own `config_overrides` narrow its type's settings for that channel alone, so a type-keyed cache could not hold two channels of one type that disagree — they overwrote each other and every channel and composer of the type re-derived to a value correct for at most one. `client.configs` → `client.channelServerConfigs`; the cid key space is v9's, unchanged. BREAKING CHANGE: `client.configs` is renamed to `client.channelServerConfigs`. Keys are still cids, so lookups translate directly. Prefer `channel.serverConfig`.
…ast subscriber leaves
isekovanic
approved these changes
Aug 21, 2026
# Conflicts: # src/channel.ts # src/client.ts # src/logger.ts # src/pagination/paginators/PinnedMessagePaginator.ts # src/types.ts # v9-to-v10-migration-guide-methods.md # v9-to-v10-migration-guide-other.md # v9-to-v10-migration-guide-type-renames.md
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 21, 2026
## [10.0.0-rc.7](v10.0.0-rc.6...v10.0.0-rc.7) (2026-08-21) ### ⚠ BREAKING CHANGES * `client.instanceConfigurationService` is replaced by `client.config`, and the exported class `InstanceConfigurationService` is renamed to `InstanceConfigurationRegistry`. * `client.setInstanceConfigurationFunction()` is removed. Register setup functions with `client.config.setSetupFunction(key, fn)`. `client.setMessageComposerSetupFunction()` is unchanged. * the per-class setup types are replaced by generic ones keyed by instance. Removed: `ChannelSetupFunction`, `ChannelSetupState`, `ChannelTearDownFunction`, `MessageComposerSetupFunction`, `MessageComposerSetupState`, `MessageComposerTearDownFunction`, `StreamChatSetupFunction`, `StreamChatSetupState`, `StreamChatTearDownFunction`, `ThreadSetupFunction`, `ThreadSetupState`, `ThreadTearDownFunction`, `SetInstanceConfigurationFunctions`, `SetInstanceConfigurationServiceStates` and `SetupFnOf`. Use `InstanceSetupFunction`, `InstanceSetupState`, `InstanceSetupTearDownFunction` and `InstanceSetupKey`. * `ChannelInstanceConfig` is renamed to `ChannelConfig` and `ThreadInstanceConfig` to `ThreadConfig`. * `Channel.getConfig()` is removed. Read the server's channel-type configuration from `channel.serverConfig`, or the resolved configuration from `channel.config`. * `client.configs` is renamed to `client.channelServerConfigs` and `client.configsStore` to `client.channelServerConfigsStore`. Keys are still cids, so lookups translate directly. Prefer `channel.serverConfig`. * `channel.config.commands` is renamed to `channel.config.availableCommands`. The server owns the list, so it is read-only and absent from the declarative configuration tree. * `applyCommandValidatorOverride` is no longer exported. Command validation is configured through the composer's configuration. * `MessageComposerConfig` gains a required `polls`; `AttachmentManagerConfig` gains required `enabled` and `customCdn`; `LocationComposerConfig` gains a required `minShareDurationMs`. A config object built literally must supply them. * `linkPreviews.enabled` now defaults to `true`, where it defaulted to `false`. It is ANDed with the channel type's `url_enrichment`, so `true` means "no opinion, let the server decide"; the old default double-gated the feature and kept it off even where the server had enabled it. * server flags gate the features they describe, ANDed with the registered value rather than the client value winning: `typingEvents` with `typing_events`, `readEvents` with `read_events`, `attachments` with `uploads`, `polls` with `polls`, `location` with `shared_locations`, and `linkPreviews` with `url_enrichment`. Code that set a flag to `true` and assumed it took effect may now find the feature off. * setters no longer discard a write when the server is masking the field. Previously the setter compared against the effective value, so the write was dropped and the earlier request was honoured once the server relented. Affects `linkPreviewsManager.enabled`, `textComposer.enabled`, `textComposer.maxLengthOnEdit`, `textComposer.maxLengthOnSend` and `attachmentManager.maxNumberOfFilesPerMessage`. ### Features * add instance configuration service ([#1831](#1831)) ([434966c](434966c)) * channel state migration ([#1834](#1834)) ([9876add](9876add))
|
🎉 This PR is included in version 10.0.0-rc.7 🎉 The release is available on: Your semantic-release bot 📦🚀 |
6 tasks
isekovanic
added a commit
to GetStream/stream-chat-react-native
that referenced
this pull request
Aug 22, 2026
## 🎯 Goal Migrate the SDK onto `client.config`, the instance-configuration API added in [stream-chat-js#1831](GetStream/stream-chat-js#1831). Two things follow from it: - **Feature gates must be read resolved, not raw.** Six channel-type flags and four composer flags now resolve *into* the instance's configuration, ANDed with whatever the integrator registered. Reading `channel.serverConfig?.read_events` answers only the server's half, so UI gated on it offers features the client has already disabled. - **Configuration belongs in one place.** Props that duplicated what `client.config` can express are removed rather than kept as a second way in. ## 🛠 Implementation details ### Resolved config replaces raw server flags | Call site | Before | After | |---|---|---| | `useMarkRead` | `getConfig()?.read_events` | `channel.config.readEvents.enabled` | | `ChannelMessagePreviewDeliveryStatus` | same | reactive via `useStateStore` | | `ThreadMessagePreviewDeliveryStatus` | same | reactive via `useStateStore` | | `Channel` poll gate | `getConfig()?.polls` | `messageComposer.config.polls.enabled` | | `Channel` commands | `getConfig()?.commands?.length` | `channel.config.availableCommands` | | `AutoCompleteInput` | `getConfig()?.max_message_length` | `composer.config.text.maxLengthOnSend` (server-capped) | | `usePaginatedChannels` | `paginator.config.x = y` | `paginator.updateConfig({ x })` | `paginator.config` is `Readonly` now — direct assignment is a compile error, and nested writes throw because defaults are deep-frozen. ### Props removed | Removed | Replacement | |---|---| | `<Channel doMarkReadRequest>` | `client.config.set({ channel: { requestHandlers: { markReadRequest } } })` | | `<Channel doUpdateMessageRequest>` | `…{ requestHandlers: { updateMessageRequest } }` | | `doFileUploadRequest` | `client.config.set({ messageComposer: { attachments: { doUploadRequest } } })` | | `<Channel stateUpdateThrottleInterval>` | `…{ channel: { messagePaginator: { stateThrottleMs } } }` | | `<Channel newMessageStateUpdateThrottleInterval>` | same | The two throttle props had **one reference each in the whole SDK** — their own type declaration. Nothing read them. Deleted rather than left inert. `doSendMessageRequest` stays (for now). The SDK occupies `requestHandlers.sendMessageRequest` unconditionally to run `uploadPendingAttachments` inside the send pipeline (after the optimistic ingest, before the POST), so it has to wrap an integrator handler rather than be replaced by it. `TODO` in `useChannelRequestHandlers` — it can be deleted once async uploads move to the LLC, or if the LLC exposes a `next`-shaped handler slot. Since we'll be moving the async uploads feature to the LLC most likely I decided it was best to wait for this and then we can probably get rid of the hook for good. Removing `doFileUploadRequest` also fixed a latent bug: the image-compression branch in `Channel.tsx` skipped compression only when the *prop* was set, so a `doUploadRequest` registered through `client.config` still got compressed. It now reads resolved config. ### `useChannelRequestHandlers`: two correctness fixes **Re-apply on re-derivation.** `Channel.initializeConfig` *replaces* `requestHandlers` from the declarative tree, and runs on every change to `channel`, `messagePaginator` or `messageOperations` (the latter two are `alsoWatch`). Our write goes through `configState.partialNext`, which is not one of those layers — so any `client.config.set()` on those keys dropped our send handler, and with it the attachment-upload step, silently. A `configState.subscribe` re-apply guards it; the handler's identity is the guard, so there is no write loop. **Stopped deleting slots we don't own.** The hook used to `delete` `markReadRequest` / `updateMessageRequest` before re-registering. Once a handler arrives declaratively the LLC resolves it onto `configState`, and that `delete` removed it — the LLC then fell back to `ctx.defaults.*`, an unmocked request that hangs rather than errors. Now only `sendMessageRequest` / `retrySendMessageRequest` are touched. ### Other - `channel.configState` is a prototype getter now, so `{ ...channel }` no longer carries it. Guarded in the hook. - `initiateClientWithChannels` writes `client.channelServerConfigsStore` instead of `jest.spyOn(channel, 'getConfig')` — `serverConfig` is a getter, and going through the store also drives the channel's own derivation, so `channel.config` is correct too. - New `mock-builders/event/utils.ts` — `toChannelResponse()`. `Channel` was structurally assignable to `Partial<ChannelResponse>` by accident; it isn't now that `channel.config` means something different. - SampleApp: `drafts` moved to `client.config.set()` at the client-creation site; the setup function keeps only middleware and uses `config.setSetupFunction` instead of the deprecated `setMessageComposerSetupFunction`. Dropped `linkPreviews: { enabled: true }` — that is the v10 default. - `ai-docs/ai-migration-v9-to-v10.md`: new `§13.1` covering the whole API (registration site, request handlers, setup functions, resolved-vs-raw table, caveats), plus 9 quick-reference rows and corrections to `§5`, `§13`, `§16.1`, `§17.3`. `§16.1` was telling people to re-set `messagePaginator.pageSize` after mount, which is an imperative patch that gets dropped on the next derivation. ### Behaviour changes without a compile error - **`linkPreviews.enabled` defaults to `true`** (LLC change). Was `false`, and `LinkPreviewsManager.enabled` used to AND `url_enrichment` itself; that gate moved into resolved config. Link previews now appear wherever enrichment is enabled server-side. - **`attachmentManager.isUploadEnabled`** is now `config.enabled && hasAvailableUploadSlots && (!usesStreamStorage || hasUploadPermission)`, with the channel type's `uploads` flag ANDed into `config.enabled`. A custom `doUploadRequest` no longer waives the `upload-file` capability — integrators uploading outside Stream need `attachments: { customCdn: true }`. - **Poll-button timing.** `pollCreationEnabled` was `false` until the channel query landed (raw flag, `undefined` -> falsy). It is now `true` optimistically and narrows to `false` if the server says no, because `polls.enabled` defaults to `true` meaning "no opinion, let the server decide". ## 🎨 UI Changes No visual changes. Three behavioural ones are listed above — link previews appearing by default is the visible one. ## 🧪 Testing ## ☑️ Checklist - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch — **targets `V10`** - [x] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android — no changes; ExpoMessaging has no drafts or composer setup to migrate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
release-v10could already register a setup function per entity — but only a function, and onlyagainst four hardcoded keys. Anything that was just a value meant writing a function to go and set it.
This PR makes configuration values first-class: they can be registered declaratively, every key is
declared and type-checked, and every configurable object resolves its settings through one layered
pipeline with the server's say applied last.
The four ways to set configuration
new StreamChat(key, { notifications }),new MessageComposer({ config })client.config.set({ channel: { … } })client.config.setSetupFunction('messageComposer', fn)instance.updateConfig({ … }), and the setters that route through itPlus one that isn't integrator's to set but participates: the server's configuration for the
channel — its type's settings, narrowed by that channel's own
config_overrideswhere it has any.Example of configuration API in use:
client.config.reset()returns everything to its derived baseline.The layers, and how they reconcile
For any one instance, later stages win:
pageSize: 25MessagePaginatorsclient.config.set({ channel: { … } })new MessageComposer({ config })client.config.setSetupFunction(key, fn)instance.updateConfig({ … })The one non-obvious part is why 1b and 3 are separate when both arrive through a constructor. It's who
passed the argument. When the SDK builds a paginator it fills in a page size, a throttle and a
cursor — if those counted as integrator's arguments they would outrank stage 2, and no
client.config.set()could ever change them. So the SDK's own values sit at 1b, below integrator's registration; only arguments integrators
actually passed sit at 3, above it.
Three properties of this that are load-bearing:
any of them changes. So applying the server's restrictions is idempotent, and stage 6 narrowing a field
never destroys the request underneath it — if the server later relents, the request is honoured.
updateConfig— all threehave the server's restrictions re-asserted over the result. Nothing above can widen past them.
undefinedfrom the server means "no opinion", not "no", so the request stands.
The consequence worth knowing:
instance.configis the resulting answer. It should be read instead of the rawserver flag —
channel.serverConfig?.uploads- which gives us only the server's half.Why
Setting a page size used to require writing a function:
Four things were wrong with the old version:
Channel,MessageComposer,StreamChat,Thread. Paginators,the notification manager, the reminder manager and the delivery reporter had no way in at all.
to a known state.
separately and combine it in multiple places. Most code didn't, so a UI would offer a feature the SDK then
refused.
Where it lives
Two objects, and neither holds what the other holds:
InstanceConfigurationRegistry(client.config)ConfigController(per instance)Every configurable class exposes the same shape:
configState(aStateStore),config,updateConfig(),initializeConfig().Threadwas the last one resolving configuration by hand and nowgoes through
ConfigControllertoo, so there are no exceptions left.The key space is closed.
InstanceConfigTreeandInstanceSetupFunctionArgsare type aliases, notinterfaces, so a key can be neither misspelled into existence nor added by module augmentation:
A class an integrator owns configures itself — a constructor argument, a setter, its own registry.
Routing it through
client.configwould put a value the SDK can neither type nor apply into a tree whoseonly reader is the SDK.
The machinery is internal too.
ConfigController(which resolves a configuration) andapplyInstanceConfiguration(which subscribes an instance to a key) are both unexported: everyconfigurable class is one this package constructs, so neither has a caller outside it.
Details in
v9-to-v10-migration-guide-other.mdand…-type-renames.md.Two that produce no compile error
Uploads to storage outside Stream must now be declared. A custom
doUploadRequestused to be takenas proof of that, which was wrong — many still post to Stream. Without the flag, uploads are refused for
users lacking the
upload-filecapability, and the attachment button disappears:Link previews now default to on. The old default of off overrode apps that had enabled enrichment
server-side. Turning them off is now explicit:
Renames — all caught by the compiler
serverConfigreturns whatgetConfig()did.channel.configis a different thing: the resolvedvalue, with six server flags already combined with locally registered settings — that is the one to read
when checking whether a feature is on.
serverConfiganswers for this channel, not for its type. Most ofChannelConfigWithInfois achannel-type setting, but a channel's own
config_overridesnarrowuploads,url_enrichment,typing_events,replies,quotes,reactions,shared_locations,max_message_length,commandsand
user_message_remindersfor that channel alone — and this SDK is one of the things that can set them(
client.channel(type, id, { config_overrides })sends them onquery/watch;channel.update()andupdatePartial()reach the same state). So the cache behind the getter is keyed by cid: v9'sclient.configskey space, under a name that says whose configuration it holds now thatclient.configis the integrator's.
It is
undefineduntil the channel has been queried or watched, as in v9. There is deliberately notype-level fallback — the only value available to seed one with is a sibling channel's effective config,
overrides included, which is the leak the cid keying exists to prevent.
channel.configcovers thatwindow with its defaults, which is another reason to read it rather than the raw flag.
Note for tests:
serverConfigis a getter, sovi.spyOn(channel, 'getConfig')has no direct equivalent.A mocked query response has to carry a
cidmatching the channel under test, or its config lands under adifferent key.
Smaller things
MessageComposerConfiggainspolls;AttachmentManagerConfiggainsenabledandcustomCdn. Allhave defaults, so only code that types a variable as the complete config object is affected.
setInstanceConfigurationFunction,instanceConfigurationServiceandconfigsStore(all RC-line only).configsis renamed tochannelServerConfigs, not removed — thecid key space is unchanged, so a v9
client.configs[cid]lookup translates directly. Preferchannel.serverConfigover either.used to let a stale earlier request win once the server allowed it again.
RC-line only
None of these ever shipped in v9, so they are listed for anyone tracking
release-v10rather than asmigration steps.
channel.configcarries only the channel's own fields. Thechannelslice can also carrymessagePaginator,pinnedMessagesPaginatorandmessageOperations— those still work as scopedoverrides for the objects the channel owns, they are just no longer copied onto
channel.configaswell, where nothing read them and
ChannelConfignever declared them. Readchannel.messagePaginator.configinstead. Side benefit: registering a scoped option no longer notifieschannel.configStatesubscribers. Silent — the old read returnsundefined.setConfig('anything', …)andsetSetupFunction('anything', fn)no longercompile, and the two key interfaces are type aliases so augmentation cannot add one either.
applyInstanceConfiguration,ConfigControllerandConfigControllerOptionsare no longer exported.liveLocationManager.dispose()is new, and needs calling.unregisterSubscriptions()isref-counted and no longer releases the configuration subscription — it never should have, since with two
callers sharing a manager the first to leave stopped a still-live instance from tracking
client.config. Untildispose()runs, the configuration registry holds a handle to the manager.Nothing fails to compile.
SearchControlleralready worked this way.ThreadgainsupdateConfig()and resolves throughConfigControllerlike every other entity.Additive.
Review guidance
src/configuration/ConfigController.ts— the layering itself (orderedLayers,resolve). Start here.src/configuration/utils/serverAuthority.ts— stage 6: the AND rule and the upper-bound rule.src/channel.ts— the most involved consumer: controller, server restrictions, and thesubscription that re-derives when the server's answer arrives after construction.
src/configuration/shape.tsis 644 lines of field descriptions, not logic — skim it.Docs
docs/instance-configuration.md— full reference: the stages, the registry/resolver split, serverauthority, custom keys, resetting.