TypeScript SDK
@rudder/sdk is the Rudder client SDK for web games. It wraps the
player-facing /sdk/v1 HTTP API behind one client and covers only the kernel:
- authentication and identity linking;
- the player profile (player, identities, wallets, counters);
- the currency catalog;
- player storage;
- calls to installed modules, usually through a generated module client.
Game features such as the store, quests, leaderboards, battle pass and remote
config are modules, not SDK services. The SDK has no resource rows and no
project storage access; game-facing configuration comes from modules such as
remote_config.
Current version: 0.1.0. Versions stay 0.x until the API is stable, and
any minor release may break it. The package ships ESM and CJS builds with type
declarations and has no runtime dependencies.
Installation
Section titled “Installation”The package is published to the Rudder registry at hub.rudder.build, not
npmjs. Point the @rudder scope at it in your project’s .npmrc:
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/Then install as usual (anonymous read access, no token needed):
npm install @rudder/sdkCreating a client
Section titled “Creating a client”import { createClient } from '@rudder/sdk';
const client = createClient({ projectKey: 'your-sdk-key',});projectKey is the SDK key of your project. Find it in the dashboard at
app.rudder.build under Project Settings. A
project has exactly two environments, staging and prod, each with its own
key; the key you pass decides which environment the player belongs to.
A missing projectKey throws RudderError with
code: 'sdk/invalid-options' (SDK_ERROR_INVALID_OPTIONS).
Optional settings:
| Option | Default | Purpose |
|---|---|---|
baseUrl | https://api.rudder.build | API base URL |
tokenStore | localStorage with in-memory fallback | Token persistence (see below) |
appVersion | not sent | Game build version sent with platform: 'web' on every login and token refresh, for segment rules |
requestTimeoutMs | 10000 | Per-request timeout |
syncIntervalMs | 30000 (±20% jitter) | Revision poll interval |
Create one client per page or session and reuse it.
Authentication
Section titled “Authentication”Players are anonymous by default: loginWithDevice() generates a device ID on
the first call, keeps it in localStorage, and exchanges it for an
access/refresh token pair. loginWithGoogle() and loginWithApple() take the
ID token from the provider’s sign-in flow. loginWithCustom() forwards
customData to your own backend’s auth webhook. Configure providers in the
dashboard under Project Settings → Authentication.
await client.auth.loginWithDevice(); // region 'global', language 'en'await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
await client.auth.loginWithGoogle({ idToken, region: 'eu', language: 'de' });await client.auth.loginWithApple({ idToken });
await client.auth.loginWithCustom({ customData: { steamTicket: '...' } });await client.auth.loginWithCustom({ provider: 'steam', customData: { ticket: '...' } });
client.auth.isAuthenticated; // true while an access token is stored
const unsubscribe = client.auth.onAuthStateChange((state) => { // 'signed-in' | 'signed-out'; fires immediately, then on every change});
client.auth.logout(); // clears tokens, stops sync, drops cached stateAfter a login the client loads player and catalog in parallel and starts
the revision poll. Access tokens are refreshed automatically: a 401 triggers
one single-flight refresh and one retry. If the refresh fails, tokens are
cleared and listeners get 'signed-out'.
Linking identities
Section titled “Linking identities”A signed-in player can attach more identities to the same account, so a later
login through any of them lands on the same player. Link calls resolve with no
value; a conflict (the identity belongs to another player, or the provider is
already linked) throws RudderHttpError with status 409.
await client.auth.linkWithGoogle(idToken);await client.auth.linkWithApple(idToken);await client.auth.linkWithCustom({ customData: { sessionId: '...' } });await client.auth.linkWithCustom({ provider: 'steam', customData: { ticket: '...' } });
// The last remaining identity cannot be unlinked.await client.auth.unlinkIdentity('google'); // 'device' | 'google' | 'apple' | 'custom' | 'custom:<name>'Linked identities are in the profile: client.player.value?.identities.
Token storage
Section titled “Token storage”Tokens go through a pluggable TokenStore. The default store uses
localStorage (keys rudder_access_token and rudder_refresh_token) and
falls back to memory where localStorage is unavailable (SSR, private mode).
With the fallback the session lasts only as long as the page.
import { createClient, type TokenStore } from '@rudder/sdk';
const sessionStore: TokenStore = { getAccessToken: () => sessionStorage.getItem('rudder_access_token'), getRefreshToken: () => sessionStorage.getItem('rudder_refresh_token'), saveTokens: (access, refresh) => { sessionStorage.setItem('rudder_access_token', access); sessionStorage.setItem('rudder_refresh_token', refresh); }, clear: () => { sessionStorage.removeItem('rudder_access_token'); sessionStorage.removeItem('rudder_refresh_token'); },};
const client = createClient({ projectKey, tokenStore: sessionStore });createDefaultTokenStore() and createLocalStorageTokenStore() are exported
as well.
State objects
Section titled “State objects”client.player, client.catalog and client.storage are SyncedState
objects with the same interface:
value: the cached value, orundefinedbefore the first load.status:'idle' | 'loading' | 'ready' | 'error', pluserrorwhen set.onChange(listener): fires immediately with{ status, value, error }, then on every change; returns an unsubscribe function.load(): loads once and deduplicates parallel calls.reload(): always refetches.
player and catalog load at login; storage loads on first use.
The client polls GET /sdk/v1/sync every 30 seconds (±20% jitter) and
reloads only the states whose revision grew. Polling pauses while the tab is
hidden. Storage writes refresh storage right away. Otherwise data is
eventually consistent within one poll interval; call reload() when you need
fresh values now, for example after a module call that changes wallets or
counters.
onChange maps onto React’s useSyncExternalStore:
import { useSyncExternalStore } from 'react';
function useSynced<T>(state: { onChange(cb: () => void): () => void; value: T | undefined }) { return useSyncExternalStore( (onStoreChange) => state.onChange(() => onStoreChange()), () => state.value, );}
function Wallet() { const profile = useSynced(client.player); return <div>{profile?.wallets?.map((w) => `${w.currency}: ${w.balance}`).join(', ')}</div>;}Player
Section titled “Player”client.player.value is a PlayerProfile:
player:id,projectId,nickname,region,language,createdAt;identities:provider,subject,createdAtper linked identity;wallets:currencyandbalanceper released currency;counters:{ [slug]: number }with the values ofnumbercounters that are visible to game clients.
const profile = client.player.value;const coins = profile?.wallets?.find((w) => w.currency === 'coins')?.balance ?? 0;const wins = profile?.counters?.wins ?? 0;There is no client-side grant or spend call. Balances and counters change in module functions and admin operations.
Catalog
Section titled “Catalog”client.catalog.value is a Map<slug, CatalogCurrency> (slug, name,
icon, data) built from the currency counters of the environment’s latest
release.
const gold = client.catalog.value?.get('gold');Storage
Section titled “Storage”client.storage holds the player’s storage items (type, id, data). The
server keeps one item per player and type; data is a string, so serialize
JSON yourself.
await client.storage.save('progress', JSON.stringify({ level: 12 }));await client.storage.delete('progress');
await client.storage.load();const progress = client.storage.value?.items?.find((item) => item.type === 'progress');The state loads the first 100 items. See Storage for limits.
Module calls
Section titled “Module calls”Generate a typed client for the modules installed in an environment and commit the file:
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...rudder client generate --lang ts --out src/rudder.modules.tsimport { modules } from './rudder.modules';
const m = modules(client);await m.leaderboards.submit({ slug: 'weekly', score: 120 });const { entries } = await m.leaderboards.top({ slug: 'weekly', limit: 10 });Each generated function calls
client.modules.call<Result>(module, fn, args, { moduleVersion }). You can
call it directly for a module that is not in the generated file:
const result = await client.modules.call<{ values: Record<string, unknown> }>( 'remote_config', 'get', {},);argsis sent as{}when omitted; the promise resolves toresult.- Each call gets an
Idempotency-Key(UUID v4) that is reused on its retries, so the server runs the call at most once. - Network errors, 5xx and 409
conflictare retried up to 2 times with jittered backoff. Other 4xx responses are never retried. moduleVersionis sent asX-Rudder-Module-Version: <module>@<version>.
See Generated module clients for the generator,
error unions and remote_config helpers.
Error handling
Section titled “Error handling”All SDK errors extend RudderError and may carry a machine-readable code:
RudderNetworkError: the request failed or timed out; the original error is inerror.cause. GET requests and module calls are retried first.RudderHttpError: a non-2xx response withstatus,statusText,bodyandcode.RudderAuthError(extendsRudderHttpError): a 401 whose refresh failed. Tokens are already cleared and'signed-out'already emitted.RudderModuleError<C>(extendsRudderHttpError): a module call returned 422{ error, code, requestId }. It exposescode,messageandrequestId.
import { RudderAuthError, RudderHttpError, RudderModuleError, RudderNetworkError } from '@rudder/sdk';import { modules, type StoreBuyError } from './rudder.modules';
try { await modules(client).store.buy({ offerSlug: 'starter_pack' }); await client.player.reload();} catch (error) { if (error instanceof RudderModuleError) { const code = error.code as StoreBuyError; if (code === 'purchase_limit_reached') showSoldOut(); } else if (error instanceof RudderAuthError) { showLogin(); } else if (error instanceof RudderHttpError) { console.error(error.status, error.code, error.body); } else if (error instanceof RudderNetworkError) { console.error('offline?', error.cause); }}A module crash returns 500 and a timeout 504, both RudderHttpError without
details.
Disposal
Section titled “Disposal”Call client.dispose() when tearing the client down (page unmount, hot module
replacement, account switch). It stops the sync poll and drops cached state.
client.auth.logout() clears the session and cached state but keeps the
client usable.
See also
Section titled “See also”- C# SDK and Unity SDK: the same surface for .NET and Unity.
- Generated module clients.
- Game servers call the
/game/v1HTTP API with the environment’s admin key; there is no server SDK.