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

crux-sdk

v0.6.0

Published

Supercraft Crux Game Services Backend SDK - player auth, cloud-save documents, leaderboards, economy, matchmaking, server registry, and config delivery.

Readme

Supercraft Crux - JavaScript / TypeScript SDK

Fetch-based SDK for Crux (Game Services Backend). Works in Node.js 18+, browsers, and any runtime with a native fetch API. The JavaScript quickstart walks through the same first call.

Installation

npm install crux-sdk

Create a free project and open the Crux JavaScript quickstart.

Works from both ESM (import) and CommonJS (require), Node 18+.

Getting your credentials

Create a project in the Crux dashboard, then copy:

  • Project ID and Environment ID - UUIDs shown on the Projects and Environments pages.

  • Publishable key - safe to ship inside a game client. It authenticates players (/v1/auth/*) and does nothing else: it cannot change project configuration, and it cannot address a player directly. After login, the SDK uses the player's own token.

  • Secret key - the same shape, server-side only. Use it for automation, CI, and curl. Never put it in a build you distribute.

    Both are created with the project and shown on the Credentials page. Until 2026-08-12 there was only one kind, and this file said it was safe to embed while it also reached the whole project control plane - anyone who ran strings on a released build could ban players, delete credentials, and rewrite live config. Used legacy keys are marked for rotation and now have publishable-level authority only. If you shipped one, treat it as exposed: rotate it immediately and replace the client copy with a publishable key.

  • Server token - for trusted backends and dedicated servers. Keep it server-side; never ship it in a client build.

  • Leaderboard ID or key - either the UUID of a leaderboard you created on the Leaderboards page, or the key you gave it (unique per environment). Both work on runtime leaderboard calls.

Quick start - game client (browser / web game)

import { CruxClient } from "crux-sdk";

const crux = CruxClient.forPlayer(
  "https://crux.supercraft.host",
  "<PROJECT_ID>",      // UUID
  "<ENVIRONMENT_ID>",  // UUID
  "<PUBLISHABLE_KEY>", // safe to ship in a player build
);

// Both ids are checked here: a missing or mistyped one throws a CruxError
// naming the value, rather than 404ing on every call you make later.

// Log in (guest)
const auth = await crux.loginAnonymous();
console.log("Player:", auth.player_id);

// Save data. "" as the player id means the player who just logged in, so the
// id never has to be threaded through your own code.
await crux.setPlayerDocument("", "settings", { volume: 0.8 });

// Submit a score. The first argument is the leaderboard's key (or its UUID).
const LEADERBOARD_ID = "<LEADERBOARD_UUID>";
await crux.submitScore(LEADERBOARD_ID, "", 9900);

// Get leaderboard
const top = await crux.getTop(LEADERBOARD_ID, 10);
top.forEach(e => console.log(`#${e.rank} ${e.player_id}: ${e.score}`));

Quick start - server (Node.js backend / game server)

import { CruxClient } from "crux-sdk";

const crux = CruxClient.forServer(
  "https://crux.supercraft.host",
  "<PROJECT_ID>",      // UUID
  "<ENVIRONMENT_ID>",  // UUID
  "<SERVER_TOKEN>",    // secret from the Credentials page - keep server-side
);

// Admit a player into a realtime server using the access token the player
// received from login. Crux validates expiry/revocation/bans online and returns
// the trusted identity; the SDK also rejects tokens from another project.
const identity = await crux.verifyPlayerToken(playerAccessToken);
console.log(identity.player_id);

// Register
const server = await crux.registerServer({
  server_id:    "node-01",
  name:         "Deathmatch EU #1",
  region:       "eu-west",
  map_name:     "forest",
  game_mode:    "deathmatch",
  player_count:   0,
  ip_address:     "198.51.100.1",
  port:           7777,
  server_version: "1.0.0",
});
// Those are all the fields the registry stores. There is no capacity field -
// keep max players in a project document if your browser needs it.

// Heartbeat (call every 30 s)
setInterval(() => crux.heartbeat(server.server_id), 30_000);

// Clean deregister on shutdown
process.on("SIGTERM", async () => {
  await crux.deregisterServer(server.server_id);
  process.exit(0);
});

API reference

Every playerId below may be "", which means the player from the last login. Pass one explicitly to act for a different player, which is what a dedicated server does. With neither, the call throws a CruxError saying what is missing and makes no request.

Static factories

| Call | Description | |------|-------------| | CruxClient.forServer(url, projectId, envId, serverToken) | Server-side mode. Throws CruxError on a missing or malformed id or token | | CruxClient.forPlayer(url, projectId, envId, apiKey) | Client-side mode. Throws CruxError on a missing or malformed id or key | | CruxClient.forApiKey(url, projectId, envId, secretApiKey) | Trusted backend/automation mode with a secret API key; works on trusted runtime and project/KV routes. Never ship this credential in a player build |

Auth

| Method | Description | |--------|-------------| | loginAnonymous(anonymousId?) | Guest login. The device id is persisted in localStorage and reused, so the player returns to the same account - and the same saves - on the next launch. Pass your own id if your game already has a stable device identifier. | | CruxClient.clearAnonymousId() | Forget the stored guest id ("reset progress"). | | loginEmail(email, password) | Email login | | registerEmail(email, password) | Email registration | | loginSteam(ticketHex) | Verify a short-lived Steam GetAuthTicketForWebApi ticket through Crux and start/recover the linked player session. The Steam publisher key stays server-side in the Crux project config. | | linkSteam(ticketHex) | Link Steam to the currently logged-in Crux player. Requires a player session; use this after guest/email onboarding to avoid creating a second account. | | refreshAccessToken() | Refresh access token | | verifyPlayerToken(token) | Server-mode only: validate a player access token online via /v1/auth/me and recover its trusted project/player identity. | | logout() | Revoke the session |

Player Documents

| Method | Description | |--------|-------------| | getPlayerDocument<T>(playerId, key) | Get a typed document, or null when the key is missing | | setPlayerDocument<T>(playerId, key, value, version?) | Write a document | | deletePlayerDocument(playerId, key) | Delete a document | | batchGetPlayerDocuments<T>(playerId, keys[]) | Fetch multiple keys | | patchPlayerDocument<T>(playerId, key, operations[], version?) | Apply JSON-patch style operations to one document | | batchWritePlayerDocuments<T>(playerId, writes[]) | Write multiple keys atomically and return the written documents with authoritative versions |

Shared Spaces

Persistent client-safe state for a known set of players: parties, guilds, co-op saves, private rooms, and similar group state. A player-created space is owned by that player; a server-created space remains server-managed. Existing player/project document semantics are unchanged.

| Method | Description | |--------|-------------| | createSharedSpace(metadata?) | Create a player-owned or server-owned space, depending on client mode | | listSharedSpaces() | Player memberships, or all environment spaces in server mode | | getSharedSpace(spaceId) | Get one visible space, or null | | deleteSharedSpace(spaceId) | Delete an owned/server-managed space | | listSharedSpaceMembers(spaceId) | List members | | setSharedSpaceMember(spaceId, playerId, canWrite?) | Add/update membership | | removeSharedSpaceMember(spaceId, playerId) | Remove a member or leave yourself | | listSharedDocumentKeys(spaceId) | List shared document metadata | | getSharedDocument<T>(spaceId, key) | Read shared state | | setSharedDocument<T>(spaceId, key, value, version?) | Versioned write | | patchSharedDocument<T>(spaceId, key, operations, version?) | Patch shared state | | deleteSharedDocument(spaceId, key) | Delete shared state | | waitSharedDocument<T>(spaceId, key, afterVersion, timeoutSeconds?) | Long-poll; returns the monotonic cursor even after deletion |

Project Documents

Shared by every player in the environment - server capacity, event flags, drop tables, seasonal switches. Reading needs any authenticated caller; writing needs a secret API key or a server token, so a game client cannot rewrite the rules it is handed.

| Method | Description | |--------|-------------| | listProjectDocumentKeys() | Keys with size and version, no bodies | | getProjectDocument<T>(key) | Get a shared document, or null when the key is missing | | setProjectDocument<T>(key, value, version?) | Write one | | deleteProjectDocument(key) | Delete one | | batchGetProjectDocuments<T>(keys[]) | Fetch multiple keys | | batchWriteProjectDocuments<T>(writes[]) | Write multiple keys atomically |

If you think in KV rather than documents, the SDK exposes exact aliases over the same storage: kvListKeys(), kvGet(key), kvSet(key, value, version?), kvDelete(key), kvMGet(keys) and kvMSet(writes). There is no duplicate store or migration step.

Stats

Named 64-bit counters the backend owns, so progression is queryable instead of buried in a save blob. Writing a stat settles every achievement bound to it in the same transaction - the write response tells you what was earned, exactly once, so a reward can never pay twice.

| Method | Description | |--------|-------------| | listPlayerStats(playerId) | Every stat for a player | | getPlayerStat(playerId, key) | One stat | | setPlayerStat(playerId, key, value) | Set to an absolute value (server token) | | incrementPlayerStat(playerId, key, delta) | Add a delta, negative to subtract (server token) |

Both writes return { stat, unlocked }, where unlocked is the achievements that crossed their threshold on this call.

Stat writes and direct unlocks require a server token. Reading takes any runtime credential, but a player token gets 403 server token required on a write - progression is server-authoritative, so a modified client cannot award itself anything. Call these from your dedicated server or a trusted backend, not from the game client.

Achievements

| Method | Description | |--------|-------------| | listAchievements() | The catalogue defined for this environment | | listPlayerAchievements(playerId) | What this player has unlocked | | unlockAchievement(playerId, key) | Grant one directly (server token) |

unlockAchievement is idempotent: unlocked is true on the call that actually granted it and false on every repeat, so it is safe to call from a retrying client.

Leaderboards

| Method | Description | |--------|-------------| | submitScore(leaderboardId, playerId, score, metadata?) | Submit a score | | getTop(leaderboardId, limit) | Top N entries | | getPlayerStanding(leaderboardId, playerId) | Player's rank + score (null if unranked) | | getAroundPlayer(leaderboardId, playerId, radius) | Neighbours in the ranking |

leaderboardId accepts either the leaderboard's UUID (returned by create / shown in the dashboard) or its key, e.g. "weekly". A UUID-shaped value is resolved as an id first.

Economy

| Method | Description | |--------|-------------| | getPlayerEconomy(playerId) | Balances + inventory | | adjustEconomy(playerId, balances?, inventory?) | Atomic credit/deduct (server token) |

Social

| Method | Description | |--------|-------------| | listFriends(playerId) | Accepted friendships | | listPendingFriendRequests(playerId) | Incoming requests | | sendFriendRequest(playerId, friendId) | Send a request | | acceptFriendRequest(playerId, friendId) | Accept one | | removeFriend(playerId, friendId) | Remove a friendship | | blockPlayer(playerId, targetId) | Block | | unblockPlayer(playerId, targetId) | Unblock |

Matchmaking

| Method | Description | |--------|-------------| | joinMatchmaking(playerId, gameMode, region) | Join a queue | | getMatchmakingStatus() | Poll for result | | leaveMatchmaking(playerId) | Leave the queue | | startMatch(matchId) | Mark a formed match as in-progress (server token) | | completeMatch(matchId) | Mark a match finished, releasing its slots (server token) |

A formed match carries the live server it was placed on, drawn from your own registry - matchmaking only picks a server that is currently heartbeating, so a crashed host is never handed out as a destination. Call startMatch when the session begins and completeMatch when it ends; a match nobody completes is swept to expired rather than pinning that server's capacity forever.

Server Registry (server token required)

| Method | Description | |--------|-------------| | registerServer(registration) | Register this instance | | heartbeat(serverId) | Send a keepalive | | deregisterServer(serverId) | Remove on shutdown | | listServers({region?, mapName?, gameMode?}) | Browse servers |

Config

| Method | Description | |--------|-------------| | downloadActiveConfigBundle() | Returns Promise<ArrayBuffer> |

Error handling

Missing player and project documents resolve to null. Other HTTP errors throw CruxError:

import { CruxClient, CruxError } from "crux-sdk";

try {
  const doc = await crux.getPlayerDocument(playerId, "inventory");
  if (!doc) {
    console.log("First run: use the default inventory");
  }
} catch (e) {
  if (e instanceof CruxError) {
    console.error(`HTTP ${e.statusCode}:`, e.message);
  }
}