diff --git a/.claude/skills/docs-sync/SKILL.md b/.claude/skills/docs-sync/SKILL.md
index a167613..8576639 100644
--- a/.claude/skills/docs-sync/SKILL.md
+++ b/.claude/skills/docs-sync/SKILL.md
@@ -51,17 +51,17 @@ Before the sync check, make sure every server-side snippet on the tested pages h
- Pages: every page of the `[chat][node].json` and `[platform][default].json` sidebars (`$DOCS_DIR/_sidebars/`) under `chat/` or `platform/`, the same list as `findSidebarPages` in `sync/parse.ts`.
- Fences: every JS fence (`js`, `javascript`, `ts`, `typescript`) labelled `JavaScript` or unlabelled, together with its tab group: the other fences in the same `` block, or the fence alone when it isn't in one.
-- Out of scope: `React` and `React Native` fences, `Node.js` fences inside a `` block, and comment-only fences.
+- Out of scope: `React` and `React Native` fences, the stream-chat-js@9 fences (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`), `Node.js` fences inside a `` block, and comment-only fences.
List them with a throwaway script in the scratchpad (page, heading, line, the labels in the group, the code). Then classify each group by its tabs first and the prose second:
-| Group | Action |
-| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
-| A `JavaScript` fence with server-only code (secret, `updateAppSettings`, `createToken`, ...) | Relabel it `Node.js` and rewrite it to the node SDK call (`client.updateApp`, ...). |
-| Only client SDKs (`JavaScript` plus Kotlin, Swift, Dart, Unity, Unreal, React, ...) | Client-side: no `Node.js` tab, even when a server equivalent exists. |
-| `JavaScript` only (or with non-SDK tabs such as `JSON`) | Add a `Node.js` tab when the node SDK can do the same thing, unless the heading or prose hints "client-side only". See below. |
-| Other server SDK tabs (Python, Ruby, PHP, Go, C#, Java) but no `Node.js` | Add a `Node.js` tab. If the others are comment-only notes ("client-side only operation"), add the same note for the Node SDK. |
-| A client group, and the page has its own server-side section for it ("Server-Side Usage", "Server Side") | No `Node.js` tab: the server-side section covers it. |
+| Group | Action |
+| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| A `JavaScript` fence with server-only code (secret, `updateAppSettings`, `createToken`, ...) | Relabel it `Node.js` and rewrite it to the node SDK call (`client.updateApp`, ...). Relabel its `JavaScript (stream-chat-js@9)` partner, if any, `Node.js (stream-chat-js@9)`, code unchanged. |
+| Only client SDKs (`JavaScript` plus Kotlin, Swift, Dart, Unity, Unreal, React, ...) | Client-side: no `Node.js` tab, even when a server equivalent exists. |
+| `JavaScript` only (or with non-SDK tabs such as `JSON`) | Add a `Node.js` tab when the node SDK can do the same thing, unless the heading or prose hints "client-side only". See below. |
+| Other server SDK tabs (Python, Ruby, PHP, Go, C#, Java) but no `Node.js` | Add a `Node.js` tab. If the others are comment-only notes ("client-side only operation"), add the same note for the Node SDK. |
+| A client group, and the page has its own server-side section for it ("Server-Side Usage", "Server Side") | No `Node.js` tab: the server-side section covers it. |
JavaScript-only groups are common: JavaScript is usually the first SDK documented, and the other SDKs follow later. Being alone is no hint that the code is client-side. Client APIs used in passing aren't either: translate them to the server side (`client.user?.language` → `user.language` of a user you have; the message from `client.chat.getMessage`). Skip the group only when:
@@ -74,6 +74,7 @@ To check what the node SDK can do, read `src/StreamClient.ts`, `src/StreamChatCl
- Mirror the `JavaScript` fence: same comments, structure and placeholder literals (`"user-1"`, `"messaging:general"`).
- Use the node SDK's shapes and types: `client.chat.*` / `client.feeds.*`, request objects (`filter_conditions`, `preferences: [...]`), `Date` where the types say `Date` (not `.toISOString()`), and `user_id` where the client code acts as the connected user (server-side `queryChannels` with `hidden` / `muted`, drafts, devices need it).
+- Don't write a `Node.js (stream-chat-js@9)` fence for it: list the group in the report so the v9 version can be added by hand.
- Put it before the group's ``. A fence that isn't in a `` block: wrap it and the new fence in ``.
- From `$DOCS_DIR`, run `npx prettier --write ` and `node scripts/tab-lint.mjs ` (it puts the tabs in the canonical order: client SDKs first, then server SDKs).
@@ -90,7 +91,7 @@ It runs without network and reports four kinds of failure. Collect them all, gro
| Failure | Meaning |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `snippets without a test region` | A `Node.js` fence on a tested sidebar page (chat Node, platform) has no region: a new snippet, a new page, or a fence whose key changed (see the next row). |
-| mislabelled JS fences | A JS fence is unlabelled or has a label other than `JavaScript`, `React`, `React Native` or `Node.js`. |
+| mislabelled JS fences | A JS fence is unlabelled or has a label other than `JavaScript`, `React`, `React Native`, `Node.js` or their stream-chat-js@9 versions. |
| `no such fence` | A region's key (`docs`, `heading`, `tab`, `index`) matches no fence: the fence was removed, moved, or its heading was renamed. The failure lists the page's fence keys. |
| ` differs from :` | The region's code and the fence's code differ. |
@@ -104,7 +105,7 @@ Work one docs page at a time.
### Mislabelled fence
-Label it `Node.js` for `@stream-io/node-sdk` (server-side) code, `JavaScript` for client code. Code that works on both sides (e.g. a bare filter object) gets `JavaScript`: an unlabelled `js` fence already renders like `label="JavaScript"` (the JavaScript route's own sample, a titled fallback on the Node route), so the page doesn't change. Whether it also needs a `Node.js` tab is decided by the sweep (step 3). Otherwise, `JavaScript`, `React` and `React Native` fences, and `Node.js` fences inside a `` block (reference implementations without the SDK), are out of scope: never test or change them.
+Label it `Node.js` for `@stream-io/node-sdk` (server-side) code, `JavaScript` for client code. Code that works on both sides (e.g. a bare filter object) gets `JavaScript`: an unlabelled `js` fence already renders like `label="JavaScript"` (the JavaScript route's own sample, a titled fallback on the Node route), so the page doesn't change. Whether it also needs a `Node.js` tab is decided by the sweep (step 3). Otherwise, `JavaScript`, `React` and `React Native` fences, the stream-chat-js@9 fences (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`), and `Node.js` fences inside a `` block (reference implementations without the SDK), are out of scope: never test or change them.
### Region differs from its fence (docs changed)
@@ -134,7 +135,7 @@ Write a test for it in the page's test file (create the file if the page is new)
- Comment-only fences need no region.
- A fence that isn't code (e.g. a bare data shape) is a docs bug: rewrite it as code that runs (e.g. reading and logging the fields), and report it. Never remove a `Node.js` fence just because it is a fragment or can't be tested: wrap it, or put it in `it.skip`.
- A snippet the test app can't run goes in `it.skip('BLOCKED: ', ...)` so it is still typechecked; report the exact API error.
-- **Client-side code in a `Node.js` fence** (needs a connected user or a WebSocket, or a method the node SDK doesn't have: `connectUser`, `channel.watch()`, `client.on(...)`): never test different code from what the fence shows. If the node SDK can do the same thing, make the smallest docs fix that turns the fence into that call (`channel.watch()` → `channel.getOrCreate()`, `user_id` instead of `connectUser`). If it can't (events, WebSocket, local client state), the fence is mislabelled: label it `JavaScript`. Use `it.skip('BLOCKED: client-side code: ...')` only while that decision is open. Report it either way. To check what a server-side endpoint accepts, read the SDK source (`src/StreamClient.ts`, `src/StreamChatClient.ts`, `src/StreamChannel.ts`, `src/gen/`) or the OpenAPI spec (`chat-serverside-api.json` in https://github.com/GetStream/protocol/tree/main/openapi/v2).
+- **Client-side code in a `Node.js` fence** (needs a connected user or a WebSocket, or a method the node SDK doesn't have: `connectUser`, `channel.watch()`, `client.on(...)`): never test different code from what the fence shows. If the node SDK can do the same thing, make the smallest docs fix that turns the fence into that call (`channel.watch()` → `channel.getOrCreate()`, `user_id` instead of `connectUser`). If it can't (events, WebSocket, local client state), the fence is mislabelled: label it `JavaScript`. Its `Node.js (stream-chat-js@9)` partner, if any, becomes `JavaScript (stream-chat-js@9)`, code unchanged. Use `it.skip('BLOCKED: client-side code: ...')` only while that decision is open. Report it either way. To check what a server-side endpoint accepts, read the SDK source (`src/StreamClient.ts`, `src/StreamChatClient.ts`, `src/StreamChannel.ts`, `src/gen/`) or the OpenAPI spec (`chat-serverside-api.json` in https://github.com/GetStream/protocol/tree/main/openapi/v2).
- **Push and webhooks**: don't test that notifications or webhook events arrive; assert the response or read the setting back (`getApp()`, `listPushProviders()`, ...). Never touch or use the test app's own push provider (see "Test app" in `AGENTS.md`): only use providers the test creates with `uniqueId` names. Snippets that need real credentials run in `describe.skipIf(!hasFirebaseCredentials)` (`helpers/push.ts`); the README's "Push credentials" section has the details.
- **App-level side effects** (app settings, built-in channel types, grants, webhook URLs, push config, ...): read the original value first and restore it with `cleanup.add(...)`, or the file fails with `DRIFT`.
diff --git a/__tests__/docs-snippets/README.md b/__tests__/docs-snippets/README.md
index 6ce1c11..3d75411 100644
--- a/__tests__/docs-snippets/README.md
+++ b/__tests__/docs-snippets/README.md
@@ -4,7 +4,8 @@ Typed tests that run the server-side Node.js snippets of the chat docs (the page
`getstream.io/content/docs/_sidebars/[chat][node].json`) and of the platform docs (the pages of
`getstream.io/content/docs/_sidebars/[platform][default].json`) against a real Stream app.
Only fences labelled `Node.js` are in scope (docs fences must always have a label): they are fixed when they are
-wrong. `JavaScript`, `React` and `React Native` fences (client-side code) and `Node.js` fences inside a `` block (reference
+wrong. `JavaScript`, `React` and `React Native` fences (client-side code), the stream-chat-js@9 fences (`JavaScript (stream-chat-js@9)`,
+`Node.js (stream-chat-js@9)`) and `Node.js` fences inside a `` block (reference
implementations without the SDK) are out of scope and are neither tested nor changed.
```sh
@@ -80,8 +81,8 @@ await channel.getOrCreate({ data: { created_by_id: creatorId } });
- `index` is 1-based among fences with the same heading text and tab (default 1).
- **COPY**: `name="literal"` pairs, applied to the region before comparing (identifier `name` becomes the docs literal). Don't use COPY variables as object keys or shorthand properties.
- **Comparison**: both sides are formatted with the same prettier options, so quote style and line wrapping don't matter, but any code or comment change does.
-- **Coverage** (checked first): every `Node.js` fence (`js`/`javascript`/`ts`/`typescript`) on the pages of the chat Node sidebar and the platform sidebar (`content/docs/_sidebars/[chat][node].json`, `[platform][default].json`), whether or not the page has tests yet, must have a region (inside `it.skip` if it can't run). `JavaScript` / `React` / `React Native` fences and comment-only fences are ignored.
-- **Labels**: every JS fence on those pages must be labelled `JavaScript`, `React`, `React Native` or `Node.js`; unlabelled or otherwise labelled JS fences fail.
+- **Coverage** (checked first): every `Node.js` fence (`js`/`javascript`/`ts`/`typescript`) on the pages of the chat Node sidebar and the platform sidebar (`content/docs/_sidebars/[chat][node].json`, `[platform][default].json`), whether or not the page has tests yet, must have a region (inside `it.skip` if it can't run). `JavaScript` / `React` / `React Native` / stream-chat-js@9 fences and comment-only fences are ignored.
+- **Labels**: every JS fence on those pages must be labelled `JavaScript`, `React`, `React Native`, `Node.js`, `JavaScript (stream-chat-js@9)` or `Node.js (stream-chat-js@9)`; unlabelled or otherwise labelled JS fences fail.
- **Excluded pages**: `EXCLUDED_PAGES` in `sync/parse.ts` lists sidebar pages that are deliberately not validated, with a reason. Both checks skip them.
- **`DOCS_SYNC_WRITE= yarn test:docs:sync`** writes the regions of that page into the docs fences (docs prettier style). Use it to apply a fix to the docs. `DOCS_SYNC_WRITE=1` writes all pages.
- **`DOCS_DIR`** points at another docs checkout (default: `../getstream.io/content/docs` next to this repo). The check is skipped if the directory doesn't exist.
diff --git a/__tests__/docs-snippets/sync/docs-sync.test.ts b/__tests__/docs-snippets/sync/docs-sync.test.ts
index 865bec8..ef58fd0 100644
--- a/__tests__/docs-snippets/sync/docs-sync.test.ts
+++ b/__tests__/docs-snippets/sync/docs-sync.test.ts
@@ -21,7 +21,8 @@ import type { DocsFence, TestRegion } from './parse';
// `yarn test:docs:sync`, in this order:
// 1. Every `Node.js` fence (except comment-only fences) on the pages of the chat Node
// sidebar and the platform sidebar has a test region. `JavaScript`, `React` and
-// `React Native` (client-side) fences, and fences inside a
+// `React Native` (client-side) fences, the stream-chat-js@9 fences
+// (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`), and fences inside a
// `` block, are out of scope.
// 2. Every JS fence on those pages is labelled with one of `ALLOWED_JS_LABELS`.
// 3. Every marked test region points at an existing fence and matches it (after
@@ -97,7 +98,7 @@ describe.skipIf(!hasDocs)(`docs sync (${DOCS_DIR})`, () => {
);
expect(
mislabelled,
- `JS fences need label="${ALLOWED_JS_LABELS.join('" or label="')}" (client-side code or @stream-io/node-sdk code)`,
+ `JS fences need label="${ALLOWED_JS_LABELS.join('" or label="')}" (client-side code, @stream-io/node-sdk code, or their stream-chat-js@9 versions)`,
).toEqual([]);
});
diff --git a/__tests__/docs-snippets/sync/parse.ts b/__tests__/docs-snippets/sync/parse.ts
index 834356c..337cc6c 100644
--- a/__tests__/docs-snippets/sync/parse.ts
+++ b/__tests__/docs-snippets/sync/parse.ts
@@ -55,8 +55,16 @@ const JS_LANGS = new Set(['js', 'javascript', 'ts', 'typescript']);
export const JS_TAB = 'JavaScript';
/** The tested tab (`@stream-io/node-sdk` code). */
export const NODE_TAB = 'Node.js';
-/** Out of scope too: client-side React / React Native SDK code. */
-const OUT_OF_SCOPE_TABS = ['React', 'React Native'];
+/**
+ * Out of scope too: client-side React / React Native SDK code, and the stream-chat-js@9
+ * versions shown next to `JavaScript` (client-side) and `Node.js` (server-side).
+ */
+const OUT_OF_SCOPE_TABS = [
+ 'React',
+ 'React Native',
+ 'JavaScript (stream-chat-js@9)',
+ 'Node.js (stream-chat-js@9)',
+];
/**
* Opens a block of reference implementations that don't use the SDK (platform pages).
* Its `Node.js` fences are out of scope.