This article shows you how to combine:

  • a cloud code script (triggered by an API post-hook)

  • a user statistic (to keep track of when a user's data was last saved)

  • a custom entity collection (for the user backups)

to create a simple backup system for your app's user account data.

Note that this solution uses Custom Entities - and thus requires a brainCloud Plus plan. Refer to the linked article for how to update your plan -- https://help.getbraincloud.com/en/articles/3275913-changing-your-plan

Also note that usage of Custom Entities incurs Deep Data usage pricing. The cost of backing up player accounts varies according to the average amount of data per player, the time between backups, and the backup retention period. The good news is that this approach only backs up active accounts -- no storage is wasted on users that have left your app.

---

How it works

  • The script will run each time a user authenticates

  • The script will check to see when the user's data was last backed up - and if it was more than 24 hours ago - it will export a copy of the user's data and store in a custom entity

  • The user backups will be deleted automatically after 30 days

With this system in place - it will be possible to view old copies of a user's data - useful for debugging and/or recovering a user's account in the event of an app (or user) failure of some kind.

Caveat - restoring the backed-up data is a manual process. Devs will need to carefully copy-and-paste the data into the right entities for the user to restore the account. If you have questions, you can always contact brainCloud support for assistance.

---

Setting it up

We create the system in 5 steps:

  1. Create the playerBackup Custom Entity type

  2. Create the lastBackup User Statistic

  3. Create the authPosthook_backupPlayerAccount Cloud Code script

  4. Configure the API Hook

  5. Test

Step 1: Create the playerBackup Custom Entity type

Remember: you need to be on a Plus plan for Custom Entities.

It's very important that this solution use Custom Entities:

  • User Entities aren't suitable because they are owned by a user, and will be deleted if the user account is deleted... (and thus don't protect against accidental account deletion!). Also - User Entities do not support TTL.

  • Global Entities aren't suitable because they are meant for smaller collections of objects - normally less than 1000. They won't scale to the levels you need for this solution - where thousands (even millions!) of users may have multiple backups each within a 30 day period of time.

To create the Custom Entity collection:

  • Go to Design | Cloud Data | Custom Entities

  • Click [+ Create Entity Type]

  • Enter playerBackup for the Name

  • Enter identifier for the Identifier

  • Ensure that Owned is set to false. This is important so that the backup exists even if the user is deleted - and also so that your backups to do include other backups! Migrate should also be false.

  • You might want to set Notes to Daily backups of active players

  • The rest of the values can be left as is

  • Click [Create] to create the entity type collection

Then create three indexes for easy searching:

  • Create the profileId + saveDate index

    • Set Name to profileId_1_saveDate_1

    • Set Keys to { "data.profileId": 1, "data.saveDate": 1 }

  • Create the profileId + createdAt index

    • Set Name to profileId_1_createdAt_min1

    • Set Keys to { "data.profileId": 1, "createdAt": -1 }

  • Create an optional email + createdAt index

    • Set Name to email_1_createdAt_min1

    • Set Keys to { "data.emailAddress": 1, "createdAt": -1 }

    • Set Options to the JSON settings below

{
"partialFilterExpression": {
"data.emailAddress": {
"$exists": true
}
}
}

---

Step 2: Create the lastBackup User Statistic

The script uses a user statistic to record the last backup time in UTC milliseconds. Be sure to adjust the Maximum Value that statistic because it is not large enough for our purposes.

In the Design Portal:

  • Go to Design | Statistics Rules | User Statistics

  • Click the [+]

  • Enter lastBackup as the Name

  • Enter backup for the Category

  • Ensure that Type is set to Long

  • Enter 32503698000373 as the Maximum Value (that will get you to the year 3000!)

  • For Description, put Time in milliseconds of the user's last backup

  • The rest of the values can be left as is.

  • Click [Save] to create the statistic

---

Step 3: Create the authPosthook_backupPlayerAccount Cloud Code script

Now for the meat of the solution. The script will perform an export of the player's account, and store it in a custom entity for safe keeping.

Before performing the export, the script will first check to see when the last backup was performed - and if it was less than 24 hours ago (86,400,000 millis) - it will skip the backup. The playerBackup entity itself will be created with a TTL (Time to Live) of 30 days (i.e. 2,592,000,000 millis) - after which it will be automatically deleted. These values have been implemented as constants in the script and can be tweaked for your app's requirements.

To create the script:

  • Go to Design | Cloud Code | Scripts

  • Click Actions | Create Script

  • Enter authPostHook_backupPlayerAccount as the Script Name

  • Enter Perform daily player backup as the Description

  • You can leave the rest of the defaults as is

  • Then switch to the Editor tab - and copy-and-paste the following code:

  • Click [Save] to save the script

"use strict";

function getFieldSafely( parentObject, fieldName, valueIfAbsent ) {

if (parentObject.hasOwnProperty(fieldName)) {
return parentObject[fieldName];
} else {
return valueIfAbsent;
}

}

function main() {

var response = {};

const BACKUP_INTERVAL = 86400000; // 24 hours
const TTL = 2592000000; // 30 days
const ENTITY_COLLECTION = "playerBackup";
const LAST_BACKUP_STAT = "lastBackup";

var message = data.message;

// Use the authenticate response (i.e. data.message)
// to get some of the values we need
var currentTime = data.message.server_time;

var lastBackupMillis = getFieldSafely(
data.message.statistics, LAST_BACKUP_STAT, 0);

// Do we need to perform a backup?
if ( (currentTime - lastBackupMillis) < BACKUP_INTERVAL ) {

// No. Just exit early
message.backupMessage = "Skipping backup (recently backed up)";
response.status = 200;
response.data = message;
return response;

} else {

// Perform the backup

var customEntityProxy = bridge.getCustomEntityServiceProxy();
var playerStatisticsProxy =
bridge.getPlayerStatisticsServiceProxy();
var userProxy = bridge.getUserServiceProxy();

var optionsJson = { "includeEntities": true };
var profileId = data.message.profileId;

// Update the stat before we do export - to prevent
// concurrency issues.
var stat_lastBackup = {};
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+currentTime;
playerStatisticsProxy.processStatistics(stat_lastBackup);

var userExport =
userProxy.sysGetUserExport(profileId, optionsJson);

if (userExport.status == 200) {

var saveDate = new Date().toISOString().slice(0, 10);

// Format the identifier
var identifier = "";
var playerName = getFieldSafely(
data.message, "playerName", "" );
var emailAddress = getFieldSafely(
data.message, "emailAddress", null);
if ((playerName !== null) && (playerName !== "")) {
if ((emailAddress !== null) && (emailAddress !== "")) {
identifier = playerName +
" (" + emailAddress + ") - " + saveDate;
} else {
identifier = playerName +
" (" + profileId + ") - " + saveDate;
}
} else {
if ((emailAddress !== null) && (emailAddress !== "")) {
identifier = emailAddress + " - " + saveDate;
} else {
identifier = profileId + " - " + saveDate;
}
}

var dataJson = {
"profileId": profileId,
"saveDate" : saveDate,
"identifier": identifier,
"exportData": userExport.data
};

// Only write the email address if it is set...
if ((emailAddress !== "") && (emailAddress !== null)) {
dataJson.emailAddress = emailAddress;
}

var backupResult =
customEntityProxy.createEntity(
ENTITY_COLLECTION, dataJson, {"other": 1}, TTL, false);

if (backupResult.status == 200) {
message.backupMessage = "new backup entity " +
backupResult.data.entityId +
" saved in custom entity collection " +
ENTITY_COLLECTION;

} else {
message.backupMessage =
"Unexpected error backing up player data";
message.backupResult = backupResult;

// Set lastBackup time back to what it was previously
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+lastBackupMillis;
playerStatisticsProxy.processStatistics(stat_lastBackup);
}

} else {
message.backupMessage =
"Unexpected error exporting player data";
message.backupResult = userExport;

// Set lastBackup time back to what it was previously
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+lastBackupMillis;
playerStatisticsProxy.processStatistics(stat_lastBackup);
}

response.status = 200;
response.data = message;
return response;

}

}

main();

---

Step 4: Configure an API Hook to call the script

You need to hook this script up to the Authentication call. To do so:

  • Go to Design | Cloud Code | API Hooks

  • Click [+ Create]

  • Select Authenticate for Service

  • Select Authenticate for Operation

  • Select Post for Pre/Post

  • Select authPostHook_backupPlayerAccount for Script

  • Remove "key": "value" from the Params field (leave just the empty braces)

  • Click [Save]

---

Step 5: Test it!

We should now be all set.

Go to the API Explorer (Design | Cloud Code | API Explorer), select the Authenticate Service and Authenticate Operation - and click [Run].

You should see a new "backupMessage" field in the Authentication results that indicates that a backup of the player data was created.

Click [Run] again - and this time the message will say that the backup was skipped - because a recent backup already exists.

---

Searching for backups

Now that backups are being created, how do you search for them?

It is pretty simple:

  • Go to Monitoring | Global Monitoring | Custom Entities

  • Click on the playerBackup entity type

  • Click on the [Filter] button, and then enter a filter query - and click [Apply]

Here are a few simple examples:

Search for all backups for a given player (i.e. profileId):

{
"data.profileId": "aaa-bbb-ccc"
}

Search for backups for the specific player on a specific day:

{
"data.profileId": "aaa-bbb-ccc",
"data.saveDate": "2022-04-29"
}

Search for all backups for a given email address:

{
"data.emailAddress": "bruce@waynetech.com",
"data.saveDate": "2022-04-29"
}

Note - you can search for other player data of course - but be sure to create indexes that cover the fields of your search.

If you have questions - please reach out to our Support team!

Did this answer your question?