# brainCloud Cloud Code — AI Reference Guide

> **Purpose**: This document is optimized for AI code generation. It contains precise rules, correct patterns, and explicit anti-patterns for writing brainCloud Cloud Code scripts. Follow all rules exactly. Do not infer or hallucinate API method names — use only those documented here or in the official API reference at https://docs.braincloudservers.com/api/cc/

---

## CRITICAL RULES (read before generating any code)

1. **JavaScript engine is Mozilla Rhino 1.8.0** — NOT Node.js, NOT V8. ES6 is only 62% supported.
2. **`class` keyword is NOT supported.** Use functions and closures instead.
3. **Spread operator in function calls is NOT supported.** Only works in array literals.
4. **Prefer `let` over `var`. Use `const` with caution** — it has limitations in Rhino.
5. **Every script MUST call `main()` at the bottom** as the final line.
6. **Every script MUST `return` a value from `main()`** — the return value is the script response.
7. **Always check `result.status == 200`** before using `result.data`. Never assume success.
8. **Cache service proxies** — call `bridge.getXxxServiceProxy()` once per script, store in a variable, reuse it.
9. **Input parameters come from the `data` object** — e.g. `data.playerId`, `data.amount`.
10. **`bridge` is the gateway to all brainCloud services** — never construct service objects directly.

---

## Supported & Unsupported ES6 Features

### Supported ✅
- `let`, `const` (use `let` preferentially)
- Arrow functions: `(x) => x * 2`
- Template literals: `` `Hello ${name}` ``
- Default parameters: `function foo(x = 0)`
- Rest parameters: `function foo(...args)`
- Destructuring: `let { a, b } = obj`
- `for...of` loops
- Generators
- Computed properties, shorthand methods

### NOT Supported ❌
- `class` keyword
- Block-scoped functions
- Spread in function calls: `foo(...args)` — INVALID
- Spread in array literals IS valid: `[...arr]` — OK
- `async`/`await`
- `Promise`
- `import`/`export`
- `Map`, `Set`, `WeakMap`, `WeakSet`

---

## Script Structure — Required Template

Every cloud code script must follow this structure exactly:

```javascript
"use strict";

// Optional: include utility scripts
bridge.include("UtilityFunctions.ccjs");

function main() {
    // Initialize response object
    let response = {
        success: false,
        data: null,
        error: null
    };

    // Validate required input parameters
    if (!data.requiredParam) {
        response.error = "Missing required parameter: requiredParam";
        return response;
    }

    // Get service proxies — cache them, do not call bridge.getXxx() more than once per service
    let entityService = bridge.getEntityServiceProxy();

    // Call service methods and check results
    let result = entityService.getEntity(data.entityId, data.entityType);

    if (result.status == 200) {
        response.success = true;
        response.data = result.data;
    } else {
        response.success = false;
        response.error = {
            status: result.status,
            reasonCode: result.reasonCode,
            message: result.statusMessage
        };
        bridge.logErrorJson("Operation failed", result);
    }

    return response;
}

// REQUIRED: Call main() as the final line
main();
```

---

## Global Objects

### `data`
Contains all parameters passed to the script by the caller.

```javascript
// If script called with: { "playerId": "abc123", "amount": 50 }
let playerId = data.playerId;  // "abc123"
let amount = data.amount;      // 50
```

### `bridge`
Gateway to all brainCloud services and utilities. Key methods:

| Method | Returns | API Count |
|--------|---------|-----------|
| `bridge.getPlayerStateServiceProxy()` | PlayerState service | FREE |
| `bridge.getEntityServiceProxy()` | Entity service | FREE |
| `bridge.getCustomEntityServiceProxy()` | CustomEntity service | FREE |
| `bridge.getLeaderboardServiceProxy()` | Leaderboard service | FREE |
| `bridge.getGlobalEntityServiceProxy()` | GlobalEntity service | FREE |
| `bridge.getHttpClientServiceProxy()` | HTTPClient service | FREE |
| `bridge.getSessionForProfile(profileId)` | Session object | FREE |
| `bridge.include("Script.ccjs")` | void | FREE |
| `bridge.logInfoJson(msg, data)` | void | 0.5 counts |
| `bridge.logDebugJson(msg, data)` | void | 0.5 counts |
| `bridge.logWarningJson(msg, data)` | void | 0.5 counts |
| `bridge.logErrorJson(msg, data)` | void | 0.5 counts |

---

## API Response Format

All brainCloud service calls return this structure:

```javascript
// Success
{
    "status": 200,
    "data": { /* response payload */ }
}

// Error
{
    "status": 400,          // or 403, 500, etc.
    "reasonCode": 40332,    // numeric, use for programmatic error handling
    "statusMessage": "..."  // human-readable, for logging only — do NOT parse this in client code
}
```

**Rule**: Always branch on `result.status == 200`. Never use `statusMessage` for logic.

---

## Service Proxy Caching — Critical Pattern

```javascript
// ❌ WRONG — allocates a new proxy object each call (inefficient)
bridge.getEntityServiceProxy().getEntity(id, type);
bridge.getEntityServiceProxy().updateEntity(id, type, data, acl, version);

// ✅ CORRECT — cache proxy, reuse
let entityService = bridge.getEntityServiceProxy();
entityService.getEntity(id, type);
entityService.updateEntity(id, type, data, acl, version);
```

---

## Session Types

### Client Session (default)
When a script is called by an authenticated client, the session context is inherited automatically. Service calls automatically operate on the authenticated player — no explicit session needed.

```javascript
function main() {
    let playerStateService = bridge.getPlayerStateServiceProxy();
    // Automatically operates on the calling player
    let result = playerStateService.readPlayerState();
    return { success: result.status == 200, data: result.data };
}
main();
```

### Server Session (scheduled jobs / webhooks)
No player is associated. To perform player-specific operations, use `bridge.getSessionForProfile()`.

```javascript
function main() {
    // Get a session for a specific player
    let playerSession = bridge.getSessionForProfile(data.profileId);
    
    // Pass session to service proxy
    let playerStateService = bridge.getPlayerStateServiceProxy(playerSession);
    let result = playerStateService.readPlayerState();
    // Session expires automatically after 20 minutes — no manual cleanup needed
    return { success: result.status == 200 };
}
main();
```

---

## Common Service Methods

### PlayerState Service
```javascript
let playerStateService = bridge.getPlayerStateServiceProxy();

playerStateService.readPlayerState();
playerStateService.updatePlayerName(name);
playerStateService.deletePlayer();
```

### Entity Service (User-scoped entities)
```javascript
let entityService = bridge.getEntityServiceProxy();

// Create
entityService.createEntity(entityType, dataObj, acl);
// acl example: { "other": 1 }  — 0=hidden, 1=read-only, 2=read-write

// Read
entityService.getEntity(entityId, entityType);

// Update — ALWAYS pass version to prevent concurrent update conflicts
entityService.updateEntity(entityId, entityType, dataObj, acl, version);
// Pass version -1 ONLY for non-concurrent admin scripts (bypasses optimistic lock)

// Delete
entityService.deleteEntity(entityId, entityType, version);

// Query entities of a type
entityService.getEntitiesbyType(entityType);
```

### GlobalEntity Service (App-scoped entities)
```javascript
let globalEntityService = bridge.getGlobalEntityServiceProxy();

globalEntityService.createEntity(entityType, timeToLive, acl, dataObj);
globalEntityService.readEntity(entityId);
globalEntityService.updateEntity(entityId, version, dataObj, acl, timeToLive);
globalEntityService.deleteEntity(entityId, version);
```

⚠️ **GlobalEntity performance degrades beyond ~1,000 entities.** Use CustomEntity for larger datasets.

### CustomEntity Service (Plus Plan — recommended for large data)
Must be pre-defined in Portal at: **Design > Cloud Data > Custom Entities**

```javascript
let customEntityService = bridge.getCustomEntityServiceProxy();

// Create
customEntityService.createEntity(entityType, dataObj, acl, timeToLive, owned);

// Paginated query with MongoDB where/sort
customEntityService.getEntityPage(entityType, whereClause, sortClause, maxResults);

// Update
customEntityService.updateEntity(entityType, entityId, version, dataObj, acl, timeToLive);

// Delete
customEntityService.deleteEntity(entityType, entityId, version);
```

### Leaderboard Service
```javascript
let leaderboardService = bridge.getLeaderboardServiceProxy();

leaderboardService.postScoreToLeaderboard(leaderboardId, score, jsonData);
leaderboardService.getGlobalLeaderboardPage(leaderboardId, sort, startIndex, endIndex);
leaderboardService.getPlayerScore(leaderboardId, versionId);
```

### HTTPClient Service (External API calls)
⚠️ **External services MUST be registered first** in the Portal at: **Design > Cloud Code > Web Services**

```javascript
let httpClient = bridge.getHttpClientServiceProxy();

// GET
let result = httpClient.getResponseJson({
    serviceCode: "registeredServiceCode",  // The code assigned in Portal
    path: "/endpoint",
    query: { param: value },
    headers: { "Authorization": "Bearer " + token }
});
// result.status = brainCloud status (200 = call succeeded)
// result.data.statusCode = HTTP status from external service
// result.data.json = parsed JSON response body

// POST
httpClient.postJsonResponseJson({
    serviceCode: "registeredServiceCode",
    path: "/endpoint",
    headers: { "Content-Type": "application/json" },
    json: { key: value }
});

// PUT
httpClient.putJsonResponseJson({ serviceCode, path, json });

// DELETE
httpClient.deleteResponseJson({ serviceCode, path });
```

**HTTPClient error handling — check TWO statuses:**
```javascript
let result = httpClient.getResponseJson({ serviceCode: "myAPI", path: "/data" });

if (result.status == 200) {
    // brainCloud successfully made the HTTP call
    if (result.data.statusCode == 200) {
        // External service returned success
        let payload = result.data.json;
    } else {
        // External service returned an error (e.g. 404, 500)
        bridge.logErrorJson("External API error", result.data);
    }
} else {
    // brainCloud failed to make the HTTP call (service not registered, network issue, etc.)
    bridge.logErrorJson("HTTP call failed", result);
}
```

---

## Script Inclusion

Scripts can include shared utility scripts. brainCloud pre-processes includes at server startup — zero runtime cost.

```javascript
// In main script
bridge.include("UtilityFunctions.ccjs");
bridge.include("ValidationUtils.ccjs");
// Both are available — duplicates are auto-deduplicated

function main() {
    // Functions from included scripts are available here
    let isValid = validateInput(data.amount);
}
main();
```

**Utility script pattern** (not directly callable, `clientCallable: false`):
```javascript
"use strict";

// Do NOT call main() — this is a library file
// Do NOT set clientCallable: true

function validateInput(value, min, max) {
    return value !== undefined && value >= min && value <= max;
}

function formatCurrency(amount) {
    return "$" + amount.toFixed(2);
}
```

---

## API Hooks

Hooks automatically trigger scripts before/after specific brainCloud API calls. Configure in Portal at: **Design > Cloud Code > API Hooks**

⚠️ **Hooks do NOT trigger for API calls made from within Cloud Code scripts** — only for direct client or S2S calls.

### Pre-Hook
Runs before an API call. Can modify parameters or abort the call.

`data` object available in hook script:
```javascript
data.service    // Service name (mixedCase, e.g. "entity")
data.operation  // Operation name (UPPERCASE, e.g. "CREATE")
data.message    // Original parameters sent by client
data.parms      // Static parameters configured in Portal
```

Return format:
```javascript
{
    status: 200,                  // 200 = proceed, any other = abort
    reasonCode: 40001,            // Used if aborting
    errorMessage: "Reason",       // Used if aborting
    messageOverride: { }          // Modified parameters — replaces data.message if provided
}
```

Example — inject a field before entity creation:
```javascript
function main() {
    let result = { status: 200 };
    result.messageOverride = JSON.parse(JSON.stringify(data.message));
    result.messageOverride.data.createdBy = "system";
    return result;
}
main();
```

### Post-Hook
Runs after a successful API call. Can modify the returned data.

`data` object:
```javascript
data.service         // Service name
data.operation       // Operation name
data.callingMessage  // Original parameters
data.message         // API result data
data.parms           // Hook configuration parameters
```

Return format:
```javascript
{
    status: 200,
    data: { }    // Replacement result — omit or return null to leave result unchanged
}
```

### Post-Fail-Hook
Runs after an API call fails. Can convert a failure to a success or change the error.

`data` object:
```javascript
data.service         // Service name
data.operation       // Operation name
data.callingMessage  // Original parameters
data.errorData       // { reasonCode, errorMessage }
data.parms           // Hook configuration parameters
```

---

## Entity Versioning (Optimistic Locking)

brainCloud uses version numbers to prevent concurrent write conflicts.

**Rules:**
- Version starts at `1` on creation
- Increments automatically on each successful update
- You MUST pass the current version on update calls
- If versions don't match, update fails with a version conflict error
- Pass `-1` to skip version check — ONLY safe for non-concurrent operations

```javascript
function main() {
    let response = {};
    let entityService = bridge.getEntityServiceProxy();

    // Step 1: Read to get current version
    let readResult = entityService.getEntity(data.entityId, "playerData");
    if (readResult.status != 200) {
        response.error = "Could not read entity";
        return response;
    }

    let entity = readResult.data;
    let currentVersion = entity.version;  // e.g. 5

    // Step 2: Modify data
    // IMPORTANT: Deep copy if modifying nested objects
    let updatedData = JSON.parse(JSON.stringify(entity.data));
    updatedData.score += data.points;

    // Step 3: Update with version — fails if another process updated since we read
    let updateResult = entityService.updateEntity(
        data.entityId,
        "playerData",
        updatedData,
        null,
        currentVersion  // Must match current version
    );

    if (updateResult.status == 200) {
        response.success = true;
        response.newVersion = updateResult.data.version;  // Now 6
    } else {
        response.success = false;
        response.error = "Version conflict — another update occurred";
        response.shouldRetry = true;  // Signal client to retry
    }

    return response;
}
main();
```

**When to use version -1 (bypass):**
✅ Administrative / scheduled scripts with no concurrency
✅ Cache updates where last-write-wins is acceptable
❌ Never for currency, inventory, guild data, or any shared resource

---

## MongoDB Query Syntax

Used with CustomEntity `getEntityPage` and GlobalEntity queries.

```javascript
// All user data fields are accessed under "data.*"
// Top-level fields: "entityType", "entityId", "createdAt", "updatedAt"

// Equality
{ "data.status": "active" }

// Comparison
{ "data.level": { "$gt": 10 } }     // greater than
{ "data.level": { "$gte": 10 } }    // greater than or equal
{ "data.score": { "$lt": 1000 } }   // less than
{ "data.score": { "$lte": 1000 } }  // less than or equal
{ "data.status": { "$ne": "banned" } }  // not equal

// In / Not In
{ "data.role": { "$in": ["admin", "moderator"] } }
{ "data.type": { "$nin": ["weapon", "armor"] } }

// Logical operators
{ "$and": [ { "data.level": { "$gte": 10 } }, { "data.region": "US" } ] }
{ "$or": [ { "data.vip": true }, { "data.level": { "$gte": 50 } } ] }
{ "$nor": [ { "data.banned": true }, { "data.suspended": true } ] }
{ "data.price": { "$not": { "$gt": 1.99 } } }

// Field existence
{ "data.premiumExpiry": { "$exists": true } }
{ "data.premiumExpiry": { "$exists": false } }

// Regex
{ "data.username": { "$regex": "^player", "$options": "i" } }

// Array element match
{ "data.scores": { "$elemMatch": { "$gt": 50, "$lt": 100 } } }

// Combined example
let whereClause = {
    "$and": [
        { "$or": [ { "data.vip": true }, { "data.level": { "$gte": 50 } } ] },
        { "data.region": "North America" },
        { "data.banned": { "$ne": true } }
    ]
};

let sortClause = { "data.score": -1 };  // -1 = descending, 1 = ascending

let result = customEntityService.getEntityPage("playerProgress", whereClause, sortClause, 50);
```

⚠️ **MongoDB aggregation pipeline is NOT supported.**

---

## API Counts — Pricing Reference

| Operation | API Count |
|-----------|-----------|
| Direct client API call | 1.0 |
| Calling a Cloud Code script | 1.0 |
| First 2 API calls inside a script | FREE |
| Each additional API call inside a script | 0.5 |
| `bridge.getXxxServiceProxy()` | FREE |
| `bridge.include()` | FREE |
| `bridge.getSessionForProfile()` | FREE |
| `bridge.logInfoJson()`, `logDebugJson()`, etc. | 0.5 each |

**Cost optimization**: Wrap multiple API calls in a single script. 4 direct client calls = 4 counts. Same 4 calls from a script = 1 (script) + 0 (calls 1-2) + 0.5 + 0.5 = 2 counts.

---

## Data Storage Decision Guide

| Need | Use |
|------|-----|
| Per-player data, simple queries | **Entity** service (`bridge.getEntityServiceProxy()`) |
| App-wide data, under ~1,000 records | **GlobalEntity** service (`bridge.getGlobalEntityServiceProxy()`) |
| Large datasets, fast indexed queries, cross-user queries | **CustomEntity** service (`bridge.getCustomEntityServiceProxy()`) — requires Plus Plan and Portal setup |
| Atomic numeric values (scores, counters) | **Statistics** service |
| Simple key-value per player | **Attributes** service |
| App-wide config constants | **GlobalProperties** (read-only at runtime) |

---

## Error Handling Patterns

### Basic pattern
```javascript
let result = service.method(params);
if (result.status != 200) {
    bridge.logErrorJson("Description of what failed", result);
    return { success: false, reasonCode: result.reasonCode, error: result.statusMessage };
}
// Use result.data here
```

### Retry pattern for version conflicts
```javascript
function updateWithRetry(entityId, entityType, updateFn, maxRetries) {
    let entityService = bridge.getEntityServiceProxy();
    for (let i = 0; i < maxRetries; i++) {
        let readResult = entityService.getEntity(entityId, entityType);
        if (readResult.status != 200) return readResult;

        let entity = readResult.data;
        updateFn(entity.data);  // Caller mutates the data object

        let updateResult = entityService.updateEntity(
            entityId, entityType, entity.data, null, entity.version
        );
        if (updateResult.status == 200) return updateResult;
        // Otherwise loop and retry
    }
    return { status: 409, statusMessage: "Max retries exceeded" };
}
```

---

## Logging

Use structured JSON logging. All log calls cost 0.5 API counts.

```javascript
bridge.logInfoJson("Player purchased item", { playerId: data.playerId, itemId: data.itemId });
bridge.logDebugJson("Processing state", { step: "validation", data: data });
bridge.logWarningJson("Low balance warning", { balance: wallet.balance });
bridge.logErrorJson("Payment failed", { result: result, input: data });
```

---

## Script Timeouts

- Default: **10 seconds**
- Maximum configurable: **20 seconds** (set in Portal script metadata)
- Client SDK default timeout: **15 seconds**
- ⚠️ If script timeout > client timeout, client may retry while script is still running — avoid this

Background/scheduled scripts can run longer since no client is waiting.

---

## Complete Working Example

**Script: DeductBalance.ccjs** — deducts an amount from a player's wallet entity with version safety.

```javascript
"use strict";

function main() {
    let response = {
        success: false,
        data: null,
        error: null
    };

    // Validate inputs
    if (!data.walletEntityId || !data.amount || data.amount <= 0) {
        response.error = "Missing or invalid parameters: walletEntityId, amount";
        return response;
    }

    let entityService = bridge.getEntityServiceProxy();

    // Read wallet
    let walletResult = entityService.getEntity(data.walletEntityId, "playerWallet");
    if (walletResult.status != 200) {
        response.error = { message: "Failed to read wallet", reasonCode: walletResult.reasonCode };
        bridge.logErrorJson("Wallet read failed", walletResult);
        return response;
    }

    let wallet = walletResult.data;

    // Check balance
    if (wallet.data.balance < data.amount) {
        response.error = "Insufficient balance";
        return response;
    }

    // Deep copy before modifying
    let updatedWalletData = JSON.parse(JSON.stringify(wallet.data));
    updatedWalletData.balance -= data.amount;
    updatedWalletData.lastTransaction = Date.now();

    // Update with version check
    let updateResult = entityService.updateEntity(
        data.walletEntityId,
        "playerWallet",
        updatedWalletData,
        null,
        wallet.version
    );

    if (updateResult.status != 200) {
        response.error = { message: "Failed to update wallet", reasonCode: updateResult.reasonCode };
        response.shouldRetry = (updateResult.reasonCode == 40332); // Version conflict
        bridge.logErrorJson("Wallet update failed", updateResult);
        return response;
    }

    bridge.logInfoJson("Balance deducted", {
        amount: data.amount,
        newBalance: updatedWalletData.balance
    });

    response.success = true;
    response.data = {
        newBalance: updatedWalletData.balance,
        version: updateResult.data.version
    };

    return response;
}

main();
```

---

## Quick Reference: Common Anti-Patterns to Avoid

| ❌ Wrong | ✅ Correct |
|---------|-----------|
| `class MyService { }` | Use plain functions |
| `foo(...args)` spread in call | Pass args individually |
| `bridge.getEntityServiceProxy().getEntity(...)` (inline, repeated) | Cache proxy in variable |
| Checking `result.data` without status check | Always check `result.status == 200` first |
| Parsing `result.statusMessage` in logic | Use `result.reasonCode` for programmatic checks |
| `updateEntity(..., -1)` on shared data | Pass `entity.version` for concurrent-safe updates |
| Using `Promise`, `async`, `await` | Not supported — all calls are synchronous |
| Forgetting `main()` at bottom | Always call `main()` as the last line |
| Returning from script without a `return` in `main()` | Always return a value from `main()` |

---

## References

- Full API Reference: https://docs.braincloudservers.com/api/introduction
- Cloud Code Reference: https://docs.braincloudservers.com/api/cc/
- Reason Codes: https://docs.braincloudservers.com/api/appendix/reasonCodes
- MongoDB Query Reference: https://docs.braincloudservers.com/api/appendix/mongodbWhereQueries
- API Hooks: https://docs.braincloudservers.com/api/cc/writingscripts/apihooks
- Custom Entities: https://help.getbraincloud.com/en/articles/3754150
- ES6 Rhino Compatibility: https://mozilla.github.io/rhino/compat/engines.html

---

*brainCloud Cloud Code AI Reference — optimized for AI code generation*
*© 2026 bitHeads inc. All rights reserved.*
