⌘K
↑↓ Navigate ↵ Open Esc Close
Tier 2: Data & Economy

Server-authoritative

Currency, catalogs, the profile fields brainCloud already tracks, files, and portal-tunable config all need to be trusted, so most of it routes through the same server-side pattern instead of client-computed math.

Tier 2Script05 / 29

Cloud Code, the pattern: one client call, a fully customizable script behind it

Virtual Currency is the first article that actually depends on this one. This is the only article in the series that covers writing cloud code itself; for everything beyond the client call itself (API hooks, webhooks, external auth scripts, matchmaking filters, entity versioning, MongoDB query syntax, the API-count pricing model), brainCloud's own Cloud Code introduction and Writing Scripts reference are the authoritative source, not this series.

brainCloud doesn't trust the client with anything that touches the economy: currency awards, purchases, quest claims, and cross-account writes all need to happen somewhere the client can't tamper with the outcome. Cloud code is that trusted place. The client asks for an outcome; a script running on brainCloud's own servers decides whether it happens and by how much. Every one of those requests can go through the same client call, ScriptService.RunScript(scriptName, jsonParams, success, failure). What makes it powerful is what's allowed to run behind it: a script can read and write any service the platform exposes, branch on whatever logic the game needs, and be customized as far as the game's own rules require. This article walks through that call and shows what a real script looks like on the other end, so the next three articles can refer to "a RunScript call" without re-explaining it each time.

How it works

  1. The client-side call never changes. Every cloud-code call in this codebase (award currency, claim an item, switch to a child profile) uses the same three arguments: a script name, a small params object serialized to JSON, and success/failure callbacks. What differs every time is the script behind that name, not the call itself.
  2. A script is a main() function with an implicit data global, run on Mozilla Rhino, not Node.js. The script's input arrives as an ambient data object; its return value becomes the client's response. main() must run, unconditionally, as the last line of the file. Rhino supports most modern syntax but not all of it (no class, async/await, or import/export); the full Writing Scripts reference has the exact coverage.
  3. Inside a script, bridge.getXxxServiceProxy() gives server-side access to the same services the client SDK exposes. These are in-process calls, not requests that cross the wire, which is also why they take plain JS objects instead of JSON strings.
  4. Failures return a 200 with an error field, not an exception. Every script checks .status !== 200 on each service call it makes and builds its own {error, message} response instead of throwing. The client's transport call still succeeds; the payload is what indicates whether the operation itself worked.
  5. RunScript is one of several ways a script gets invoked, not the only one. The same script format also runs from another script (bridge.callScript), a scheduled job, an S2S call, a webhook, an API hook that intercepts a built-in operation, or a matchmaking filter. This article only covers the client-invoked case; the Writing Scripts reference covers the rest.

Why write cloud code at all

  • Trust. Anything that shouldn't be decided by the client, an award, a debit, a claim, runs somewhere the client can't influence the outcome.
  • Fewer round trips. A script can chain several service calls into one request instead of the client making them one at a time.
  • Change behavior without shipping a client update. The logic lives on brainCloud's servers, so tuning a reward or fixing a bug doesn't require an app-store review cycle.
  • Triggers a client can't reach directly. Scheduled jobs, webhooks, and API hooks all run the same script format, for cases where nothing is waiting on a callback at all.

Where to actually write these scripts

Two real tools exist for this beyond the portal's own script editor. The brainCloud MCP server lets an AI assistant read the actual Cloud Code guide, look up a service method's exact signature before using it, then create, update, and run a script directly against a real app. That's the same workflow used to research and verify this entire series.

The brainCloud Cloud Code FS VS Code extension does the editor-side equivalent: it lists your brainCloud apps as folders in the file explorer, gives real create/edit/delete/rename on scripts with service code completion and validation, and syncs changes both ways between your local files and the portal, including a lock indicator so a Live app doesn't get edited by accident. It also works fully offline, editing and saving local .ccjs files with no connection at all, syncing on your terms once you're back online, rather than requiring a live link to the portal to get anything done. It goes further than plain offline editing, too: it maps git branches to specific brainCloud apps (develop to a Dev app, main to Production, for example), stored in a shareable .bcsync file committed alongside the code. Switching branches switches which app you're synced against automatically, which is what makes it practical to keep a dev app and a prod app's script source in the same repository, in sync, without hand-tracking which copy of a script belongs to which environment. None of this is specific to one team's setup or one kind of project; the extension is a generic brainCloud tool, and it's a real quality-of-life upgrade over the portal's own editor for day-to-day cloud-code work regardless of what you're building.

Beyond the client call: webhooks and scheduled scripts

RunScript is the client-invoked entry point, but the same main()/data script format also runs from triggers no client ever touches. A webhook lets an external system, a payment processor, a support tool, anything able to POST JSON, invoke a script directly over HTTP, without going through the brainCloud SDK at all; the incoming payload lands in the script's data exactly like a RunScript call would populate it. The Webhooks reference covers the endpoint shape and setup. A scheduled script runs on a timer the portal manages instead of in response to any call, suited to anything that should happen on a cadence rather than on demand: a nightly leaderboard reset, a subscription-expiry sweep, cleaning up stale data. Both are configured in the portal, not in client code, which is why neither shows up in any of this series' reference apps; the Writing Scripts reference covers the full setup for both.

Code

The client call: same three arguments everywhere (SDK source, BrainCloudScript.cs)
public void RunScript(
    string scriptName,
    string jsonScriptData,
    SuccessCallback success = null,
    FailureCallback failure = null,
    object cbObject = null)
A full script, top to bottomMarketplace/CloudCode/AwardUserCoins.ccjs
"use strict";

function main() {
  var response = {};

  var currencyService = bridge.getVirtualCurrencyServiceProxy();
  // get user level to award amount equal to user level
  var gamificationService = bridge.getGamificationServiceProxy();
  var playerStateService = bridge.getPlayerStateServiceProxy();
  var userGamificationData = gamificationService.readAllGamification(false);

  var CoinsMultiplierStatus = playerStateService.getUserStatus("coin_multiplier");
  var multiply = !isEmptyObject(CoinsMultiplierStatus.data);

  if (userGamificationData.status !== 200) {
      response.status = userGamificationData.status;
      response.error = "Failed to read user gamification data";
      return response;
  }

  if (userGamificationData.data && userGamificationData.data.xp) {
    var userLevel = userGamificationData.data.xp.experienceLevel;
    if (multiply) userLevel *= 2;

    var result = currencyService.awardCurrency("Coins", userLevel);
    if (result.status == 200) {
      response = result.data.currencyMap.Coins.balance;
    }
  }

  return response;
}

main();
A production quirk, documented right in the scriptMarketplace/CloudCode/BuyItem.ccjs:19-27
// brainCloud never marks a promotion as redeemed for items bought with virtual
// currency (only cash purchases verified through VerifyPurchase do that), so
// FetchStoreItems.ccjs would keep applying a one-time promo price forever. Record
// that this defId has now been bought at least once so it can strip the promo.
var attrKey = "promoRedeemed_" + data.defId;
var attrs = {};
attrs[attrKey] = "true";
playerStateService.updateAttributes(attrs, false);

Gotchas worth knowing up front

  • Client failure handling only tells you the transport failed. A RunScript failure callback fires for actual HTTP/auth failures. A script that runs fine but decides the operation shouldn't succeed (insufficient funds, item already owned) still returns 200 to the client. The success handler needs to check the response body's own error/success field; the callback firing does not mean the operation worked.
  • main() is called unconditionally at the bottom of the file. There is no export or entry-point wiring. The script runs top-to-bottom, and the trailing main(); is what actually executes it. Omitting that line, or leaving it inside a conditional, ships a script that silently does nothing.
  • Writing a script like it's Node.js is the fastest way to ship one that won't save. Rhino doesn't have class, async/await, Promise, import/export, or a spread operator inside a function call (foo(...args), though [...arr] in an array literal is fine). A developer used to modern Node or browser JS should assume none of that works here until checked, rather than finding out from a save error.
  • The attrs object in BuyItem.ccjs above is never JSON.stringify()-ed before it's passed to updateAttributes, and that's not a style choice. Service proxy calls inside a script are in-process, not a request over the wire, so a native JS object is expected. Stringifying one of these parameters first does not throw; it fails, mis-stores, or silently mis-routes, a much worse debugging session than a clear error.