00 — Quickstart
Install the WorldForge SDK, mint an API key, and read a world in three steps.
The SDK (@blueforge-studio/worldforge-sdk) is TypeScript-first but plain
JavaScript everywhere: it is an ESM package built on fetch, with no
framework, React or DOM requirement. Four entrypoints cover REST (.), offline
world tools (/offline), realtime (/realtime: SSE + realm WebSocket) and
MCP (/mcp). The package is ESM-only (exports has import conditions, no
require).
Install
The package is published to the BlueForge registry (packr):
# ~/.npmrc (or the project .npmrc)
@blueforge-studio:registry=https://api.packr.blueforge.studio
pnpm add @blueforge-studio/worldforge-sdk
# or: npm install @blueforge-studio/worldforge-sdk / yarn add …
Anonymous reads are allowed — no auth token is needed just to install.
Mint an API key
API keys are created through the API with a user session (a browser
session JWT — an API key cannot mint or revoke keys, by design). The SDK
has no wrapper for /api-keys; call it with curl or client.raw:
curl -X POST https://api.worldforge.blueforge.studio/api-keys \
-H "authorization: Bearer $USER_JWT" \
-H 'content-type: application/json' \
-d '{"name": "my-integration", "scopes": ["world:read", "world:write"]}'
# → 201 { "id": …, "key": "wk_live_….…", "expiresAt": "…" }
Keys are wk_live_<id>.<secret>, hashed at rest, scoped (see
01 — Authentication) and expire (90 days by default,
365 max; pass an ISO expires_at in the body to choose). Scopes must come
from the catalogue — * is refused with 400 invalid_scopes. Store the
secret immediately — only the hash is kept, and the response does not echo
the scopes back.
First request
import { createClient } from '@blueforge-studio/worldforge-sdk';
const client = createClient({
baseUrl: 'https://api.worldforge.blueforge.studio',
apiKey: process.env.WORLDFORGE_API_KEY!,
});
const worlds = await client.worlds.list(); // WorldSummary[]
const world = await client.worlds.get(worlds[0]!.id); // WorldDetail (has `json`)
// The canonical world document — migrate, then validate, offline.
// Migrate FIRST: the schema pins `world.schemaVersion` to the current
// version, so an older document fails `safeParseWorld` until it is migrated.
import { safeParseWorld, runMigrations, type World } from '@blueforge-studio/worldforge-sdk/offline';
const current = runMigrations(world.json as World); // old schema → WORLD_SCHEMA_VERSION
const parsed = safeParseWorld(current);
if (!parsed.ok) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('; '));
const doc = parsed.data;
Errors
Non-2xx responses throw WorldForgeApiError, which preserves the API's
{ error, reason } envelope:
import { WorldForgeApiError } from '@blueforge-studio/worldforge-sdk';
try {
await client.worlds.patch(id, { confirm: true });
} catch (err) {
if (err instanceof WorldForgeApiError) {
console.error(err.status, err.code, err.reason, err.requestId);
// e.g. 403 'forbidden' + reason, or 400 'commit_required'
}
}
WorldForgeApiError carries status, body (the parsed JSON, or the raw
text / null when the body was not JSON), code (body.error), reason
(body.reason) and requestId (the x-request-id response header, when the
server sent one). A misconfigured client (no auth option) throws
WorldForgeConfigError at createClient time instead.
Next
- 01 — Authentication — keys vs JWTs, scopes, limits
- 02 — Worlds and resources — the typed surface
- 03 — Offline world tools — validate, migrate, lore, calendar