--- name: rudder-web-sdk description: Use when working with the Rudder TypeScript/JavaScript SDK (@rudder/js-sdk) — the browser/player-facing SDK for the Rudder LiveOps platform. Covers client setup, device/google/apple/custom auth, identity linking, observable kernel domains (player, catalog, storage), modules.call, resources(slug), and error handling. Load this whenever code imports from @rudder/js-sdk or RudderClient appears. --- # Rudder Web SDK (@rudder/js-sdk) Browser-facing player SDK for the Rudder LiveOps platform. Source of truth: `liveops-web-sdk/src` (generated wire types in `src/generated` come from the gateway's apigen — never edit them by hand). 2.0 kernel surface: auth, player (wallets), catalog currencies, storage, project storage, revision sync, `client.modules.call(module, fn, args)`, and `client.resources(slug)`. Module-specific methods (battle pass, quests, leaderboards, stores, inventory, remote config, scenarios) are gone. ## Install Published to a private registry, not npmjs. Point the `@rudder` scope at it in `.npmrc` (anonymous read, no token): ``` @rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/ ``` ```bash npm install @rudder/js-sdk ``` ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies. ## Init ```ts import { RudderClient } from '@rudder/js-sdk'; const client = new RudderClient({ baseUrl: 'https://api.rudder.build', // required projectKey: 'your-project-key', // required // tokenStore?: TokenStore — default: localStorage, in-memory fallback // requestTimeoutMs?: number — default 10000 // syncIntervalMs?: number — revision poll, default 30000 (±20% jitter) // onEffectError?: (error) => void — default console.error }); ``` Missing `baseUrl`/`projectKey` throws `RudderError` with `code: 'sdk/invalid-options'` from the constructor. Call `client.dispose()` on unmount/HMR to stop the sync poll and drop cached state. ## Auth essentials ```ts await client.auth.loginWithDevice({ region, language, nickname }); // device id auto-generated/persisted await client.auth.loginWithGoogle({ idToken, region, language, nickname }); await client.auth.loginWithApple({ idToken, region, language, nickname }); await client.auth.loginWithCustom({ customData, provider?, region, language, nickname }); await client.auth.linkWithGoogle(idToken); await client.auth.linkWithApple(idToken); await client.auth.linkWithCustom({ customData, provider? }); await client.auth.unlinkIdentity(provider); // 'device' | 'google' | 'apple' | 'custom' | 'custom:' client.auth.logout(); client.auth.isAuthenticated; client.auth.onAuthStateChange(cb); ``` Linked identities are listed on the profile: `client.player.data.identities` (`PlayerProfileIdentity { provider, subject, createdAt }`). Tokens are saved to the `TokenStore` automatically. The transport injects `Authorization: Bearer `, retries GETs, and single-flight refreshes on 401; a failed refresh clears tokens and emits `signed-out`. Details: [reference/auth.md](reference/auth.md). ## Client surface | Area | Access | Kind | |---|---|---| | Auth | `client.auth` | service | | Player profile + wallets | `client.player` | observable `SyncedState` | | Catalog currencies | `client.catalog` | observable `SyncedState>` | | Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations | | Project storage | `client.projectStorage` + `.save(items)` | observable + mutation | | Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionClientResponse` (`{ result }`) | | Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` | There is no UGC, wallet service, or standalone economy service — wallets live on `client.player.data.wallets`. Quests, stores, inventory, leaderboards, battle pass, and remote config are installed modules: call `client.modules.call('quests', 'list')` (etc.) and read rows with `client.resources('items')`. ## Observable domains — the shared pattern `player`, `catalog`, `storage`, and `projectStorage` all extend `SyncedState`: - `.data: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`, `.error?: Error` - `onChange(cb: (snapshot: SyncedSnapshot) => void): () => void` — fires immediately with the current snapshot; maps onto React `useSyncExternalStore` - `load(): Promise` — deduplicated, no-op when ready - `reload(): Promise` — forced refetch `player` and `catalog` are warmed once at login; `storage` and `projectStorage` load on first use. All observable domains are then kept fresh by a revision poll every 30 s (±20% jitter); polling pauses while the tab is hidden. Storage mutations invalidate their domain immediately. Call `reload()` when you need a freshness guarantee. ## Errors All SDK errors extend `RudderError` (optional machine-readable `code`): - `RudderNetworkError` — fetch failed / timeout (`error.cause` holds the original) - `RudderHttpError` — non-2xx, carries `status`, `statusText`, `body`, `code` - `RudderAuthError extends RudderHttpError` — 401 after failed refresh; tokens already cleared, `signed-out` already emitted - `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`) — constructor validation The platform error codes are `forbidden` and `row_referenced` (`src/generated/errors.ts`, not exported from the package entry); compare `error.code === 'forbidden'`. `client.modules.call` resolves to `{ result }` only; console output and the kernel call log never reach players. A module business error rejects with `RudderHttpError` whose `status` is 422 and whose `code` is the module's own code (the body is `{ error, code, requestId }`). Module codes are open strings with no generated constants, so compare against the codes the module documents. A module trap is a 500 and a timeout a 504, both without details. ## Reference files - [reference/auth.md](reference/auth.md) — login flows, options, token store, session lifecycle - [reference/player.md](reference/player.md) — profile + wallets (`PlayerProfile`, `Wallet`) - [reference/storage.md](reference/storage.md) — player + project storage, limits