Skip to main content

Grids and permissions

Crowded Kingdoms separates app access (does a player have access to your game, and what does their tier allow?) from grid permissions (what may a player do in a specific 3D region of the world?).

Two layers​

LayerAPIWhat it answers
Tier / app accessManagement APIDoes this player have access to the app? Does their tier allow update_voxel_data?
Grid permissionsGame API (this service)In this 3D region, what is this player allowed to do?

Grids do not replace tiers — they add a spatial gate on top of tier access. Both gates are always enforced: to act in a chunk a player needs the key from their tier and a grid (covering that chunk) where they hold the key. For building (update_voxel_data) that grid is not just any covering grid: it is the most specific one, see which grid decides a voxel write.

You don't have to build this from scratch. A new app starts open by default: it gets a default grid spanning the whole world, and every player you grant app access is auto-granted the four legacy gameplay keys on it (see Permissions overview → Open by default). The grids and grants below are how you layer restrictions or finer ownership on top of that open baseline — e.g. carve out a safe zone, or hand one player their own plot. The four code keys are never auto-granted.

Permission keys​

Grid permissions reuse the runtime permission keys:

KeyGates
accessEntering / moving / sending events in the region
update_voxel_dataEditing voxels (building) in the region
use_voice_chatVoice audio in the region
use_video_chatWebcam video in the region (opt-in; the sender's tier must also carry it)
write_server_codeAuthoring/deploying server Rust on a grid the player owns
run_server_codeActivating admitted server code in an owned grid
write_client_codeAuthoring browser-target Rust
run_client_codeRunning admitted browser artifacts

Query the Management API runtimePermissions for the catalog when building a key picker in your studio UI.

Grids​

A grid is an axis-aligned box of chunks inside one of your app's world bounds. A grid can be as large as a region or as small as a single chunk — so you can give one player control of exactly one chunk.

First-class ownership​

Permission grants answer "what may this user do here?" Ownership answers "whose grid is this, and whose identity does its code run as?" They are separate. gridOwnership reads current title; assignGridOwnership is the P1 studio/bootstrap assignment path; and transferGridOwnership transfers title and removes the old direct grants. The grid's mods stop and become the new owner's, switched off, without the state the old owner's runs kept.

Owning a grid grants no permissions by itself. A player still needs the appropriate tier + grid keys. See mods for the code a grid owner runs there.

mutation {
createGrid(input: {
appId: "1",
corner1: { x: "0", y: "0", z: "0" },
corner2: { x: "0", y: "0", z: "0" } # single-chunk grid
}) { grid { grid_id } error }
}

Where you can place a grid​

createGrid returns a hybrid result — always inspect error first. On NO_ERROR the grid is populated; otherwise grid is null and error tells you why:

errorMeaning
NO_ERRORGrid created.
NO_MATCHING_GRID_ASSIGNMENTThe box isn't inside any of the app's buildable regions. An open-by-default app's world region covers everything, so you normally only see this on a brand-new app before it has been initialized (it clears once the app is in use — e.g. after the first player connects).
GRID_OUTSIDE_ASSIGNMENTThe box extends past the region it sits in.
GRID_OVERLAPS_EXISTINGThe box partially overlaps another grid at the same level.
GRID_ALREADY_EXISTSA grid with those exact bounds already exists.

Two placement rules:

  1. Inside a buildable region. A grid must fit within one of your app's buildable regions (grid assignments). The open-by-default world grid spans the whole world, so in a normal app you can place a grid anywhere.
  2. Nest, don't straddle. A grid may be nested inside a broader grid — this is exactly how you carve a smaller, more-restricted area out of the world grid (e.g. a safe zone or one player's plot). It may not partially overlap a peer grid: give peers disjoint boxes, or nest one fully inside the other. A nested grid takes over voxel writes in its chunks from the grid around it.

Deleting a grid​

If you create a grid with the wrong bounds, createGrid may keep returning GRID_OVERLAPS_EXISTING for every follow-up attempt in that area. Use deleteGrid to remove the mistaken peer grid so you can recreate it with correct corners.

mutation {
deleteGrid(input: {
appId: "1",
gridId: "10"
}) { gridId error }
}

Like createGrid, deleteGrid returns a hybrid result — inspect error first:

errorMeaning
NO_ERRORGrid deleted; gridId is the removed id.
GRID_NOT_FOUNDNo grid with that id in the app (wrong app or already deleted).
CANNOT_DELETE_DEFAULT_WORLD_GRIDThe open-by-default world grid cannot be removed.
GRID_HAS_NESTED_CHILDRENA smaller grid is still nested inside this one — delete the child grids first.
UNKNOWN_ERRORUnexpected failure; retry or contact support.

Safety guards. The mutation requires manage_apps on the app's organization (same as other grid admin operations). It rejects the default world grid and any grid that still contains nested child grids. It only removes the grid definition and its permission rows (direct grants, group grants, limits, and the materialized effective ACL entries that cascade from them). It does not delete world content — chunks, voxels, and actors in that region are untouched.

Overlap unblock workflow. When createGrid returns GRID_OVERLAPS_EXISTING, identify the overlapping peer grid (for example via your studio map or by listing grids for the app), call deleteGrid on the mistaken peer, then run createGrid again with the intended bounds.

Granting permissions on a grid​

There are two ways to give players permissions on a grid. Both contribute to a player's effective permissions, and both take effect immediately.

Grants driven by game logic

Your app's ck-exec code can also grant or revoke direct grid permissions itself — buying land, earning access, banishment — with ctx.permissions() and the permissions.write scope (see world and platform data). Those grants land in the same direct-grant layer as grantGridPermissions.

Direct grants (per player)​

mutation {
grantGridPermissions(input: {
appId: "1", gridId: "10", userId: "42",
permissionKeys: ["access", "update_voxel_data"]
}) { permissionKeys }
}

Use revokeGridPermissions to remove keys (omit permissionKeys to remove all), and gridUserPermissions(appId, gridId, userId) to read a player's effective keys on a grid. grantGridPermissions requires the target player to have active app access.

Group grants (per team or group)​

Assign a team or group to a grid so that every member (or every member holding a particular role) gets the keys. This is the easiest way to grant a whole guild build rights in their territory.

mutation {
assignGroupToGrid(input: {
appId: "1", gridId: "10", groupId: "7",
# groupRoleId is optional: omit to grant to all members,
# or set it to grant only to members with that role (e.g. "builder").
permissionKeys: ["access", "update_voxel_data"]
}) { groupId groupRoleId permissionKey }
}

Use revokeGroupFromGrid to remove a group's grant (optionally a subset of keys or a single role), and gridGroupGrants(appId, gridId, groupId) to list them.

When a player joins or leaves the group, or gains/loses the relevant role, their grid permissions update automatically.

Limiting what a grid allows​

setGridPermissionLimits caps which keys can ever take effect on a grid, regardless of grants. This is how you build a safe zone (e.g. movement only, no building):

mutation {
setGridPermissionLimits(input: {
appId: "1", gridId: "10",
permissionKeys: ["access", "use_voice_chat"] # building disallowed here
}) { permissionKeys }
}

A grant of update_voxel_data on a limited grid simply won't take effect until update_voxel_data is added to the limits. Pass an empty array to remove all limits (every key becomes allowed again).

Which grid decides a voxel write​

Where grids nest, the most specific grid covering a chunk decides who may edit voxels there: the covering grid with the smallest box (width × height × depth, in chunks); of two equal boxes, the one with the lower gridId. A player may edit voxels in the chunk only when they hold update_voxel_data on that grid, and their tier carries it too. A grant on a broader grid does not reach into a grid nested inside it:

  • a claimed chunk or a plot decides its own chunks: its owner builds, and a visitor is refused even where they may build all around it;
  • a safe zone that grants nobody update_voxel_data (spawn protection, say) refuses building to everyone;
  • your app's world grid decides only the wilderness, the chunks no other grid covers;
  • a chunk no grid covers at all is refused.

This holds for every voxel write: updateVoxel, sendVoxelUpdate, updateChunk (as a player), a mod's set_voxels, and a client's own realtime voxel update, which the replication servers refuse with UNAUTHORIZED. Only building works this way. access, teleport, use_voice_chat and use_video_chat are still granted by any covering grid where the player holds them, so a player keeps moving and talking inside a plot they cannot build on.

A grant or revoke reaches every Game API server's voxel checks at once, and the replication servers' within 15 seconds.

Voxel positions and types are yours to define​

A voxel write names a position inside its chunk and a voxel type, and both are app-defined signed 16-bit integers (-32,768 to 32,767): updateVoxel, sendVoxelUpdate, updateChunk's voxelStates, a hub's set_voxels and a client's own realtime voxel update all accept the full range and check nothing narrower. A game on 16×16×16 chunks uses 0–15 on each axis; another may address voxels however its world needs. The chunk's dense grid (Chunk.voxels, one byte per voxel) is an optional 16×16×16, 8-bit format: a type above 255 or a position outside it travels as a voxel state (Chunk.voxelStates) and in the edit log. A voxel's state is at most 1 KiB on every path. A chunk read applies up to 65,536 recorded edits and sets voxelStatesTruncated past that; listVoxels pages the whole log.

Open grids​

A zone meant for everyone, like a public build area or an arena, must now grant everyone itself. Nesting it in the world grid is no longer enough. Open it:

mutation {
setGridOpenPermissions(input: {
appId: "1", gridId: "10",
permissionKeys: ["access", "update_voxel_data", "use_voice_chat"]
}) { gridId permissionKeys }
}

Every player with active access to the app then holds those keys on the grid, within its limits, and so does every player who gains access later. The call replaces the grid's open keys; pass an empty array to close it again. gridOpenPermissions(appId, gridId) reads them. The four code keys cannot be opened, the world grid is open already, and an app has at most 32 open grids; each is refused BAD_REQUEST. In the SDKs: client.gameApps.setOpenPermissions / openPermissions in CrowdyJS (18.1.0), gameApps().setOpenPermissions / openPermissions in CrowdyCPP (0.55.0).

Claims are made in the wilderness​

claimGridChunk claims only a wilderness chunk. A chunk inside another grid (a plot, a zone you created, someone else's claim) is refused, FORBIDDEN with GRID_NOT_CLAIMABLE, because the claim would become that chunk's most specific grid and take it from the grid's owner or grantees. A chunk that is already claimed answers GRID_ALREADY_CLAIMED. See claims.

Wilderness​

The wilderness is every chunk that no grid covers except your app's default world grid, i.e. land nobody has claimed and you have not zoned. By default it is open: a player whose tier and world-grid grant carry update_voxel_data may build there.

An org admin (manage_apps on the app) can close it with updateApp:

mutation {
updateApp(appId: "1", input: { wildernessWritesOpen: false }) {
wildernessWritesOpen
}
}

While it is closed, the Game API refuses every voxel and chunk write to a wilderness chunk made for a player: updateVoxel, sendVoxelUpdate, updateChunk (org admins included) and a player's mod. Each is answered FORBIDDEN ("This app has closed its wilderness…"). A client's own realtime voxel update there is refused with UNAUTHORIZED. Your app's own ck-exec code is not held to it: its ctx.world().set_voxels writes the wilderness either way, so a hub can run the world while players may not build in it. A chunk that any other grid covers (a claimed plot, a zone you created) is unaffected: its most specific grid decides. App.wildernessWritesOpen reports the setting to the app's org admins, a game client reads it in gameClientBootstrap.wildernessWritesOpen, and a hub with grids.read in ctx.grids().settings(). Every Game API server applies a change at once; the replication servers within 15 seconds.

Writing whole chunks​

updateChunk replaces a chunk's dense voxel grid. It takes an app token for the app and either manage_apps on the app (your studio tooling and seed scripts, like updateChunkState and updateChunkLods) or the same permission a single voxel edit needs in that chunk: app access, update_voxel_data from the tier, and update_voxel_data on the most specific grid covering the chunk. The ChunkStore write-back in CrowdyJS and CrowdyCPP runs as the player, so a player's client persists only chunks that player may build in; the store sends a refused write-back once, drops it and reports it (onWriteBackFailed).

Effective permissions​

A player's effective permissions on a grid are:

(direct grants ∪ group/role grants ∪ the grid's open keys) ∩ grid limits

For a voxel write, only the effective permissions on the chunk's most specific grid count. Use gridUserPermissions for one grid, or nearbyGridPermissions to fetch every grid overlapping a chunk box together with the player's keys — handy for a studio map overlay.

Who can manage grids​

All grid operations (createGrid, deleteGrid, grantGridPermissions, revokeGridPermissions, assignGroupToGrid, revokeGroupFromGrid, setGridPermissionLimits, setGridOpenPermissions) require the manage_apps permission on the app's organization. Your app's own ck-exec code can make the same changes with the grids.write and permissions.write scopes: create grids (and delete the ones it created), give groups keys, open grids, and grant players keys.

Reference​

See the Game API GraphQL reference for the exact inputs and return types of each operation.