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 fullmountCrowdyStudiocall — including the in-browser agent pane when the game passes thedshoption (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-modhud_setpayloads plus the drawer HUD preview. Untrusted payloads render as text, never HTML.ensureCrowdyStudioEmbedStyles()— injectedck-crowdy-studio-embed-*styles; games restyle by overriding classes. While docked, the panel sets--ck-game-right-insetondocument.bodyso 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.
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'sdist/dsh-web/on the game's own origin, at the path the embed'sdsh.webBasenames (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-originand aCross-Origin-Embedder-Policyon every response, socrossOriginIsolatedistrue; 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_codeandrun_server_codefor SERVER mods,write_client_codeandrun_client_codefor CLIENT halves. The agent pane appears only for a player whose tier also holdsuse_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 needrun_client_codeon 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.
Full-featured embed (The Construct)
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 usesck-crowdy-studio-dsh-*); override them in game CSS to restyle. - While docked, the panel maintains
--ck-game-right-insetondocument.body. Usevar(--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;
suppressGameplayInputis invoked in both presentations.