Skip to main content

Builds and starter packs

You can build your hubs and spokes on the platform instead of on your machine: send your crates' sources to execBuild, poll execBuildStatus, and deploy the build with execDeploy. You need no Rust toolchain. The starter packs, which replace the compute templates, come the same way.

execStarters and execBuild need the organization's manage_compute permission; execBuildStatus needs view_compute_diagnostics. None of them take an app token: use your own session.

Starter packs​

execStarters(appId) returns fifteen crates and a manifest that deploys them as one app, seventeen node types (racing and territory each serve two):

CrateNode typeWhat it doesReplaces
world-tickworld, the root hubThe world's clock (day and night), weather, resource nodes that empty and grow back, and liveops events your developers schedule. It publishes the conditions on its world topic.world-engine, liveops-scheduler
matchmakermatchmakerA rating queue for each key (ranked, casual). The rating window widens the longer a player waits. Every player in a proposed match must accept, and then the queue reserves a session for them.matchmaking
sessionsessionOne game from lobby to result: a host, ready checks, turns with a time limit, points the hub decides, and a winner.match-engine
npc-mobsnpcsOne area's mobs and NPCs, keyed x,z, moved on a timer and shown to nearby players as realtime actors. Mobs chase, go home past their leash, take hits the hub referees, and respawn. NPCs walk routes and talk.mob-engine, npc-engine
leaderboardleaderboardA board other hubs submit to, ranked by the hub, with seasons on a schedule.board-engine
deckdeckA card table with true hidden information: hands only their player reads.deck-engine
tacticstacticsA grid battle in phases against the hub's units.tms-battle
directordirectorWaves down a lane, towers that may not cut it, a boss with phases.director
minigameminigameA duel of hidden throws, revealed together.minigame
instanceinstanceA party's private run: seeded floors, permadeath, expiry, depth to a leaderboard.instance-engine
arenaarenaClasses and abilities in real time, with a movement warden for reported positions.abilities-engine, movement-warden
racingrace, ballRaces (gates, laps, splits, a ghost) and ball games (possession, goals).racing, possession
territoryterritory, zoneControl points captured by standing on them, and a shrinking circle.territory
playerplayerA player's own hub, keyed by their id: inventory, soft currency, crafting, loot with pity, idle production, levels, achievements, quests. Not a ledger.loot, production engines
marketmarketVendors that restock, order books and trades between players, with escrow in each player's own hub. Not a ledger.market-engine
query {
execStarters(appId: "…") {
manifestJson
starters { crate nodeType description files { path content } }
}
}

Their endpoints:

TypeMethodWhoWhat
worldstateanyoneThe clock, weather, active modifiers, resource nodes and events
worldharvest { node }playersTakes one from a resource node (twice a second at most)
worldschedule { name, start_ms, end_ms, modifiers }, cancel { name }developersLiveops events. Modifiers multiply where events overlap; the NPC engine reads aggro
matchmakerjoin { rating, party?, leader? }, leave, accept, decline, statusplayersYour own place in the queue: party (while a party forms), queued, proposed, then matched with the session key. A party is matched whole
sessionjoin, leave, ready { ready }, start (host), act { points } (whoever's turn it is), stateplayersThe game. points_for in the crate is the rule to replace with yours
sessionconfigure { .. }the host in the lobby, or developersPlayer counts, turn time, target score, rounds
npcsstate, attack { mob }, talk { npc }playersMobs and NPCs, public properties only. A hit needs the player near the mob; the damage is the hub's
npcsadopt { species, name }, rename, summon, dismiss, release { pet }, petsplayersPets that follow their owner while summoned; pets answers the caller's, with their bond

Since ckx-sdk 0.10.0 the starters speak the Game Kit protocol: their topics carry numbered snapshot and delta messages, their changes take an action_id they answer once, and their refusals carry a kit:<code>: code. The SDKs' Game Kit drives them with typed clients, and its topology composes, builds and deploys them for you: see the Game Kit.

The genre and economy starters' endpoints and topics are on the Game Kit's starters page (ckx-sdk 0.11.0 and 0.12.0). The economy lives in hub state and is not a ledger: read its limits first.

Building​

mutation {
execBuild(input: {
appId: "…"
crates: [{ name: "world-tick", files: [
{ path: "Cargo.toml", content: "…" },
{ path: "src/lib.rs", content: "…" }
] }]
}) {
buildId
status
}
}

It returns at once, with the build queued. Poll it until it is succeeded or failed:

query {
execBuildStatus(appId: "…", buildId: "…") {
status
sdkVersion
log
artifacts { crate digest sizeBytes }
}
}

Each crate becomes one module. log is the compiler's output, and a failed build says why; its first line names the toolchain the build ran with. sdkVersion is the version of ckx-sdk the build compiled against (of crowdy-client-sdk for a CLIENT half), once the build has started. A build of the four starters with nothing cached takes about a minute; the same sources again come from the cache.

What a crate may contain​

  • Files: Cargo.toml, README.md, and .rs files under src/, with src/lib.rs among them.

  • Cargo.toml holds [package] (name, version, edition, description, publish, license, authors, rust-version), [lib] with crate-type = ["cdylib"], and [dependencies] on ckx-sdk, serde and serde_json only. Each key is on one line, and there is no workspace inheritance. The platform points ckx-sdk at its own copy, whatever version the crate names, and adds the release profile. A build's sdkVersion says which version that was; code written for a newer SDK than the platform's fails to compile against it.

    [package]
    name = "lobby"
    version = "0.1.0"
    edition = "2024"

    [lib]
    crate-type = ["cdylib"]

    [dependencies]
    ckx-sdk = "0.7.0"
    serde = { version = "1", features = ["derive"] }
  • Code may not read the machine it is built on. include!, include_str!, include_bytes!, env!, option_env!, the asm! macros, #[path] and #[link] are refused, even renamed, and so is mod name; inside a macro_rules! body. The names are fine in comments and strings. The compiler also runs in a sandbox with no network, no environment and nothing of the host but the toolchain.

  • The module may import only the ck-exec guest ABI, which is what ckx-sdk uses, and must export what export_hub! or export_spoke! generates.

A mod's CLIENT half is built from one crowdy-client-sdk crate under the same rules, with its own dependency list, and runs in browsers rather than on ck-exec.

Limits​

Crates in a build16
Files in a crate64
Source in a build2 MB
Build time10 minutes for the whole build: its crates compile one after another, about 25 seconds each, so the full starter pack (15 crates) takes about six
Module size16 MB
At onceOne build per app, and one per person across every app, on each API instance; eight waiting per instance
Kept7 days, with their modules

The requests are billed like any other; the compile itself is not, as the compute compile it replaces was not.

On your machine​

The open dev kit runs this build on your own machine: the same checks on the same files, the Rust release the platform pins, its release profile and its dependency versions, then the same module check. ckx-kit check runs the source checks alone, and cargo test tests your crates natively against a fake of the platform. When a crate passes there, build it here from the same sources and deploy that build.

Deploying a build​

Pass the build's id to execDeploy. A type may name its crate instead of a digest, and the modules the manifest names come from the build. The starter pack's manifestJson is ready for this.

mutation {
execDeploy(input: {
appId: "…"
buildId: "…"
manifestJson: "{\"root\":\"world\",\"types\":{\"world\":{\"kind\":\"hub\",\"crate\":\"world-tick\",\"client\":true}}}"
}) {
version
}
}

A build belongs to its app: another app cannot deploy it.

From an SDK​

CrowdyJS does the whole flow for you:

const pack = await client.exec.starters(appId);
const build = await client.exec.build(
appId,
pack.starters.map((s) => ({ name: s.crate, files: s.files })),
);
const done = await client.exec.waitForBuild(appId, build.buildId);
if (done.status !== 'succeeded') throw new Error(done.log ?? 'build failed');
await client.exec.deploy({ appId, buildId: build.buildId, ...pack.manifest });
CrowdyJS client.execCrowdyCPP client.exec()
starters(appId)starters(appId)
build(appId, crates), each crate's files a path map or a { path, content } listbuild(appId, std::vector<ExecCrate>)
buildStatus(appId, buildId)buildStatus(appId, buildId)
waitForBuild(appId, buildId, { intervalMs, timeoutMs })waitForBuild(appId, buildId, intervalMs, timeoutMs), blocking; from an event loop, poll buildStatusAsync
deploy({ appId, root, types, buildId }), a type naming its cratedeploy(appId, root, types, buildId), an ExecNodeType with crate set

Every CrowdyCPP method but waitForBuild has an …Async twin. Use CrowdyJS 18.1.0 or CrowdyCPP 0.55.0 or later on dev.

Typed state​

The legacy game models kept containers with typed properties in the database. In ck-exec, a hub keeps them in its own state with ckx_sdk::model: declared container types with typed, defaulted properties, edges between containers, and a visibility for each property that decides what a player may read. The model is plain data, so it is snapshotted with the rest of the hub.

use ckx_sdk::prelude::*;

fn schema() -> Schema {
Schema::new().container("Character", |c| {
c.prop("hp", PropKind::Int, 100, Visibility::Public)
.prop("gold", PropKind::Int, 0, Visibility::Owner)
.prop("weakness", PropKind::Str, "", Visibility::Hidden)
})
}

// In a handler:
let hero = self.model.ensure(&schema(), "Character", &player.to_string(), Some(player))?;
self.model.add(&schema(), hero, "hp", -12)?;
let theirs = self.model.view(&schema(), hero, Viewer::Player(player))?; // hp and gold, not weakness
self.model.publish_changes(ctx, &schema(), "world"); // public changes, to the topic's subscribers

link, linked and traverse (five levels at most) relate containers. A property added in a new version reads as its default in containers made before it. The npc-mobs starter keeps its mobs and NPCs this way.

Coming from the legacy APIs​

Legacyck-exec
computeTemplates, computeDeployTemplateexecStarters, then execBuild and execDeploy
computeDeployVersion (compiled on the server)execBuild, then execDeploy with the buildId
Game-model schemas: container types, typed properties, visibilityckx_sdk::model in a hub
The Game Kit (client.kit(appId).deploy)The starter packs, and the new Game Kit's kit.topology.deploy
playerComputeDeploy (players' server code)Mods: execModBuild, then execModDeploy
playerComputeDeploy with the CLIENT target (players' browser code)CLIENT halves: execModClientBuild, then execModClientDeploy