Skip to main content

Set up a game and give players access

Bringing a game online involves two roles:

  • The admin (an org owner / studio) sets the game up: an organization, an app, and one or more access tiers.
  • Players then get access to that app — either automatically (open‑by‑default) or by an explicit grant.

Entitlements — who may play, at which tier, and which runtime permission keys that tier carries — are defined here on the Management API. The Game API enforces them at runtime: on a player's first connect it mirrors the entitlement and provisions the matching world‑grid permissions, and Buddy (the UDP server) checks both app‑ and grid‑level permissions on every spatial message.

All operations below are available in the Management UI and via GraphQL with a user session token or an org API token. Exact field names per version are in the schema reference.


1. Admin: set up your game

a. Create an organization

You become the org owner (full manage_apps / manage_environments permissions).

mutation { createOrganization(input: { name: "Acme Games", slug: "acme" }) { orgId } }

b. Create an app

Creating an app automatically provisions a free, default access tier holding the standard runtime permission keys (access, teleport, update_voxel_data, use_voice_chat). That single step makes the app immediately playable and is what enables open‑by‑default access (next section).

mutation {
createApp(input: { orgId: "ACME_ORG_ID", name: "My World", slug: "my-world", status: LIVE, visibility: PUBLIC }) {
appId
}
}

createApp requires the manage_apps permission on the org (org owners have it by default). status is one of DRAFT, LIVE, ARCHIVED; visibility is PUBLIC, UNLISTED, or PRIVATE (only PUBLIC + LIVE apps surface in the marketplace).

Manage the app over its lifecycle (also manage_apps):

# Update metadata: name, description, visibility, status, metadata.
mutation { updateApp(appId: "APP_ID", input: { description: "Now with voxels", visibility: UNLISTED }) { appId } }

# Take an app offline without deleting it (sets status to ARCHIVED).
mutation { archiveApp(appId: "APP_ID") { appId status } }

Read an app's current state any time with the app(appId) query — including the routing fields (gameApiUrl, splitMode, deploymentTarget) covered in step d.

c. (Optional) Define additional access tiers

Add tiers for premium/paid access, or to grant a narrower/broader set of permissions. List assignable keys with the runtimePermissions query.

mutation {
createAccessTier(input: {
appId: "APP_ID",
name: "Premium",
isFree: false,
isDefault: false,
permissionKeys: ["access", "teleport", "update_voxel_data", "use_voice_chat"]
}) { tierId }
}

Tier operations require the manage_access_tiers permission. Edit or retire tiers with updateAccessTier(tierId, input) and archiveAccessTier(tierId), and list an app's tiers (no auth) with appAccessTiers(appId).

Opting out of open‑by‑default: if you do not want anonymous/auto access, remove (or never create) a tier that is both isFree and isDefault. Players then only get access through an explicit grant (below).

The four player-code keys (write_server_code, run_server_code, write_client_code, run_client_code) are not in the generated default tier. Add them only to tiers intended for authors or mod users; see Player code and owned grids.

c.1 (Optional) Censor player code with strict admission

New apps default to IMPLICIT_ALLOW (censorship off). To require studio approval for every running player artifact:

mutation {
setAppCodeAdmissionMode(appId: "APP_ID", mode: ALLOW_LIST)
}

ALLOW_LIST is strict: self-authored code in its author's own grid still waits for the code, author, or authoring org to be admitted. Deploy and compile remain available; only execution is gated.

mutation {
admitAppCode(input: {
appId: "APP_ID"
subjectKind: AUTHOR
subjectRef: "PLAYER_USER_ID"
}) {
admissionId
admittedAt
}
}

Use appCodeAdmissions to inspect active/history rows and revokeAppCodeAdmission to drain admitted code. Writes require manage_compute; reads require view_compute_diagnostics. Admission never grants source access — closed source remains author-only with no moderation override.

Admission at scale (P4a). Once the app has a marketplace catalog, the moderation surface is appCodeAdmissionQueue(appId): every listing joined with its allow-list standing (ADMITTED / PENDING / REVOKED) and which subject matched (the listing, its author, or its owning org). The wholesale pattern is admitting an org once (subjectKind: ORG) so every listing that org owns — current and future — is admitted; per-listing admission remains for precise control. De-admission drains running installs exactly like a run-key revocation. For a hostile listing, pair the catalog kill (setPlayerCodeListingStatus(..., status: KILLED)) with the game-side fleet-wide runtime kill (playerComputeSetSwitch(scope: "listing", listingRef: ...)).

c.1a (Optional) Choose how claims confer grid ownership (P4a)

Games differ on how a player comes to own a grid. Configure the app's claim policy (requires manage_apps):

mutation {
setAppGridClaimPolicy(appId: "APP_ID", policy: SELF_CLAIM)
}

SELF_CLAIM (default) lets claimGridOwnership assign ownership directly; APPROVAL turns claims into requests your designated approvers accept; INVITE requires a standing invite. setAppGridClaimPolicy refuses MARKETPLACE_ONLY while paid grid sales are off the public API. Changing policy never revokes existing ownership rows.

c.2 (Optional) Bound player compute cost and take a markup

Player compute bills the player's own wallet, never the org — see Player wallets & billing. Two knobs belong to the studio:

  • Player policy (setPlayerWasmPolicy, manage_compute): per-player or cohort clamps at app_default / tier / grid / user scope, including unitsPerHour/unitsPerDay compute quotas, maxCompilesPerHour, and runtime budgets. Quotas protect world health independent of anyone's ability to pay.
  • Rate-card markup (setPlayerRateMarkup, manage_billing): basis points added on the platform's base player rates — the studio's usage revenue, always itemized separately in the player's spend history.

appPlayerUsage (view_compute_diagnostics) shows per-player consumption; appPlayerMarkupAccrued (view_billing) totals accrued markup income.

d. Choose where the app runs

Your app needs a Game API to serve runtime traffic, and there is one way to get it: the shared platform. Publish with publishAppToShared and the app is immediately served by that tier's shared Game API fleet, scoped by your appId. You do not provision VMs, choose a datacenter, or wait for a stack.

Availability: shared platform hosting is the only hosting model. Customer-provisioned environments — the developer sandbox (environmentClass: "dev_single") and multi‑VM dedicated stacks — were retired without replacement, and their mutations are no longer in the published SDL. If you are following an older guide that starts with createEnvironment, stop: see Shared environment & billing instead.

Whichever hosting you use, query the app's routing fields (gameApiUrl, splitMode, deploymentTarget) before a player joins so the client connects to the right Game API. See Shared environment & billing for the shared model and routing fields, and Loading an app's Game API for the client walkthrough.


2. How players get access

There are two paths — most public games use the first.

Open‑by‑default (zero‑friction)

If the app has a free + default tier (the one createApp made for you), then any player is automatically granted that tier the first time they connect — no per‑player admin action. The Game API resolves the entitlement on connect, mirrors it into the per‑tenant game database, and grants the player the app's default world‑grid permissions. From that point Buddy authorizes their spatial traffic.

Explicit grant (admin‑controlled)

For premium tiers, or for apps that opted out of open‑by‑default, the admin grants a specific player a specific tier:

mutation { grantAppAccess(input: { appId: "APP_ID", userId: "PLAYER_USER_ID", tierId: "TIER_ID" }) { appUserAccessId status } }

Revoke a player's access with revokeAppAccess(appId, userId). Granting and revoking both require manage_access_tiers.

The grant propagates to the Game API automatically (the matching grid permissions are provisioned server‑side via replica‑sync), so the player is authorized end to end — again with no direct database access.


3. The player's flow

  1. Register or log in through the Management API (register / login) — returns the identity session token. (Browser games can use the SDK's guest/anonymous auth; see CrowdyJS and the Build a game tutorial.)
  2. Mint an app token for this app — mintAppToken (native/same-origin) or the browser portal flow (client.portal). This is the app-scoped token used for gameplay; the session token is rejected by the Game API. The first mint auto-grants access on free tiers. See Portals & app-scoped tokens.
  3. Connect with CrowdyJS, the Unreal SDK, or a raw UDP client, pointed at the app's gameApiUrl (returned by the mint), sending the app-scoped token as the Bearer.
  4. Play — send actor, voxel, text, audio, and client‑event updates. The Game API and Buddy authorize each message against the player's tier and grid permissions. Refresh the app token (refreshAppToken) before it expires.

What happens under the hood

Management API Game API (per app `gameApiUrl`) Buddy (UDP)
────────────── ────────────────────────────── ───────────
app + access tier (free, default)
│ open-by-default on 1st connect (or explicit grantAppAccess)
└────────────────────────────► mirror app_user_access
+ grant default world-grid permissions ──► enforces app + grid
permissions per message

A player with an active tier on an app is authorized everywhere that matters — no manual grid setup, no database writes by the integrator.


The permission keys

runtimePermissions returns the assignable set. There are nine, and the last five are the ones people are surprised by.

KeyWhat it allowsApp scopeGrid scope
accessAccess an app runtime. Without it nothing else matters.
teleportTeleport within an app runtime
update_voxel_dataUpdate voxel data
use_voice_chatUse voice chat
write_server_codeAuthor and deploy player server code
run_server_codeRun admitted player server code
write_client_codeAuthor and deploy player client code
run_client_codeRun admitted player client code
use_studio_agentUse the app-scoped Agentic Crowdy Studio orchestrator

The generated default tier carries the first four. Everything below the line is opt-in, per tier.

run_server_code is what automations need. Enabling a player automation requires effective run_server_code, and so does invoking a server module export. A game whose damage, spawning or scoring runs as an automation will start, connect, replicate actors — and then quietly do nothing — if the tier its players land on omits that key. The symptom is not a permission error in the client; it is automations that never fire, game-model containers that never bind, and actor updates refused for actors nothing registered.

Two scopes, and the one that does not follow a tier edit

Every key except use_studio_agent is checked at two scopes, and code execution requires both:

  • App scope — the access tier the player is on.
  • Grid scope — a per-user, per-grid row, mirrored from the tier on the player's first connect.

That mirroring is one-way and one-time. It is what makes the happy path effortless, and it is the trap:

Editing a tier does not re-mirror to players who have already connected. They keep the grid-scope permissions they were given the first time they entered, however many times you update the tier afterwards. New players get the new set; existing ones do not.

So a permission added to a live game reaches its existing players only if you also grant it at grid scope:

mutation {
grantGridPermissions(input: {
appId: "APP_ID", gridId: "GRID_ID", userId: "USER_ID",
permissionKeys: ["run_server_code", "run_client_code"]
}) { __typename }
}

Two things about that call, both of which return a clear error rather than failing silently:

  • It is app-resident — send it to the app's own gameApiUrl (from mintAppToken), not the shared entry name. The shared name answers from whichever datacenter you land in and will tell you to reconnect.
  • It needs an app-scoped token, not an identity session token.

What a new app developer should check first

  1. The default tier's keys. appAccessTiers(appId) — is isDefault present, and does it carry what your game actually needs? Four keys is the generated default, not a recommendation.
  2. run_server_code at both scopes, if you use automations or player code at all.
  3. The Studio agent policy, if you use the agent: it is fail-closed on a rebuilt fleet and reports AGENT_APP_KILLED until an operator enables it for your app.
  4. The org runtime walletapp(appId){ runtimeStatus runtimeDenialReason }. A denied wallet stops server code independently of every permission above, with its own message.

Quick reference

GoalAdmin doesPlayers get access via
Open/public gamecreateApp (keeps the free default tier)Open‑by‑default on first connect
Invite‑only / paidcreateApp, then remove the free default tier and define paid tiersExplicit grantAppAccess per player (or after purchase)
Premium tier on an open gamecreateApp + createAccessTier (premium)Free tier auto; premium via grantAppAccess

See the GraphQL schema reference for exact inputs, and the Dev tier page for integration‑testing endpoints.