⌘K
↑↓ Navigate ↵ Open Esc Close
Tier 2EntityLeaderboardGlobal AppScript07 / 29

Persistent State: one app's four tiers, cheapest one that fits

Join-in-Progress is what fills the gap for state that lives only in tier 1; Leaderboards is the deep dive on tier 4; Entity Storage covers the entity services this app never reaches for, and which a different app's ladder would be built on.

State doesn't all need the same durability, and paying for more durability than a piece of data actually needs costs round-trips. This article walks the four tiers CursorParty uses, cheapest first, and why each piece of its state sits where it does.

These are this app's tiers, not brainCloud's. They're an inventory of what one realtime multiplayer app reached for, not a map of brainCloud storage. A turn-based game, an economy, or anything with a large queryable dataset would draw a different ladder from the same platform, most obviously by using the entity services this app never touches (see Entity Storage). What transfers is the habit rather than the list: choose per piece of data, and choose the cheapest tier that still meets the durability that data actually needs. Skipping that choice leads to two common mistakes, paying for server round-trips on state nobody outside the current match cares about, and assuming state survived when it was only ever held in one client's memory.

The four tiers, as this app uses them

Ordered by durability, cheapest first. The tier numbers are this article's shorthand for discussing one app's choices, not brainCloud terminology.

  1. Ephemeral, client-memory-only, relay-transported. The CursorParty splotch canvas (every paint splotch on screen) is never written to any brainCloud service. Every connected client, not just the host, appends to its own local splotch array as relay events arrive. The host's copy isn't authoritative; it's just the one used to resync a late joiner (see the Join-in-Progress article). A host disconnecting mid-match doesn't wipe the canvas or end the match: brainCloud's relay server auto-migrates ownership (MIGRATE_OWNER), and surviving clients, including the newly migrated host, still hold their own full copy. The canvas is lost only if every member leaves the match, and nothing needed it to survive past that point.
  2. Lobby-scoped extra data, genuinely persisted, but only for the lobby's lifetime. Anything passed in extra on updateReady/joinLobby/findOrCreateLobby* (color pick, region ping table, "present since start") is held server-side and round-trips through every subsequent lobby event to every member. This tier fits data every match member needs to see about each other, data that doesn't need to outlive the match.
  3. Portal-configured global properties: read-only config, not per-player state. AllLobbyTypes, the color palette, and splotch duration/lifespan are admin-managed values, fetched once via the Global App service (readProperties/readSelectedProperties). They're persistent in the sense that they don't live in the build, but they're config, not player data, and no client ever writes to them. This is brainCloud's Remote Config mechanism, and it's built for values that change occasionally: a designer retuning splotch duration between releases, not a number moving on a gameplay cadence. A write refreshes an app-wide cache on every API server, which is cheap when it happens rarely and wasteful when it doesn't. Shared state that changes rapidly belongs in Global/System Entities or unowned Custom Entities instead: ordinary documents you can rewrite as often as you like, and query and sort besides. See Global Properties for the read path and its parsing traps, and Entity Storage for both alternatives.
  4. True durable, cross-session persistence: leaderboards and player profile. This is the tier that needs a write API, and the one place a naive implementation causes real bugs. The reference apps write leaderboard results through a Cloud Code script instead of a direct client-side leaderboard post, because the cumulative points leaderboard would double-count if more than one client posted for the same match. Player identity fields, like a chosen display name, go through the Player State service directly, since there's no cross-client duplication risk there.

Code

Tier 2: lobby extra (C++)
// colorIndex/pings round-trip through every lobby event for every member
pBCWrapper->getLobbyService()->updateReady(lobbyId, isReady, buildExtraJson(), callback);
// buildExtraJson() => {"colorIndex":N,"rank":N,"pings":{"us-east":42,...}}
Tier 2: lobby extra (JavaScript)
makeExtraJson (colorIndex, presentSinceStart) {
  const extra = { colorIndex, presentSinceStart: !!presentSinceStart }
  if (Object.keys(this.state.pingData || {}).length > 0) extra.pings = this.state.pingData
  return extra
}
this.bc.lobby.updateReady(this.state.lobby.lobbyId, this.state.user.isReady, extraJson)
Tier 3: portal config (Godot GDScript)
var response: Dictionary = await AppState.bc.global_app_service.read_selected_properties(
    ["Colours", "AllLobbyTypes", "SplotchDuration"]
)
Tier 4: leaderboard write via Cloud Code, host-only (C++)
// host-only, once per round: a duplicate post from a second client would
// permanently inflate the cumulative points leaderboard
pBCWrapper->getScriptService()->runScript("PostMatchResults", payloadStr,
    new BCCallback(
        [round](const Json::Value &result) { applyLeaderboardResultsFromCloud(round, result["data"]["results"]); },
        [round](const std::string &msg) { /* log */ }));
Tier 4: leaderboard write via Cloud Code (JavaScript)
this.bc.script.runScript('PostMatchResults', scriptData, callback)
Tier 4: own rank, shared to peers via tier 2 (any SDK)
getGlobalLeaderboardView(leaderboardId, ...) has no "look up another player's rank" mode.
Each player fetches their own rank, then shares it with the lobby the same way as colorIndex:
by putting it in their own `extra` JSON.

Gotchas worth knowing up front

  • These four tiers are one app's ladder, not a platform taxonomy. They're what CursorParty happens to use. Read them as a worked example of choosing per piece of data, not as the set of options brainCloud offers; the entity services alone would add several more rungs.
  • Relay is transport, not storage. State that lives only in a client's memory is gone when the match ends. That's tier 1: a legitimate design choice when the state doesn't need to survive, not an oversight.
  • Global Properties are Remote Config, not a shared variable store. They're built for values read constantly and written occasionally, and each write invalidates an app-wide cache across every API server. If a value changes on a gameplay cadence rather than a release cadence, that's a Global/System Entity or an unowned Custom Entity, both designed to be written often. Note that tier 3 is really about who writes a value and how often rather than how long it survives: a global property is every bit as durable as tier 4.
  • Route match-result posting through Cloud Code, host-only, never a direct client write. A duplicate leaderboard post from more than one client silently corrupts cumulative totals. postScoreToLeaderboardOnBehalfOf isn't reachable from the client SDK at all, only from Cloud Code, which is what enforces this.
  • There's no "look up any player's leaderboard rank" call. If the UI needs to show every lobby member's rank, each client fetches its own and shares it via extra (tier 2), the same mechanism used for color and ping.