Parent–Child Accounts: one login, multiple isolated save files
Authentication covers Identity, a different concept from the Profile-switching this article describes. Virtual Currency covers BuddyBling, the child-scoped currency this architecture makes possible.
brainCloud's parent–child app relationship is what makes bitBuddies' single most distinctive design pattern possible: one player login (the "Parent" app) owns top-level currency and progression, while each individual bitBuddy the player owns lives in its own isolated Child app profile, with its own stats, its own currency (BuddyBling), its own inventory. The client never merges these. Every call that touches a specific buddy explicitly states which child app and which child profile it means. The pattern fits any game that needs many independently-progressing sub-entities under one player account (pets, bases, characters, save slots), without hand-rolling that isolation.
How it works
- Fetch every linked child profile with one script call, right alongside the other post-login calls. No separate "does this player have any buddies yet" check is needed first.
- Every child-scoped script name is namespaced under "child/…", and profile-creation scripts nest one level further under "child/lootboxes/…". This is a real, verified naming scheme, visible as literal string constants in the client, not a convention described for the article's sake.
- Every child-scoped call explicitly passes which child app it means. A single hardcoded child app id (APP_CHILD_ID) answers "which app is a bitBuddy," and most calls additionally pass a profileId that answers "which specific buddy." Together, those two values are the client's half of the parent/child switch.
- An empty child list is a normal state, not an error. A brand-new player has zero child profiles until they open their first loot box. The response handler checks for that and just refreshes the (empty) screen instead of treating it as a failure.
- The actual profile switch happens server-side, inside cloud code, through IdentityService. The client never touches this directly. Every child-scoped script calls identityProxy.switchToChildProfile(profileId, appId, forceCreate) to make the session act as that child's profile for the rest of the script, does its work, then calls identityProxy.switchToParentProfile(parentIdentifier) to switch back before returning. That pair is the real mechanism behind "one login, multiple isolated save files": the session hops between Profiles mid-script, it doesn't merge them.
- A first-time switch and a returning switch are different calls, not a flag. switchToChildProfile(null, appId, true) (forceCreate=true, no profile id) creates a brand-new child profile. switchToChildProfile(profileId, appId, false) switches to one that already exists. switchToSingletonChildProfile(appId, forceCreate) is a shortcut for apps where a parent only ever has one child profile in a given child app, and it fails with a specific reason code if more than one exists.
Code
Dictionary<string, object> scriptData = new Dictionary<string, object> { { "childAppId", BitBuddiesConsts.APP_CHILD_ID } };
Wrapper.ScriptService.RunScript
(
BitBuddiesConsts.GET_CHILD_ACCOUNTS_SCRIPT_NAME, // "child/getChildProfiles"
scriptData.Serialize(),
HandleSuccess("Getting Child Accounts Success", OnGetChildAccounts),
HandleFailure("Getting Child Accounts Failed", OnFailureCallback)
);private void OnGetChildAccounts(string jsonResponse)
{
var data = jsonResponse.Deserialize("data", "response");
var children = data?.GetJSONArray("children");
if (children == null || children.Length == 0)
{
StateManager.Instance.RefreshScreen(); // new player, no buddy yet, not an error
_isProcessing = false;
return;
}
ReadChildrenInfo(children);
GetChildItemCatalog();
// ...
}private void GetChildItemCatalog()
{
var scriptData = new Dictionary<string, object>
{
{ "childAppId", BitBuddiesConsts.APP_CHILD_ID },
{ "profileId", GameManager.Instance.AppChildrenInfos[0].profileId }
};
Wrapper.ScriptService.RunScript(
BitBuddiesConsts.GET_CHILD_ITEM_CATALOG_SCRIPT_NAME, // "child/getChildItemCatalog"
scriptData.Serialize(),
HandleSuccess("GetItemCatalog Success", OnGetItemCatalog),
HandleFailure("GetItemCatalog Failed", OnFailureCallback));
}public const string GET_CHILD_ACCOUNTS_SCRIPT_NAME = "child/getChildProfiles";
public const string ADD_CHILD_ACCOUNT_SCRIPT_NAME = "child/addChildAccount";
public const string AWARD_STARTER_BUDDY_SCRIPT_NAME = "child/lootboxes/addStarterChildAccount";
public const string AWARD_BASIC_LOOTBOX_SCRIPT_NAME = "child/lootboxes/addBasicChildAccount";
public const string AWARD_RARE_LOOTBOX_SCRIPT_NAME = "child/lootboxes/addRareChildAccount";
public const string UPDATE_CHILD_PROFILE_NAME_SCRIPT_NAME = "child/updateChildAccountName";
public const string DELETE_CHILD_PROFILE_SCRIPT_NAME = "child/deleteChildProfile";
public const string CLAIM_CHILD_ITEM_SCRIPT_NAME = "child/claimMouseMerchantItem";
public const string APP_CHILD_ID = "50974";const MASTER_PARENT_NAME = "Master";
function switchToChildAccount(profileId, appId, identityProxy) {
var switchResponse = identityProxy.switchToChildProfile(profileId, appId, false);
return switchResponse.status == 200;
}
function switchToChildAppSafely(appId, identityProxy) {
var switchResponse = identityProxy.switchToSingletonChildProfile(appId, false);
if (switchResponse.status != 200) {
switch (switchResponse.reason_code) {
case 40208:
// no parent-child relationship created yet, force it
switchResponse = identityProxy.switchToSingletonChildProfile(appId, true);
break;
case 40372:
// run state disabled, nothing to do
break;
}
}
return switchResponse.status == 200;
}function updateBuddyLevel(profileId, amountToIncrease, childAppId, identityProxy, playerStatProxy, playerStateProxy) {
var switchToChildResult = identityProxy.switchToChildProfile(profileId, childAppId, false);
if (switchToChildResult) {
// ... read the child's XP, increment it, check for a level-up ...
// Return to parent
identityProxy.switchToParentProfile(MASTER_PARENT_NAME);
}
}Gotchas worth knowing up front
- APP_CHILD_ID is a single hardcoded constant, not per-buddy. All bitBuddies share one child app. What's per-buddy is the profileId passed alongside it. Confusing the two ("which app" versus "which profile within that app") sends a call to the wrong buddy.
- switchToChildProfile and switchToParentProfile change what the current session is, not what data a query targets. Every service call made between those two lines operates on the child's Profile: reading stats, incrementing XP, checking currency, all of it. Forgetting the switchToParentProfile call at the end of a script leaves the session pointed at the child profile for anything that runs after it.
- switchToSingletonChildProfile's failure reason code tells you what to do next, not just that it failed. 40208 means no child profile exists yet for that app, the fix is retrying with forceCreate=true, not treating it as an error. 40372 means the app's run state is disabled, and the correct response is to do nothing, not retry.