npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@crowdedkingdoms/crowdyjs

v8.0.1

Published

Client SDK for Crowded Kingdoms GraphQL API with UDP proxy support

Readme

CrowdyJS

The official browser-first TypeScript SDK for Crowded Kingdoms. CrowdyJS gives you typed clients for auth, the world/replication GraphQL API, and the UDP proxy subscription stream. As of v7 it follows the Overworld two-token model: an identity session token for the Management API, and short-lived app-scoped tokens for gameplay via client.portal (see Overworld portals & app-scoped tokens (v7)).

Install

npm install @crowdedkingdoms/crowdyjs

Renamed package, same version line. This SDK moved to the @crowdedkingdoms npm org but kept its v6 version line. @crowdedkingdoms/[email protected] continues directly from the former @crowdedkingdomstudios/[email protected] — same code, new package name. (Two interim 1.0.x publishes during the org move reset the version by mistake; they remain on npm but are superseded by 6.1.1, which is latest.) See MIGRATION.md.

CrowdyJS v4 targets browsers by default and uses native fetch, WebSocket, crypto, btoa, and atob. Node tools can still use the SDK, but must provide browser-compatible globals when opening realtime connections.

Server compatibility: v5.2+ targets environments on release v0.1.19 or later (cks-game-api >= v0.10.3, cks-management-api >= v0.1.70). The destructive mutations send an idempotencyKey argument that older servers don't define. v6.1's client.gameApps.deleteGrid additionally requires release v0.1.33+ (cks-game-api >= v0.12.3).

Standalone builds and schema refresh

CrowdyJS is a standalone public package. A clean external clone must be able to run npm install && npm run build without sibling CKS API repos checked out. For that reason the build uses committed schema artifacts in this repo:

  • schema.gql — merged Management API + Game API SDL.
  • src/generated/graphql.ts — generated TypeScript operation types.

Schema refresh is explicit:

# Refresh from the published production SDLs.
npm run schema:sync:prod
npm run codegen

# Or, inside the CKS wrapper repo, refresh from local API checkouts.
npm run schema:sync:local
npm run codegen

# Or use exact file/URL sources.
npm run schema:sync:paths -- \
  --management ../cks-management-api/schema.gql \
  --game ../cks-game-api/schema.gql
npm run codegen

Commit schema.gql and src/generated/graphql.ts together whenever the public GraphQL surface changes. Do not make npm run build depend on local API repos or network access; use npm run check:schema in CI/release work to detect drift.

Quick start

import {
  BrowserLocalStorageTokenStore,
  createCrowdyClient,
} from '@crowdedkingdoms/crowdyjs';

const client = createCrowdyClient({
  // Game API (world data + UDP proxy)
  httpUrl: 'https://game.example.com',
  wsUrl: 'wss://game.example.com',
  // Management API (passwordless sign-in, profile)
  managementUrl: 'https://management.example.com',
  tokenStore: new BrowserLocalStorageTokenStore(),
  realtime: {
    retryAttempts: 8,
    waitTimeoutMs: 5000,
  },
});

// Restore a previous session if there is one, otherwise sign in (passwordless).
await client.session.restore();
if (!client.session.getToken()) {
  // Magic link: email a one-time link, then complete with the token from it.
  await client.auth.requestLoginLink({ email: '[email protected]', redirectUri });
  await client.auth.completeLoginLink(tokenFromLink);
  // Or social/OIDC: socialLoginStart('google', redirectUri) -> socialLoginComplete({ provider, code, state })
  // Or dev/test only (server has DEV_AUTH_BYPASS): client.auth.devLogin('[email protected]')
}

// Passwordless sign-in returns an identity SESSION token (Management API only);
// the account is created on first sign-in. Identity reads run on it:
const me = await client.users.me();
console.log(me.email);

Gameplay needs an app-scoped token, not the session token. Mint one per app and drive the Game API world/UDP surface (including gameClientBootstrap) from a per-game client — see Overworld portals & app-scoped tokens (v7).

If managementUrl is omitted, the SDK falls back to httpUrl for backwards-compat with the single-endpoint deployment.

Sub-clients at a glance

Game-client surface (end-user, browser-safe):

| Sub-client | What it does | |---|---| | client.auth | Passwordless sign-in (magic link, social/OIDC, dev bypass), log out, and linked identities (myIdentities, linkIdentity/unlinkIdentity). | | client.users | me, updateGamertag, profile reads. | | client.session | Token store, restore(), getToken(), manual setToken(). | | client.serverStatus | gameClientBootstrap(appId) — per-app version info, UDP status, spatial limits. | | client.chunks, client.voxels, client.actors, client.avatars, client.state | World data reads + writes. | | client.host | Game-host election (get, amIHost) + actor liveness heartbeat. amIHost is UI convenience only — authoritative host gating uses gameModelInvoke's is_host policy. | | client.teleport | Teleport requests. | | client.channels, client.teams | Messaging channels and app-scoped player teams (membership + roles). | | client.gameModel | Abstract game model: containers, properties, functions (incl. model-driven notify_* effects), sessions, and automations / NPCs (upsertAutomation, runAutomation, automationRuns, automationStats, …). | | client.udp | UDP proxy subscriptions + spatial mutations (sendActorUpdate, sendVoxelUpdate, sendAudioPacket, sendTextPacket, sendClientEvent). | | client.realtime | Connection status, manual connect() / disconnect(), onStatus() listener. | | client.world(appId) | Higher-level helpers for browser games (actor.join, actor.sendState, actor.sendText). |

Studio-admin surface (privileged; drive with a server-side / studio token, grouped under client.admin):

| Sub-client | What it does | |---|---| | client.organizations | Orgs, members, RBAC roles, org API tokens. | | client.apps | App discovery + routing (createApp etc. via the management API directly). | | client.appAccess | Access tiers + per-user grants. | | client.billing | Org wallet + per-app spend budgets. | | client.payments | Payment checkouts (wallet top-ups, plan purchases). | | client.quotas | Usage quotas at the org/app scope. | | client.environments | Dedicated environments: quote, provision, scale, deploy, link apps. | | client.usage | Replication + GraphQL usage reporting. | | client.sharedEnvironment | Publish to shared, runtime gating, spend caps, auto-billing. | | client.gameApps | App grids (createGrid / deleteGrid) + grid runtime-permission administration. |

Operator surface (platform operations; requires is_operator):

| Sub-client | What it does | |---|---| | client.operator | Control plane: cross-org environments, change orders, secrets, release management, audit. |

Auth, user reads, and the studio-admin / operator surfaces target managementUrl and use the identity session token; the game-client world/UDP surfaces target httpUrl / wsUrl and require an app-scoped token for that app. Use one identity client plus a per-game client (see Overworld portals & app-scoped tokens (v7)); each client's AuthState carries its token to its own endpoints, so HTTP and WebSocket auth never drift within a client.

Game-loop lifecycle

  1. Sign in (passwordless) on the identity client with client.authrequestLoginLink/completeLoginLink (magic link), socialLoginStart/socialLoginComplete (social/OIDC), or devLogin (dev/test only) — or client.session.restore(). This yields the session token (Management API only).
  2. Mint an app-scoped token for the app (identity.portal.mintAppToken(appId), or the PKCE portal flow across origins) and build a per-game client holding it (game.setToken(token)). The gameplay steps below run on that game client.
  3. Subscribe to UDP proxy notifications with game.udp.subscribe(handlers, appId)appId is required (the SDK opens the realtime socket on demand and scopes it to that app).
  4. Join a chunk by sending an initial actor update.
  5. Send actor, voxel, text, audio, and client-event updates through game.udp or the higher-level game.world(appId) helpers.
  6. Call game.udp.disconnect() when leaving the world; game.portal.refresh() before the token expires to keep playing.
  7. Call client.close() (and game.close()) when disposing the SDK instances.

Per-app routing

When a player is about to join an app, query its routing fields on the management API first:

query AppForRouting($appId: BigInt!) {
  app(appId: $appId) {
    appId
    splitMode
    deploymentTarget
    gameApiUrl
  }
}

gameApiUrl is populated for both dedicated (splitMode) and shared (deploymentTarget: "shared") apps. When it's set, build a second CrowdyClient with httpUrl: gameApiUrl (and the matching wsUrl) holding that app's app-scoped token (identity.portal.mintAppToken(appId) — do not reuse the identity client's session token store), then drive gameplay through that client. In practice mintAppToken already returns gameApiUrl / gameApiWsUrl, so you rarely need this separate routing query. Apps with no gameApiUrl keep working against the default httpUrl you configured.

Realtime notifications

subscribe takes the handlers and a required appId (second argument). The Game API scopes the realtime session to that app and rejects an app-agnostic subscription with a RealtimeConnectionEvent (code: 'APP_ID_REQUIRED'). It also rejects an identity session token (APP_TOKEN_REQUIRED) or a token scoped to a different app (APP_SCOPE_MISMATCH). Run one client per app (each holding that app's app-scoped token) when a player is in multiple apps at once.

const appId = '1';

const unsubscribe = client.udp.subscribe(
  {
    actorUpdate: (event) => {
      console.log(event.uuid, event.state);
    },
    voxelUpdate: (event) => { /* ... */ },
    text: (event) => { /* ... */ },
    audio: (event) => { /* ... */ },
    clientEvent: (event) => { /* ... */ },
    serverEvent: (event) => { /* ... */ },
    singleActorMessage: (event) => {
      // A direct actor-to-actor message addressed to you.
      console.log(event.uuid, event.payload); // payload is base64
    },
    genericError: (event) => {
      console.warn(event.sequenceNumber, event.errorCode);
    },
    connectionEvent: (event) => {
      console.warn(event.code, event.message);
    },
    error: (error) => {
      console.error(error.code, error.message);
    },
  },
  appId,
);

// Or use the world helper, which passes its appId automatically:
//   client.world(appId).subscribe(handlers);

client.realtime.onStatus((status) => {
  console.log('realtime:', status);
});

// Later:
unsubscribe();

The SDK uses the graphql-transport-ws protocol through graphql-ws, reconnects with backoff, re-reads the current token before reconnecting, and resubscribes automatically.

Spatial sends

const response = await client.udp.sendActorUpdateAndWait({
  appId: '1',
  chunk: { x: '0', y: '0', z: '0' },
  uuid: '0123456789abcdef0123456789abcdef',
  state: 'AA==',           // base64-encoded payload
  distance: 8,
  decayRate: 1,
});

console.log(response.__typename, response.sequenceNumber);

The plain sendActorUpdate, sendVoxelUpdate, sendAudioPacket, sendTextPacket, and sendClientEvent methods return the GraphQL mutation result immediately. The AndWait variants allocate a sequenceNumber when one is missing and wait for either a matching notification or GenericErrorResponse.

Actor-to-actor messages

// Delivered only to the actor whose UUID matches `targetUuid`; you must know
// that actor's current chunk. Fire-and-forget — the sender gets no echo, so
// there is no `AndWait` variant. The target receives a
// `SingleActorMessageNotification` on its subscription.
await client.udp.sendSingleActorMessage({
  appId: '1',
  chunk: { x: '7', y: '1', z: '2' }, // the TARGET actor's chunk
  targetUuid: '0123456789abcdef0123456789abcdef',
  payload: 'aGVsbG8=', // base64; embed sender identity here if you need it
});

World helpers

const world = client.world('1');
const actor = world.actor();

await actor.join({ x: '0', y: '0', z: '0' });
await actor.sendState('AA==');
await actor.sendText('hello nearby players');

// Direct message to one other actor (you supply its UUID + current chunk):
await actor.sendToActor(
  '0123456789abcdef0123456789abcdef',
  'aGVsbG8=', // base64 payload
  { x: '7', y: '1', z: '2' },
);

The world helpers are thin wrappers over client.udp.* with the appId pre-bound — convenient for browser games. Advanced callers can always use client.udp.* with the generated GraphQL input types directly.

Errors

Transport and protocol failures throw structured error classes:

  • CrowdyHttpError — non-2xx response from a GraphQL endpoint.
  • CrowdyGraphQLError — preserves every GraphQL error including path and extensions.code.
  • CrowdyNetworkError — network-level failure (DNS, TLS, connection refused).
  • CrowdyTimeoutError — request or AndWait timed out.
  • CrowdyRealtimeError — realtime subscription couldn't be established or was dropped.
  • CrowdyProtocolError — server response failed schema validation.

GraphQL errors carry a stable extensions.code (e.g. UNAUTHENTICATED, SCOPE_MISSING, FORBIDDEN, IDEMPOTENCY_CONFLICT) plus, where applicable, extensions.remediation and extensions.requiredPermission. Branch on error.extensions?.code rather than parsing messages.

Idempotent retries

Destructive game-client mutations accept an optional idempotency key. Pass a stable key (e.g. crypto.randomUUID()) and a network retry replays the first result instead of applying the side effect twice. Reusing a key with different arguments throws a CrowdyGraphQLError with extensions.code === 'IDEMPOTENCY_CONFLICT'. Keys expire server-side after 24h.

const key = crypto.randomUUID();
await client.actors.delete(uuid, key);          // first call deletes
await client.actors.delete(uuid, key);          // retry → replays the first result
await client.teams.remove(groupId, key);        // deleteTeam
await client.teams.leave(groupId, key);         // leaveTeam
await client.voxels.rollback({ ...input, idempotencyKey: key }); // input field

The key parameter is optional and trailing, so it's safe to omit. Requires a server on release v0.1.19+ (see Server compatibility above).

Auth notes

  • Use client.auth.setToken(token) if you need to seed a token externally (e.g. when restoring auth from a non-default storage).
  • client.session.restore() reads from the configured tokenStore. BrowserLocalStorageTokenStore is provided; bring your own for SSR or Node usage.
  • Each client's AuthState is observed by both its HTTP client and its realtime socket, so HTTP and WebSocket auth never drift within a client. Hold the identity session token on the management/identity client and an app-scoped token on each per-game client (client.portal — see the v7 section).

Overworld portals & app-scoped tokens (v7)

As of v7 gameplay requires an app-scoped token, not the session token. Passwordless sign-in returns an identity session token (Management API only — account, studio admin, and minting); each game is entered with a short-lived token confined to that one app, so a game stack never receives the player's full session.

Use two clients: an Overworld/identity client (session token) and a per-game client (app token), sharing only the Management URL.

// Overworld/identity client
const overworld = createCrowdyClient({ managementUrl, tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session') });
// Passwordless sign-in (magic link, social/OIDC, or dev bypass) yields the session token.
await overworld.auth.requestLoginLink({ email, redirectUri });
await overworld.auth.completeLoginLink(tokenFromLink);

// Native / same-origin: mint directly, then build a game client.
const t = await overworld.portal.mintAppToken(appId);
const game = createCrowdyClient({ httpUrl: t.gameApiUrl!, wsUrl: t.gameApiWsUrl!, managementUrl,
  tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:' + appId) });
game.setToken(t.token);
game.world(appId).subscribe({ actorUpdate: (n) => { /* ... */ } });

Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the verifier never leaves the game origin:

// Game origin, on "enter": redirect to the Overworld authorize page.
location.assign(await game.portal.beginEntry({
  appId, authorizeUrl: 'https://overworld.example.com/authorize',
  redirectUri: location.origin + location.pathname,
}));

// Overworld /authorize page (holds the session token):
location.assign(await overworld.portal.handleAuthorizeRequest());

// Game origin, on callback boot: exchange code+verifier -> app token (stored).
const entered = await game.portal.completeEntry();
// Keep playing past expiry without re-portaling (same app):
await game.portal.refresh();

Game-to-game routes through the Overworld for a fresh per-game token. New realtime codes: APP_TOKEN_REQUIRED, APP_SCOPE_MISMATCH; new UdpErrorCode: TOKEN_EXPIRED. See MIGRATION.md for the full v7 breaking guide.

Surface scope & security

As of v6 (completed in v6.1), CrowdyJS wraps the full management-api + game-api public surface, not just the game-client subset — every non-deprecated public root field has a typed method, with Relay *Connection cursor-pagination variants alongside the legacy offset lists. The surfaces are namespaced by audience:

  • Game-client (client.auth, client.users, client.udp, client.world(...), client.chunks/voxels/actors/avatars/state/teleport/channels/teams/ gameModel/host) — safe for untrusted browser clients with an end-user token.
  • Studio-admin (client.admin.* — also reachable at the top level, e.g. client.billing) — privileged organization/app administration. Drive these from a studio backend with an org-scoped or admin token, not from an untrusted browser; the server still enforces the relevant org/app permission on every call.
  • Operator (client.operator) — platform control-plane operations that require users.is_operator. For internal operator tooling only.

The SDK never relaxes server-side authorization — exposing an operation here just gives you a typed wrapper; the caller still needs the right token and permission. For any brand-new server field not yet wrapped, the low-level escape hatch (client.graphql.request(...) / client.management.request(...)) always works.

Low-level GraphQL access

Game-client methods are first-class, but generated operation documents are also available through a transport escape hatch:

import { VersionInfoDocument } from '@crowdedkingdoms/crowdyjs/generated';

const data = await client.graphql.request(VersionInfoDocument);

Most consumers should prefer the typed methods on client.auth, client.users, client.udp, client.serverStatus, and client.world().

Migration

See MIGRATION.md for breaking changes between SDK majors.

License

MIT