Docs/Codex/JS SDK

JS SDK

@katforge/api is the JavaScript / TypeScript client for the codex HTTP API. Browser-, Node-, and Capacitor-friendly.

Last reviewed May 6, 2026

The codex has two distinct backends. YAML manifests are hand-authored definitions shipped as static JSON, used for games whose data is small and declarative (lextris, gear-goblins, katforge). DB-backed endpoints at /v1/codex/{game}/{id} are populated by the import pipeline and used for games with thousands of auto-extracted entities (dark-and-darker, wc3, mechabellum). The current JS SDK ships the manifest surface in full; entity / diff accessors over the DB-backed surface are described in Roadmap and ship next.

Install

shell
bun add @katforge/api

Construct

TypeScript
import { KATforge } from '@katforge/api';

export const katforge = new KATforge ({
   baseUrl: 'https://api.katforge.com',
   games:   [ 'lextris' ]   // games this client will read manifests for
});

The codex namespace is exposed as katforge.codex. Auth and other modules sit alongside it on the same instance.

Bind to a game

The recommended shape: bind a game once, then call methods on the bound accessor. Same surface as the root, but every method is scoped to one game and locale.

TypeScript
const lex = katforge.codex.game ('lextris');

await lex.preload ();              // GET /v1/codex/lextris/manifest.json
lex.isLoaded ();                   // true
lex.has ('id.award.first_word');   // true
lex.get ('id.award.first_word');   // Definition (generic)

Most callers stick to this form — it's the cleanest read path inside a single-game app (e.g. lextris.com).

preload(locale?)

Fetches and caches the manifest for this game. Returns the parsed manifest. Subsequent reads are synchronous.

TypeScript
const manifest = await lex.preload ();
manifest.hash;                     // server's content hash — matches ETag
manifest.definitions;              // { [id: string]: Definition }

isLoaded(locale?)

True after preload has resolved. Useful to gate UI on cache state.

TypeScript
if (! lex.isLoaded ()) {
   await lex.preload ();
}

invalidate()

Force a refetch on the next read. Drops the in-memory cache for this game.

TypeScript
lex.invalidate ();
await lex.preload ();              // fetches fresh

get(id) / has(id)

Generic lookup over every type. Throws if the manifest hasn't been preloaded yet — preload is mandatory before sync reads.

TypeScript
const def = lex.get ('id.award.first_word');
def?.type;                         // 'award'
def?.data;                         // Record<string, unknown>

lex.has ('id.award.unknown');      // false

awards.list() / awards.get(id) / awards.has(id)

Typed accessor for type === 'award' entries. list() returns every award in stable order; get returns null on type mismatch (so passing a level id silently returns null instead of throwing).

TypeScript
const all = lex.awards.list ();
all.map (a => `${ a.data.name }${ a.data.ember_value } embers`);

const award = lex.awards.get ('id.award.first_word');
award?.data.ember_value;           // 25

lex.awards.has ('id.award.first_word');   // true
lex.awards.has ('id.level.tier_3');       // false (it's a level, not an award)

levels.list() / levels.get(id) / levels.has(id)

Same shape, scoped to type === 'level'. list() is sorted by ord (1-indexed ladder position) ascending — the natural display order for tier ladders.

TypeScript
const tiers = lex.levels.list ();
tiers.map (t => `${ t.data.ord }. ${ t.data.name }  (${ t.data.threshold_lifetime })`);

const tier = lex.levels.get ('id.level.tier_3');
tier?.data.threshold_lifetime;     // 500

Root accessor

The root form takes game as a first arg. Use it in code that touches multiple games (admin tooling, cross-game leaderboards), or for one-off reads where binding feels heavy.

TypeScript
await katforge.codex.preload ('lextris');

const award = katforge.codex.awards.get ('lextris', 'id.award.first_word');
const tiers = katforge.codex.levels.list ('lextris');

// Drop a single game's cache, or every game when no arg:
katforge.codex.invalidate ('lextris');
katforge.codex.invalidate ();

Asset URLs

The manifest exposes Award.data.icon and Level.data.icon as plain string references. Loading the actual bytes goes through the DB-backed asset endpoint:

TypeScript
const baseUrl = 'https://api.katforge.com';
const award   = lex.awards.get ('id.award.first_word');
const iconUrl = award?.data.icon
   ? `${ baseUrl }${ award.data.icon }`   // assumes the YAML stores an origin-relative URL
   : null;

For the full HTML / <picture> / CDN-hint patterns, see Assets.

Types

These are reference-grade type definitions — same shape across the runtime SDK and the YAML schema. Imported alongside the module:

TypeScript
import type { Definition, Award, Level, CodexManifest } from '@katforge/api';

Definition

Generic codex entry. Specialized types (Award, Level) narrow data further.

FieldTypeNotes
idstringCanonical id, e.g. id.award.first_word.
gamestringGame slug.
typestring'award', 'level', etc.
dataRecord<string, unknown>Per-type projection.

Award

Definition with type === 'award'. name and description are localized strings resolved from the YAML's sibling keys.

FieldTypeNotes
data.idstringSame as the outer id.
data.namestringDisplay name (locale-resolved).
data.descriptionstringDisplay text (locale-resolved).
data.ember_valuenumberEmbers awarded on grant.
data.repeatablebooleanWhether the award can fire multiple times per player.
data.categorystring?Optional grouping label.
data.iconstring?Optional icon reference.

Level

Definition with type === 'level'. ord is 1-indexed and stable across the ladder.

FieldTypeNotes
data.idstringSame as the outer id.
data.namestringTier name.
data.ordnumber1-indexed ladder position.
data.threshold_lifetimenumberLifetime-Embers requirement to reach this tier.
data.iconstring?Optional icon reference.
data.perksRecord<string, unknown>?Optional per-tier perk payload.

LevelService on the server resolves a player's current tier as the highest one whose threshold does not exceed the player's lifetime balance.

CodexManifest

Returned by preload(). The hash matches the response ETag, so clients can pass it as If-None-Match on a re-fetch and skip parsing when the manifest is unchanged.

FieldTypeNotes
gamestringGame slug.
localestringResolved locale (defaults to 'en').
hashstringServer's content hash.
definitionsRecord<string, Definition>Keyed by canonical id.

Roadmap

The DB-backed entity / diff surface is implemented in PHP today (see PHP SDK) and shipping next on the JS side. The shape will mirror the PHP API:

TypeScript
// COMING NEXT — not yet implemented in @katforge/api.
const dnd   = katforge.codex.game ('dark-and-darker');
const sword = await dnd.entity ('id.item.longsword_5001');
sword.name;                       // "Longsword"
sword.data.gear_score;            // 50
sword.icon.url;                   // "/v1/codex/assets/22cd8c50…"

const diff = await dnd.diff ({ from: '0.16.135.8645', to: '0.16.137.8750' });
diff.summary;                     // { added: 12, changed: 184, removed: 3 }

Until that lands, fetch the DB-backed endpoints directly:

TypeScript
const baseUrl = 'https://api.katforge.com';
const sword   = await fetch (`${ baseUrl }/v1/codex/dark-and-darker/id.item.longsword_5001`).then (r => r.json ());
sword.icon.url;                   // "/v1/codex/assets/22cd8c50…"