Skip to main content

Embed Crowdy Studio in your game

CrowdyJS ships the Crowdy Studio embed kit: the window chrome a game puts around Studio, packaged as game-agnostic components. The public starter The Construct is the reference integration. A game no longer hand-rolls a dock, fullscreen fallback, focus trap, context drawer, or HUD sink around mountCrowdyStudio — it supplies only what is genuinely game-specific:

  • an entry point — how a player claims/selects an owned grid and opens the studio (keybind, HUD button, build menu);
  • input suppression — pausing gameplay keys while the studio has focus;
  • layout hooks — reacting to the dock inset so HUDs and the game viewport stay clear; and
  • (CLIENT-mod games only) the world-read host-call router and player-host adapter.

Everything else — panel, splitter, styles, safety chrome, and the glue-worker packaging — comes from the SDK. A project's SERVER target runs as the grid's ck-exec mod and its CLIENT target as that mod's CLIENT half, so the embed needs the client's exec domain. Since CrowdyJS 18, ck-exec is its only engine: there is no serverEngine option and no playerCompute service.

What the kit provides​

From @crowdedkingdoms/crowdyjs/crowdy-studio:

  • createCrowdyStudioEmbed(options) / CrowdyStudioEmbed — a responsive panel that renders a resizable right dock on desktop and a focus-trapped fullscreen modal on narrow viewports. It owns Escape/close-key semantics, the compact header (title, grid pill, Context drawer, Close), loading and retry chrome, and assembles the full mountCrowdyStudio call — including the in-browser agent pane when the game passes the dsh option (CrowdyJS 16; see Agentic Crowdy Studio).
  • CrowdyStudioEmbedDock — the accessible game/studio splitter with persisted width (ck:crowdy-studio:embed:dock-width:v1), arrow-key/Home/End resize, and ARIA value text.
  • CrowdyStudioTextHud — the text-only presentation sink for CLIENT-mod hud_set payloads plus the drawer HUD preview. Untrusted payloads render as text, never HTML.
  • ensureCrowdyStudioEmbedStyles() — injected ck-crowdy-studio-embed-* styles; games restyle by overriding classes. While docked, the panel sets --ck-game-right-inset on document.body so game HUDs can keep clear of the dock.

From @crowdedkingdoms/crowdyjs/player-host: PlayerHostAdapterV1, the observation contract the agent's game_observe reads. (The PlayerControlGate and AgentControlBanner chrome left with the agent orchestrator in 16.0.0; the in-browser agent only observes and never takes control.)

New package subpath:

  • @crowdedkingdoms/crowdyjs/player-glue-worker — the self-starting, tokenless CLIENT-mod glue worker entry. Bundle it as a same-origin module worker (for example Vite's ?worker&url) instead of copying a worker wrapper into the game.
Choosing SERVER-only or SERVER + CLIENT

CLIENT-target mods run untrusted player code in the visitor's browser. The platform admits them for any app whose access tier grants write_client_code / run_client_code; whether your game offers them is your decision. A SERVER-only embed (below) omits workerUrl, onHostCall, hud and playerHost and forces CLIENT: { canWrite: false, canRun: false }, which hides every client-run affordance. The full SERVER + CLIENT integration — same-origin glue worker, an allowlisted host-call router, the text HUD, the visitor trust lifecycle, and the cross-origin-isolation headers it all depends on — is implemented and documented in the public starter The Construct (docs/MODDING.md), which ships it on by default with a build switch.

What a game needs for mods​

Players mod a game from inside it, so the game and its app have to offer it. The Construct does all of this out of the box (its npm run setup and its default build). A game of your own needs:

  • The dock. The page embeds Studio with this kit, and gives players a way to claim or pick a grid they own and open Studio on it (a key, a HUD button, a build menu).
  • The agent's harness under /dsh/. For the agent pane, the build ships @crowdedkingdoms/crowdy-dsh's dist/dsh-web/ on the game's own origin, at the path the embed's dsh.webBase names (Shipping the harness), and the host serves that path with a policy of its own (Headers).
  • Cross-origin isolation, for CLIENT mods. The host sends Cross-Origin-Opener-Policy: same-origin and a Cross-Origin-Embedder-Policy on every response, so crossOriginIsolated is true; without it CLIENT halves cannot run. Host your game lists every header.
  • An access tier with the keys. A player writes and runs mods with the keys their access tier grants: write_server_code and run_server_code for SERVER mods, write_client_code and run_client_code for CLIENT halves. The agent pane appears only for a player whose tier also holds use_studio_agent. The Construct's setup creates a Constructor tier with all five keys and grants it to you. Visitors who should run CLIENT halves need run_client_code on their own tier.
  • Allow Live Modding. The agent tests drafts, and deploys live after the player confirms, only while the app's Studio Agent policy is on: the Allow Live Modding switch on the app's page in Studio (see the agent policy). While it is off, the API refuses those calls, and the dock still opens.
  • Nothing more for SERVER mods. A SERVER target runs on ck-exec as the grid's mod, wherever the game is hosted.

Crowdy Studio and mods in the browser covers the CLIENT sandbox and mounting Studio, and Build mods with Crowdy Studio is the walkthrough from a player's side.

Minimal SERVER-only embed (the reverse-tower-defense pattern)​

The smallest complete integration is one module: claim the chunk the player is standing on, then toggle the embed with SERVER-only permissions. This is the pattern piloted in reverse-tower-defense ("Tower Assault").

Create the embed once at startup. Only the studio, mod, compile and wallet services are exposed and there is no dsh option, so this game has no agent pane:

import {
createCrowdyStudioEmbed,
type CrowdyStudioEmbedContext,
} from '@crowdedkingdoms/crowdyjs/crowdy-studio';

const embed = createCrowdyStudioEmbed({
client: {
get crowdyStudio() { return network.sdk.crowdyStudio; },
get exec() { return network.sdk.exec; },
get playerWallet() { return network.sdk.playerWallet; },
},
appId: () => network.currentAppId(),
gameName: 'Tower Assault',
closeKeyCode: 'KeyL',
onClosed: () => refreshLauncherButton(),
});

Claim the player's current chunk with the platform claimGridChunk mutation (the same game-API surface The Construct uses), then open the studio with SERVER-only permissions derived from the claim's effectivePermissionKeys:

const claimed = await network.sdk.marketplace.claimGridChunk({ appId, chunk });

const context: CrowdyStudioEmbedContext = {
gridId: String(claimed.gridId),
grid: { low: claimed.lowChunk, high: claimed.highChunk },
targetPermissions: {
SERVER: {
canWrite: claimed.effectivePermissionKeys.includes('write_server_code'),
canRun: claimed.effectivePermissionKeys.includes('run_server_code'),
},
// This game offers SERVER mods only, even when the claim grants the CLIENT keys.
CLIENT: { canWrite: false, canRun: false },
},
permissionsNote: 'This game runs SERVER mods only.',
};

embed.toggle(context);

That is the whole loop: the panel handles create project → edit (Monaco with the local Rust language worker) → Test draft → Deploy live → Invoke and Logs against the grid's mod. The server-code permission keys come from the player's app access tier, so the claim result is authoritative — no game-side permission logic.

Bundler note (Vite)​

The studio's Monaco/language workers are created with new Worker(new URL(...), import.meta.url) inside the SDK. Two Vite settings keep them working:

export default defineConfig({
worker: {
// The studio language worker code-splits; iife workers cannot.
format: 'es',
},
optimizeDeps: {
// Pre-bundling would inline the SDK into .vite/deps and break the
// worker URL resolution in dev. The @codingame pair must also stay
// un-bundled so the VS Code service overrides register against the
// same module instance the editor API reads from (a pre-bundled copy
// fails with "…is not supported. You are using a feature without
// registering the corresponding service override" and the studio
// falls back to the basic editor).
exclude: [
'@crowdedkingdoms/crowdyjs',
'@codingame/monaco-vscode-editor-api',
'@codingame/monaco-vscode-api',
],
include: [
'@crowdedkingdoms/crowdyjs > vscode-jsonrpc',
'@crowdedkingdoms/crowdyjs > vscode-languageserver-protocol',
'@crowdedkingdoms/crowdyjs > vscode-languageserver-types',
],
},
});

If the workers still cannot load, the embed degrades to the file-aware textarea workspace rather than failing; deploys remain authoritative server-side either way.

The Construct consumes the same kit with every optional surface enabled; its packages/construct/src/platform/studio/StudioService.ts is the working example. The additional wiring, all game-supplied:

import {
CrowdyStudioEmbed,
CrowdyStudioTextHud,
} from '@crowdedkingdoms/crowdyjs/crowdy-studio';
import glueWorkerAssetUrl from '@crowdedkingdoms/crowdyjs/player-glue-worker?worker&url';

const hud = new CrowdyStudioTextHud();

const studio = new CrowdyStudioEmbed({
client: game, // crowdyStudio, exec, playerWallet, crowdyStudioGitHub
appId,
gameName: 'My Game',
dsh: {
graphql: game.graphql,
webBase: `${import.meta.env.BASE_URL}dsh/`, // @crowdedkingdoms/crowdy-dsh dist/dsh-web, copied at build
graphqlUrl: game.graphqlEndpoint,
apiOrigin: new URL(game.graphqlEndpoint).origin,
getToken: () => game.getToken(),
persistScope: `${appId}/${playerUuid}`,
},
suppressGameplayInput: () => lockPlayerInput(),
onLayoutChange: () => relayoutHud(),
});

studio.open({
gridId,
grid,
targetPermissions, // authoritative effective grid keys, both targets
workerUrl: glueWorkerAssetUrl, // CLIENT mods: the same-origin glue worker
onHostCall: (call) => routeClientHostCall(call), // owner-lawful world reads
hud,
playerHost: playerHostAdapter, // observe only -> game_observe
dshHost: {
captureFrame: async () => {
renderer.render(scene, camera); // or preserveDrawingBuffer: true
return renderer.domElement;
},
},
});

The two-layer CLIENT sandbox, presentation hooks, and deploy loop are unchanged from Crowdy Studio & mods in the browser; the agent pane is described in Agentic Crowdy Studio. The kit is chrome — it grants no authority. Deploys, drafts and invokes are authorized server-side exactly as before, and a live deploy the agent asks for waits for the player to confirm in the pane.

Blocks with Friends, the first game to embed the kit, wires the same options in its own source and is a further example of them. Crowded Kingdoms no longer hosts it; The Construct is the reference to follow.

GitHub repository card (17+)​

GitHub is optional: a project is source: 'STUDIO' until its owner binds a repository and 'GITHUB' while one is bound, and nothing in the embed changes for a game that never binds. When the client is a full CrowdyClient, the embed passes client.crowdyStudioGitHub to the Studio controller and the settings pane grows a GitHub repository card: Connect, Create repository on GitHub (17.1.0), Bind owner/repo@branch (choosing push the project into the repository or take the repository as the project for the first commit), Refresh, and Unbind. The card hides itself when the environment has no GitHub App.

Create repository on GitHub exists because the app cannot create one for you — it holds installation tokens only. controller.createGitHubRepository() opens GitHub's new-repository page prefilled (githubNewRepositoryUrl: your connected login as owner, the project name slugged, private), then prefills the card's bind input with owner/name and selects push this project. When status.repositorySelection is 'selected', the message reminds you to add the new repository to the installation first. The first bind commit carries crowdy.json, a README.md (when the branch has none) and the files.

While bound, the repository is the working tree: saveProject sends each changed file as its own commit carrying the project's current commit SHA (expectedCommitSha), so a stale commit surfaces as the same CrowdyStudioRevisionConflictError the editor already recovers from. The project's files are the server's mirror of the repository, read exactly as before. Both builds send their crate's files from that mirror: the SERVER target's mod build and the CLIENT target's CLIENT-half build. There are no Push / Pull buttons and no autosave toggle any more (removed in 17.0.0 with pushToGitHub, pullFromGitHub, setAutosave and the SDK-side crowdy.json helpers; client.crowdyStudioGitHub.layout() is the only layout grammar). Refresh brings the mirror to the branch head after a push made elsewhere.

Connect and Bind need the modder's identity session, so the card offers them only where one exists (hosted Studio). A game keeps the default github transport, client.crowdyStudioGitHub — the app-token client that plays — and the API scopes every GitHub field to projects that token's user owns, so a game never needs an identity session to author against GitHub; a third-party game must never read one. A host that does hold an identity session passes its own transport through the github option of createCrowdyStudioEmbed / mountCrowdyStudio. Nothing GitHub-related is stored in the browser. See Connect your GitHub repo to a Studio project.

The game editor (18.23+)​

A project bound to a Construct game's repository (layout().kind === 'CONSTRUCT_GAME') opens the game editor when the embed has a draft store:

import {
createCrowdyStudioEmbed,
GitHubDraftStore,
IndexedDbDraftOverlayStorage,
} from '@crowdedkingdoms/crowdyjs/crowdy-studio';

const storage = new IndexedDbDraftOverlayStorage();
const studio = createCrowdyStudioEmbed({
// ...
draftStore: ({ appId, projectId, githubSha }) =>
new GitHubDraftStore({
transport: client.crowdyStudioGitHub,
appId,
projectId,
baseCommitSha: githubSha,
storage,
}),
});
// A Live code link names the project to open first.
studio.toggle({ gridId, targetPermissions, projectId });

GitHubDraftStore keeps the player's edits in IndexedDB on the game's own origin, reads the bound branch in batches (crowdyStudioGitHubFiles) and commits the draft as one commit (crowdyStudioGitHubCommitFiles). Any DraftStore will do: the Construct framework supplies one over a local dev server's checkout. Return null for a project the page cannot edit, and it opens in the mod editor.

Styling and layout contract​

  • All kit chrome uses ck-crowdy-studio-embed-* classes (the agent pane uses ck-crowdy-studio-dsh-*); override them in game CSS to restyle.
  • While docked, the panel maintains --ck-game-right-inset on document.body. Use var(--ck-game-right-inset, 0px) in HUD/viewport rules so layouts are correct whether or not the studio is open.
  • The dock width persists per browser under ck:crowdy-studio:embed:dock-width:v1.
  • Below the fullscreen breakpoint the panel becomes a focus-trapped modal; suppressGameplayInput is invoked in both presentations.