@crownfall/sdk
v0.1.0
Published
TypeScript SDK for authoring Crownfall server plugins. Types and stubs only; the runtime is provided by crownfall-pluginhost.
Maintainers
Readme
@crownfall/sdk
The TypeScript SDK for authoring Crownfall server-side plugins.
Pre-alpha. Version
0.1.0(first npm-published release; types- stubs-only contract still applies). The real implementation lives incrownfall-pluginhost, which is not yet built. You can install the SDK and get full IDE support today; you cannot yet run a plugin against a real server.
What's in the box
- Strict-typed event map for every engine event (
player.connect,combat.hit,tick, ...). - Typed stubs for the database, state-bag, chat, RPC, entity, player, config, and logger APIs.
- A
CrownfallErrorhierarchy (RuntimeUnavailableError,PermissionDeniedError,InvalidArgumentError). - A mockable runtime contract (
__CROWNFALL_RUNTIME__) for local testing.
The SDK has no runtime dependencies. It is shipped as ESM, tested
with Vitest, and lint-clean under strict TypeScript rules
(strict, noUncheckedIndexedAccess).
Install
npm install @crownfall/sdkRequires Node.js >=20 (the host bundles its own runtime in production;
the Node requirement is for tooling: tsc, vitest, your test
harness).
Hello, world
A minimal plugin is two files: a manifest and an entrypoint.
crownfall.toml:
[plugin]
name = "hello-world"
version = "0.1.0"
authors = ["You <[email protected]>"]
description = "Greets new arrivals."
license = "MIT"
sdk_version = "1"
[server]
entrypoint = "server/main.ts"
permissions = [
"event:subscribe:player.connect",
"chat:broadcast",
]server/main.ts:
import { Crownfall } from "@crownfall/sdk";
Crownfall.events.on("player.connect", async (e) => {
await Crownfall.chat.global(
`Welcome to Crownfall, ${e.characterName}!`,
);
});That is the entire plugin. The complete worked example lives in
examples/hello-world/.
API surface (v1)
Every API forwards to the host via an op. Without a host bound to
globalThis.__CROWNFALL_RUNTIME__, every call throws
RuntimeUnavailableError — but the types still resolve cleanly, so
your IDE works.
| Module | Purpose |
|---|---|
| events | on(event, handler), off(handle), emit(event, payload), typed event map |
| db | characters, inventories, transactions, pluginState query builders |
| state | Replicated per-entity key/value bags with CRDT semantics |
| chat | send(playerId, msg), broadcast(msg, scope) |
| rpc | expose(name, handler), call(plugin, method, args) |
| entity | Query/mutate entities (byId, queryNearby, spawn) |
| player | Player-specific accessors (byId, online, kick) |
| config | Operator-set configuration (get, getOr, has) |
| logger | Structured logger (info, warn, error, child) |
For the full specification, see
docs/plugin-system/.
Two import styles
Both of the following are valid; use whichever you prefer.
// Named imports — friendly to tree-shaking.
import { events, chat, db } from "@crownfall/sdk";
events.on("player.connect", (e) => chat.send(e.playerId, "Hi!"));// Namespace import — mirrors the docs.
import { Crownfall } from "@crownfall/sdk";
Crownfall.events.on("player.connect", (e) =>
Crownfall.chat.send(e.playerId, "Hi!"),
);Testing your plugin locally
Until crownfall-pluginhost exists, you can drive your plugin under
Vitest by installing a mock runtime:
import type { CrownfallRuntime } from "@crownfall/sdk";
const calls: Array<[string, unknown[]]> = [];
const mock: CrownfallRuntime = {
callOp: (op, args) => { calls.push([op, [...args]]); return Promise.resolve(); },
callOpSync: (op, args) => { calls.push([op, [...args]]); return undefined; },
pluginName: "my-plugin",
sdkVersion: "0.0.1",
};
(globalThis as Record<string, unknown>).__CROWNFALL_RUNTIME__ = mock;
// import your plugin
// drive events
// assert on `calls`The SDK ships its own test suite using exactly this pattern; see
src/__tests__/sdk.test.ts.
Scripts
| Command | What it does |
|---|---|
| npm run build | Compile src/ to dist/ via tsc |
| npm run test | Run Vitest |
| npm run lint | Run ESLint |
| npm run clean | Remove dist/ and tsconfig.tsbuildinfo |
Versioning
Pre-1.0 the SDK ships from main. The sdk_version field in
crownfall.toml tracks the major API version; minor and patch
bumps within a major are backwards-compatible. See
ROADMAP.md for the path to 1.0.
Pre-publish checklist (operator)
Phase 2 closes the SDK as code-complete; the only remaining
gate before npm publish is operator-side metadata + auth. The
package.json already carries publishConfig.access: "public"
and a prepublishOnly hook that runs clean + build + test +
lint, so the publish fails closed on any breakage.
Before running npm publish:
Fix the repository URL.
package.jsonships with the placeholderhttps://github.com/example/crownfall.git. When the repo lives somewhere public, updaterepository.url,homepage, andbugs.urlto match. Without this, the npmjs.com listing's "Repository" link goes nowhere — the package publishes fine, just looks half-finished.Reserve the
@crownfallscope on npm. From your npm account → "Create Organization" →crownfall. Free tier is fine for public scoped packages. Skip if you already own it.Verify the name is unclaimed.
npm view @crownfall/sdkshould return404; if it returns metadata, someone else has published under that name already.Log in + publish. From this directory:
npm login # opens browser for 2FA npm publish --access publicThe
--access publicflag is belt-and-suspenders alongside thepublishConfigin package.json. TheprepublishOnlyscript will block the publish ifclean / build / test / lintdoesn't pass.Bump version for subsequent publishes. Either edit
package.jsonmanually or usenpm version patch | minor | majorto bump + tag in one command.
License
MIT. See LICENSE.
