@rudder/sdk
Rudder client SDK for web games: device and social auth, player state (profile, balances, items), catalog currencies, player storage, and calls to installed modules through generated typed clients.
Install
The package is published to the Rudder package 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/sdk
Quickstart
import { createClient } from '@rudder/sdk';
const client = createClient({
projectKey: 'your-project-key', // the key selects the environment
// baseUrl: 'https://api.rudder.build' (default)
// tokenStore: localStorage with an in-memory fallback (default)
// appVersion: '1.4.2', sent with platform 'web' on login and refresh
});
await client.auth.loginWithDevice();
const profile = client.player.value;
const gold = profile?.balances?.find((b) => b.currency === 'gold')?.balance;
const swords = profile?.items?.find((i) => i.item === 'sword')?.quantity ?? 0;
const progress = client.storage.value?.items?.find((i) => i.type === 'progress')?.data;
client.dispose();
Module clients
Game features are installed modules. Generate a typed client for the modules
installed in an environment with the rudder CLI and commit the file:
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
rudder client generate --lang ts --out src/rudder.modules.ts # --environment staging (default) | prod
import { createClient, RudderModuleError } from '@rudder/sdk';
import { modules, type LeaderboardsSubmitError } from './rudder.modules';
const client = createClient({ projectKey });
const m = modules(client);
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });
try {
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
} catch (error) {
if (error instanceof RudderModuleError) {
const code = error.code as LeaderboardsSubmitError;
console.warn(code, error.message, error.requestId);
}
}
Only trigger: client functions are generated. Each generated function calls
client.modules.call<Result>(module, fn, args, { moduleVersion }), which you
can also call directly:
argsis always sent ({}when omitted); the promise resolves toresult.- Every call gets one
Idempotency-Key, reused on its retries, so a retried call runs at most once on the server. - Retries: up to 2, with jittered backoff, on network errors, 5xx and 409
conflict. Other 4xx responses are never retried. moduleVersionis sent asX-Rudder-Module-Version: <module>@<version>. The backend logs a warning on a major mismatch and still runs the call.- A 422 rejects with
RudderModuleError(code,message,requestId).
Surface
| Area | Access |
|---|---|
| Auth | client.auth.loginWithDevice(), loginWithGoogle({ idToken }), loginWithApple({ idToken }), loginWithCustom({ customData }), linkWithGoogle(idToken), linkWithApple(idToken), linkWithCustom({ customData }), unlinkIdentity(provider), logout(), isAuthenticated, onAuthStateChange(cb) |
| Player | client.player state: player, identities, balances, items |
| Catalog currencies | client.catalog state (Map<slug, CatalogCurrency>) |
| Storage | client.storage state, read-only (modules write it) |
| Modules | client.modules.call<T>(module, fn, args?, { moduleVersion? }), usually through a generated client |
balances carries one entry per released currency and items one per
inventory stack.
Error handling
All SDK errors extend RudderError and carry an optional machine-readable
code:
import {
RudderError,
RudderNetworkError,
RudderHttpError,
RudderAuthError,
RudderModuleError,
} from '@rudder/sdk';
try {
await client.player.reload();
} catch (error) {
if (error instanceof RudderAuthError) {
// 401: session expired and the refresh failed; the client already
// cleared tokens and emitted 'signed-out' via onAuthStateChange.
showLogin();
} else if (error instanceof RudderHttpError) {
console.error(error.status, error.code, error.body);
} else if (error instanceof RudderNetworkError) {
// fetch failed or timed out; the underlying error is in error.cause
console.error('offline?', error.cause);
}
}
RudderModuleError extends RudderHttpError (status 422). Its type
parameter narrows code to a generated union such as
LeaderboardsSubmitError. A module trap is a 500 and a timeout a 504.
A missing projectKey throws RudderError with
code: 'sdk/invalid-options' from createClient.
Track the session lifecycle without polling the token store:
client.auth.onAuthStateChange((state) => {
// fires immediately with the current state, then on every change
setSignedIn(state === 'signed-in');
});
Data liveness model
State objects (player, catalog, storage) are
warmed at login (player and catalog) or on first use, then kept fresh by
a revision poll: the client asks the backend for entity revisions every
30 s (±20% jitter) and reloads only the entities whose revision grew.
Polling pauses while the tab is hidden and resumes on visibilitychange.
Storage writes invalidate the affected domain immediately. Expect data to be
eventually consistent within one poll interval; call reload() when you need
a guarantee.
React recipe
SyncedState.onChange fires immediately with the current snapshot, which maps
straight onto 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?.balances?.map((b) => `${b.currency}: ${b.balance}`).join(', ')}</div>;
}
Development
npm run typecheck # tsc over src and tests
npm test # unit + local e2e (jsdom)
npm run build # tsup → dist (ESM + CJS + d.ts)
npm run test:e2e-prod # drives a seeded project on the live backend
Generated wire types (src/generated) come from the platform's API
contract. Do not edit them by hand.
test/fixtures/rudder.modules.ts is CLI output for
test/fixtures/leaderboards.manifest.json; npm run typecheck checks that it
accepts RudderClient. Refresh it after generator changes:
rudder client generate --lang ts --manifests <dir with leaderboards.manifest.json> --out test/fixtures/rudder.modules.ts