Skip to content

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.

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):

Terminal window
npm install @rudder/sdk
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:

OptionDefaultPurpose
baseUrlhttps://api.rudder.buildAPI base URL
tokenStorelocalStorage with in-memory fallbackToken persistence (see below)
appVersionnot sentGame build version sent with platform: 'web' on every login and token refresh, for segment rules
requestTimeoutMs10000Per-request timeout
syncIntervalMs30000 (±20% jitter)Revision poll interval

Create one client per page or session and reuse it.

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 state

After 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'.

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.

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.

client.player, client.catalog and client.storage are SyncedState objects with the same interface:

  • value: the cached value, or undefined before the first load.
  • status: 'idle' | 'loading' | 'ready' | 'error', plus error when 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>;
}

client.player.value is a PlayerProfile:

  • player: id, projectId, nickname, region, language, createdAt;
  • identities: provider, subject, createdAt per linked identity;
  • wallets: currency and balance per released currency;
  • counters: { [slug]: number } with the values of number counters 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.

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');

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.

Generate a typed client for the modules installed in an environment and commit the file:

Terminal window
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
rudder client generate --lang ts --out src/rudder.modules.ts
import { 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',
{},
);
  • args is sent as {} when omitted; the promise resolves to result.
  • 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 conflict are retried up to 2 times with jittered backoff. Other 4xx responses are never retried.
  • moduleVersion is sent as X-Rudder-Module-Version: <module>@<version>.

See Generated module clients for the generator, error unions and remote_config helpers.

All SDK errors extend RudderError and may carry a machine-readable code:

  • RudderNetworkError: the request failed or timed out; the original error is in error.cause. GET requests and module calls are retried first.
  • RudderHttpError: a non-2xx response with status, statusText, body and code.
  • RudderAuthError (extends RudderHttpError): a 401 whose refresh failed. Tokens are already cleared and 'signed-out' already emitted.
  • RudderModuleError<C> (extends RudderHttpError): a module call returned 422 { error, code, requestId }. It exposes code, message and requestId.
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.

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.