Parse Push Experiments: A/B Testing Best Practices and the Screencast

We recently launched Push Experiments, which lets you conduct A/B tests on push notification campaigns to identify the most engaging message variant. With Push Experiments, we wanted to make it easy to run successful A/B tests. In this post, we’ll discuss some of the statistical techniques we’re using behind the scenes. And, catch a screencast showing you the ins and outs of the new tool below.

Parse Push Experiments in Action

Ready to try Parse Push Experiments?  Watch the screencast here.  Or, read on for A/B Testing best practices.

 

What makes an A/B test successful?

We can say that an A/B test succeeds whenever we get a precise, correct answer to the question that originally motivated us to run the test. In other words, a good platform for A/B testing should try to prevent two kinds of failure:

(1): We should rarely get a result that leaves the answer to our question in doubt.
(2): We should rarely get an answer that seems precise, but is actually incorrect.

Parse Push Experiments uses three strategies to prevent these two kinds of failure:

  1. Encourage developers to ask precise questions that can be answered unambiguously.
  2. Prevent developers from reaching wrong conclusions by always reporting results along with a margin of error.
  3. Ensure that most A/B tests will give a precise answer by suggesting the minimum number of users that must be included in an A/B test in order to reasonably expect accurate results.

Step 1: Asking Precise Questions

Here’s one of the most important things you can do while running A/B tests: Commit to the metric you’re testing before gathering any data. Instead of asking questions like, “Is A better than B?”, the Push Experiments platform encourages you to ask a much more precise question: “Does A have a higher open rate than B?”

The distinction between these two questions may seem trivial, but asking the more precise question prevents a common pitfall that can occur in A/B testing. If you allow yourself to choose metrics post hoc, it’s almost always possible to find a metric that makes A look better than B. By committing up front to using open rates as the definitive metric of success, you can rest assured that Push Experiments will produce precise answers.

Step 2: Acknowledging Margins of Error

Once you’ve chosen the question you’d like to answer, you can start gathering data. But the data you get might not be entirely representative of the range of results you’d get if you repeated the same test multiple times. For example, you might find that A seems to be better than B in 25% of your tests, but that B seems to be better than A in the other 75%.

As such, when reporting the difference between the A and B groups (we’ll call this difference the lift), it’s important to emphasize the potential for variability in future results by supplementing the raw result with a margin of error. If you have an A/B test that has a lift of +1% and a margin of error that is between -1% and +3%, you should report that your A/B test’s results were inconclusive. If you simply reported a +1% change, your results would be misleading and might set up unrealistic expectations about the success of your push strategy in the future. By reporting a range of values that should contain the true answer to your question (this range is what a statistician would call a 95% confidence interval), you can help to ensure that anyone reading a report about your A/B test will not reach premature conclusions.

At Parse, we determine margins of error for open rate data using a calculation called the Agresti-Caffo method. When you’re working with push notification open rates, the Agresti-Caffo method produces much more reliable margins of error than naive methods like normal approximations.

In addition to automatically calculating margins of error using the Agresti-Caffo method, the Push Experiments platform only reports results after it’s become clear that either A offers a lift over B or that B offers a lift over A — helping to further protect you from reaching premature conclusions. Until there’s enough data to determine a clear winner, the Push Experiments dashboard will report that there’s still uncertainty on whether A or B is more successful.

Step 3: Choosing the Right Sample Size

Given that the Push Experiments platform will always report results with a margin of error, you’ll want to try to make that margin smaller in order to draw definite conclusions from more of your tests. For example, if you think that your A group will show a lift of 1% over your B group, you’ll want to make sure you gather enough data to ensure your margin of error will be smaller than 1%.

The process of picking a sample size that ensures that your margin of error will be small enough to justify a definite conclusion is called power analysis. The Push Experiments platform automatically performs a power analysis for your A/B test based on the historical open rates for your previous push notifications. To simplify the process, we provide suggested sample sizes based on the assumption that you’ll be trying to measure lifts at least as large as 1% with your A/B tests.

Only running A/B tests with carefully chosen sample sizes makes it much more likely that your A/B tests will succeed. If you select a sample size that’s much smaller than the size we suggest, you should expect that many of your A/B tests will lead to inconclusive results.

Putting It All Together

We believe the combination of precise questions, clean statistics and careful choice of sample size is essential for running a successful A/B test. You can achieve that with Parse Push Experiments, and we hope this look into the statistical methods behind our platform will help you do it.

 

johnmyleswhite
November 20, 2014

Attack of the Clones

A long time ago, developers had to painstakingly create multiple versions of their Parse apps in order to manage development and production environments; but no more! After long hours of research, we’ve cracked the app genome and we’re happy to announce the brand new ability to clone apps.

From the newly redesigned Parse dashboard, you’ll now be able to quickly clone an app that you’ve made or that has been shared with you by a colleague. This will clone the schema, cloud code, security and white-labeling settings, and config parameters; everything about the app except the data and the background jobs schedules. (We know cloning data is a highly requested capability, and we’re exploring ways to do it in a limited fashion.)

clone-dashboard

If you want to customize what goes into your new app, you can head over to the settings page and select exactly what you need in your clone — a clone to order, if you will!

Screen Shot 2014-11-13 at 1.01.39 PM

We’re excited to see how you’ll use this new feature and how we can improve similar workflows in the future. After all, large apps require a lot of effort from many developers, and we want to make this as easy and simple as possible with Parse.

If there’s a feature that would make development on Parse easier for your team, join the discussion here!

Mattieu Gamache-Asselin
November 17, 2014

Let the Browser Handle Your UI Logic for You

At Parse, we’re in the middle of refreshing our frontend stack, and much of that has meant reevaluating the way we write browser-bound code. We’ve begun developing new custom inputs such as date pickers, dropdowns, or toggles in a way that leverages default browser behaviors to let us write less JavaScript (and reduce opportunities for bugs). Nearly every web developer has seen or implemented a date input that looked like a calendar, and chances are it contained plenty of logic to ensure that only one day could be selected at a time. This is the exact type of logic we’re trying to let the browser handle as we rebuild our interfaces.

It all starts with a simple checkbox

Let’s consider the scripting that might go into implementing a checkbox-style switch. When we click a switch that’s “off,” we change its state to “on” and get some visual feedback. We’d probably store its state in a boolean variable somewhere, add a click handler that updates the boolean, and then add or remove CSS classes based upon its current value. From a logic standpoint, it’s a duplication of something the browser can already do with <input type="checkbox">, but we put up with it for years because it let us have pretty switches.

Then the technique of using checked state to style checkboxes hit the mainstream. Using a combination of new CSS selectors and older browser behavior, it was possible to have visual elements that were tied to invisible inputs without writing a single line of JS. We use this same technique for many of our basic toggle switches today, but we’ve also taken it to new extremes with many of our UI components.

When building components, examine their core behavior

When we build new UI components for Parse.com, we follow a simple strategy: use the most similar browser element as a starting point. This goes beyond the obvious cases — such as using an anchor tag for something the user clicks — and looks at the inner behavior of a complex element. Remember the date picker we discussed earlier? The core interaction relies on being able to select a single day out of the month and visually represent that selection. Your browser already knows how to handle this: it’s behaviorally identical to a set of radio buttons! In fact, you’d be amazed by how many complex elements boil down to the radio button’s select-one-of-many behavior. They’re at the core of our own date picker, our fully styled dropdown menus, and a number of other switches and toggles. Knowing that the browser will ensure a single element is selected at any time allows us to eliminate a concern from our client logic.

uie_blog_2

Simultaneously, we avoid having to synchronize our data with our visualization, because a single interaction within the browser updates both. Along those same lines, at Parse we refrain from storing any data within a component that could be reasonably derived from its inner elements. For our specialized numeric inputs, we get the value by parsing the contents of an inner text input on demand, rather than returning an in-memory value that we update with an onChange handler. This guarantees that our JS component and its visual appearance never get out of sync.

Enough talk, let’s see a demo

My favorite implementation of this technique is found in our Hosting product. It’s a file tree that features complex interactions with absolutely no JavaScript.

uie_blog_3

When I designed the tree, I broke it down into its core behaviors: it has folders that open and close to display contents, and an overall set of files that can be individually selected. If you’re following along, it should make sense that the folders are backed by checkboxes while the files themselves are a single set of radio buttons.

<!-- Files are a set of radio buttons with the same name field -->
<input type="radio" name="hosted_files" id="f_myfile_js" value="MyFile.js">
<label for="f_myfile_js">MyFile.js</label>

<input type="radio" name="hosted_files" id="f_another_js" value="Another.js">
<label for="f_another_js">Another.js</label>

<!-- Folders are checkboxes that toggle the visibility of divs -->
<input type="checkbox" id="f_myfolder">
<label for="f_myfolder">My Folder</label>
<div class="dir_wrapper">
  Folder contents...
</div>

In CSS, we simply provide default styles for labels, and additional styles for when they follow a checked input.

label {
  color: #ffffff;
}
input[type=radio]:checked + label {
  color: #5298fc;
}

For folders, we also toggle the display property of a .dir_wrapper element that comes after a selected checkbox.

.dir_wrapper {
  display: none;
}

input[type=checkbox]:checked + label + .dir_wrapper {
  display: block;
}

With these styles, I don’t need to worry about having numerous click handlers to open or close folders, and a single change handler on the radio buttons can tell me when a new file is selected.

You can examine a slightly modified version of our file tree at this CodePen.

So what?

As web developers, we tend to lean a lot on JavaScript to build fancy interfaces and effects. I know I certainly do. But the next time you set out to build something new, look at its core behaviors and ask yourself if there is some pre-written, standardized part of the web browser that can handle some of the work for you. Yes, there is a bit of a tradeoff with markup instead of code, but I’d rather write code to generate HTML than deal with synchronizing data and visualization, handling a plethora of clicks, taps, and keyboard inputs, and ultimately replicating behavior that’s already embedded in the browser.

With these methods, you can build custom inputs that play nicely with the existing web. They fire native change events, they don’t risk side-effects in your view logic, and in many cases they even improve screen reader accessibility. If you take a look at the HTML5 spec for radio buttons, you’ll see that it only talks about behavior, not appearance. It’s no long stretch to reuse that behavior in new and creative ways, and I look forward to seeing how others employ it for interface construction.

Andrew Imm
November 10, 2014

Introducing the new ParseUI for iOS

ParseUI iOS

A couple of months ago, iOS added new screen resolutions for developers to support in their apps.  As a result, many developers needed to update their user interfaces to look native on these new screen sizes.

Today we’re introducing ParseUI for iOS, a collection of user interface components aimed to streamline and simplify user authentication, data list display, and other common app elements. You might be familiar with these components already, as they were previously packaged inside the main Parse iOS SDK. With this release, we’ve updated how they look and made them resolution-independent to ensure a polished presentation on any screen.

A New Look

Inside this release you’ll find all new designs for every component with simplified user interfaces plus many under-the-hood improvements, such as smoother animations and faster rendering. To give you an example, here’s how `PFLogInViewController` looked before and how it looks today:

ParseUI iOS New Look

Resolution-Independent

All components were rebuilt from scratch to be resolution-independent, meaning they both look great and are native on any screen resolution. This resolution-independent approach also introduces support for more presentation options. It gives you the flexibility to comprehensively customize everything within your application’s navigation and layout.

ParseUI iOS

Open Source

ParseUI is all open source, and you can view the code on GitHub. You can also access the new version of the Parse iOS SDK here.

We’re really excited to be open sourcing more and more of our product. Check out these other projects on GitHub: BoltsFramework, Parse PHP SDK, ParseUI for Android and App Links Analytics.

We can’t wait to see what you build with it! Send us your feedback!

Nikita Lutsenko
November 6, 2014

Get Off My Lawn: a New Way to Control Access to Your App

Security plays an important part in releasing an app on Parse, and Access Control Lists (ACLs) are one of the most powerful ways to secure your app. As Bryan mentioned in part III of his security series, “If you have a production app and you aren’t using ACLs, you’re almost certainly doing it wrong.” In that blog post he examines how to create and save ACLs in code. Another effective way to create and manage ACLs is through the data browser, and today that is much easier with a new and improved ACL editor. The data browser is already one of our most popular and most frequently used tools, and now you can also use the data browser to easily edit and manage your app’s ACLs.

An ACL is essentially a list of users that are allowed to read or write a Parse object. For example, let’s say your social networking app has a Post object, and you want the user who created that post, jack, to be the only person who can see it. You would then create an ACL on that object and give Jack read permissions. By default, ACLs are public read and write, so to add an ACL on Jack, first uncheck public read and write, and then type Jack’s username into the ACL editor. Each step is shown in the new editor, along with the equivalent code in iOS.

Screenshot 2014-10-20 19.48.56

PFUser *jack = [PFUser currentUser]; // username "jack"
PFObject *post = [PFObject objectWithClassName:@"Post"];
PFACL *acl = [PFACL ACL];
[acl setReadAccess:true forUser:jack];

Now Jack’s post is private, and Jack is the only one who can see it. But nobody has write permissions on the object, so its fields can only be changed by using the master key. What if Jack wants to edit his post? To allow this, you need to give Jack write permissions in the object’s ACL.

Screenshot 2014-10-20 19.49.00

[acl setWriteAccess:true forUser:jack];

Now Jack can both see and edit his Post object. But what if Jack wants to share his Post with his friend jill? Right now Jack is the only user with read or write permissions on the object. You want to allow Jill to be able to see the post, but you don’t want her to be able to edit it. Therefore you need to add read permissions for Jill on the ACL.

Screenshot 2014-10-20 19.49.08

PFUser *jill; // username "jill"
[acl setReadAccess:true forUser:jill];

Jack and Jill can see the post now, and Jack is the only person who can edit it. But maybe you want to be able to make sure the content of Jack’s post is appropriate, and so you want a group of administrators to be able to see the post, and to delete it if something is wrong with it. Once you’ve assigned these users to a Role named admin, you could add permissions on that role to the ACL. Given that delete is included in “write” permissions, you would check both read and write in the ACL editor.

Screenshot 2014-10-20 19.49.15

[acl setReadAccess:true forRoleWithName:@"admin"];
[acl setWriteAccess:true forRoleWithName:@"admin"];

What if Jack wants to publish his post publicly? Now you want to give the post public read permissions. Simply check the read checkbox in the public permissions row at the top of the editor, and anyone can now see the post.

Screenshot 2014-10-20 19.49.18

[acl setPublicReadAccess:true];

Notice that all of the users and roles in the ACL now have read permissions checked automatically. That is because you have enabled public read, so anybody can read Jack’s post. But public write is still disabled, so the only users who can edit the post are Jack and all users that belong to the admin role. Click “Save ACL” and now your post object will have all of these permissions set, summarized in its cell by “Public Read, 1 Role, 2 Users”.

Screenshot 2014-10-20 19.53.15

post.ACL = acl;
[post saveInBackground];

An undefined ACL acts the same as an ACL with public read and write permissions enabled, so when you first click on a cell without an ACL defined, the editor will show the public read and write boxes checked. To add private permissions on a role or user, you must first uncheck public read and/or write.

Screenshot 2014-10-20 15.02.30

We are excited to give Parse users a fresh, simple way to add and manage ACLs in the data browser with the new ACL editor. Go try it out, and as always, let us know what you think!

Jamie
October 21, 2014

Introducing Bolts for Parse SDKs

Bolts Framework

Earlier this year we introduced the Bolts Framework, a set of low-level libraries to make developing mobile apps easier and faster. One of the core components of Bolts is Tasks. Tasks brings the programming model of JavaScript Promises to iOS and Android.

Today, we are proud to open an entirely new set of public APIs with the new versions of our iOS, OS X and Android SDKs. Inside this release you will find that every method that was once intended to be asynchronous now has a Tasks-based counterpart. Organizing complex asynchronous code is now much more manageable in addition to being easier and faster to implement.

For example, here’s how you can find posts, mark them all as published, and update the UI on iOS:

PFQuery *query = [PFQuery queryWithClassName:@"Post"];
[query whereKey:@"published" notEqualTo:@YES];
[[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) {
  NSArray *results = task.result;

  NSMutableArray *saveTasks = [NSMutableArray arrayWithCapacity:[results count]];
  for (PFObject *post in results) {
    post[@"published"] = @YES;
    [saveTasks addObject:[post saveInBackground]];
  }
  return [BFTask taskForCompletionOfAllTasks:saveTasks];
}] continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask *task) {
  _postStatusLabel.text = @"All Posts Published!";
  return task;
}];

And here’s how it looks on Android:

ParseQuery<ParseObject> query = ParseQuery.getQuery("Post");
query.whereNotEqualTo("published", true);
query.findInBackground().onSuccessTask(new Continuation<List<ParseObject>, Task<Void>>() {
  @Override
  public Task<Void> then(Task<List<ParseObject>> task) throws Exception {
    List<ParseObject> results = task.getResult();
        
    List<Task<Void>> saveTasks = new ArrayList<Task<Void>>();
    for (final ParseObject post : results) {
      post.put("published", true);
      saveTasks.add(post.saveInBackground());
    }
    return Task.whenAll(saveTasks);
  }
}).onSuccess(new Continuation<Void, Void>() {
  @Override
  public Void then(final Task<Void> task) {
    mPostStatusTextView.setText(String.format("All Posts Published!"));
    return null;
  }
}, Task.UI_THREAD_EXECUTOR);

As you can see, there’s a lot of advantages to this approach when it comes to branching, chaining tasks, parallelizing across multiple threads, and handling complex errors.

We are really excited about this release and would love to hear your feedback. Let us know what you think!

Nikita Lutsenko
October 15, 2014

Android Push Gets Major Refresh

We’re excited to announce today that the Android Push API is getting its biggest facelift since inception. We’ve rethought the API to bring Parse Push better in line with both other Parse APIs and traditional Android development. The new API simplifies developer setup, provides better reliability, and is much more easily extended or customized to override default push behavior.

With the new API, we’ve decoupled the concepts of registration and reaction; the icon and activity which you want push to use are no longer statically bound to a channel. You can replace all calls to

PushService.subscribe(myApplicationContext, "channel", MyActivity.class, intIconID);

with

ParsePush.subscribeInBackground("channel");

Notice the InBackground suffix in the new APIs on ParsePush? The Android API now exposes when the Parse servers have accepted a subscription change in addition to any reason why the request may have failed. When the new API receives a push, it fires the com.parse.push.intent.RECEIVE Intent. We’ve provided a BroadcastReceiver implementation that automatically handles the vast majority of our developer’s needs. You can register it with the following additions to your manifest:

<receiver android:name="com.parse.ParsePushBroadcastReceiver"
  android:exported="false">
  <intent-filter>
    <action android:name="com.parse.push.intent.RECEIVE" />
    <action android:name="com.parse.push.intent.DELETE" />
    <action android:name="com.parse.push.intent.OPEN" />
  </intent-filter>
</receiver>

This BroadcastReceiver will suit most developers needs without any additional configuration. Your pushes will show your application’s default icon and launch your application’s main activity when touched. To provide a different icon, specify it with the com.parse.push.notification_icon meta-data in your AndroidManifest.xml. You no longer need to add push open tracking calls throughout your activities; we do it automatically in the BroadcastReceiver. If the contents of your push are too long for the normal UI, we’ll automatically create the expanded layout available in Android 4.1 JellyBean (API 16) and up.

We’re also introducing a new special field for Android pushes too! If you specify the uri push option, the Notification created will navigate to that URI rather than launching the default Activity. For example, the sending the following push from Cloud Code would create a notification that sends users to the Parse blog.

Parse.Push.send({
  where: new Parse.Query(Parse.Installation), // Everyone
  data: {
    alert: "Android Push Gets Major Refresh",
    uri: "http://blog.parse.com",
  }
})

When the user clicks ‘back’, they will be sent to your launcher Activity (overridden with getActivity). As a security precaution, the uri option may not be used when a push is sent from a client.

If your app has more sophisticated needs, you can easily subclass ParsePushBroadcastReceiver to tweak the behavior of your application without foregoing all of the built-in features. Subclass our BroadcastReceiver and put your new class in the manifest instead of ours. You can react to push notification events by overriding any of onPushReceived, onPushOpen, or onPushDismiss. If you need to customize the Notification that we create, you can override getNotification.

We’re very excited for this new API. If you’re not ready to adopt the new API quite yet, the old push APIs are deprecated but still work exactly as they did before. We hope you are excited for the new approach. It’s now easier than ever to get started and to transition from turn-key implementations to much more sophisticated ones.

Thomas Bouldin
September 30, 2014

Parse SDK for iOS 8, Performance, and Security

Parse SDK for iOS 8

We’re excited that the big day is finally here — iOS 8 is finally available to everyone! We spent the last few weeks waiting patiently and getting ready. Today we’re rolling out a new version of the Parse SDK for iOS with support for iOS 8 as well as a bunch of other performance and security improvements.

Parse and iOS 8

We updated our SDK to make sure it runs smoothly on iOS 8 and benefits from all the new APIs available. Just to name a couple, we’ve updated how [PFGeoPoint geoPointForCurrentLocationInBackground:] works to be smarter about requesting the appropriate permissions depending on the state of the app, and we’ve updated our push notification integration everywhere to use the new permissions style and support the category key.

Performance Improvements for Parse Files

Parse Files let you easily store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular database-style Parse Object. This release of the iOS (and OS X) SDK brings greatly improved performance for Parse Files. Uploading is now up to 3 times faster and downloading is up to 35% faster. We’re also using fewer system resources which leads to better battery usage for file-heavy apps.

Improved Security for Account Data

The Parse SDK now uses the System Keychain on both iOS and OS X to store sensitive user information tied to PFUser. All of this happens automatically and under the hood so you don’t need to make any changes on your end.

Try It Out

The latest iOS SDK is now available for download here. Send us your feedback! We hope you’re as excited as we are about the new features and possibilities that iOS 8 brings.

Nikita Lutsenko
September 18, 2014

Ready for iOS 8? So is Parse Push

In this year’s WWDC, Apple announced some great changes to iOS Notifications. First, Apple has increased the maximum payload size to two kilobytes rather than 256 bytes. This new limit is retroactive and works with all existing devices! The more subtle changes to push notifications involve security and interaction with notifications.

“Silent” pushes are push notifications that don’t create UI; they instead tell your app to fetch or react to new content available online. In iOS 8, Apple has separated out the permissions for UI and push. The push permission is auto-accepted by default too! This means your iOS 8 apps will be able to much more reliably depend on the ability to receive silent notifications in iOS 8. To migrate your app, change the following code:

// Before iOS 8:
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert |
                                                                      UIRemoteNotificationTypeBadge |
                                                                      UIRemoteNotificationTypeSound];

to

// For iOS 8:
UIUserNotificationSettings *settings =
    [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert |
                                                 UIUserNotificationTypeBadge |
                                                 UIUserNotificationTypeSound
                                      categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
Even if the user declines permission for creating UI, the permission to send (silent) pushes is auto-accepted for most users.

Notice the nil param for categories above? Categories unlock a whole new dimension to push notifications in iOS 8. Categories describe “actions” that should be presented in your notification in various views. Actions provide custom buttons your users can use when interacting with your push notification; your action can launch the app into the foreground or even trigger a background action, such as accepting or declining a calendar invite. To enable this feature, you must define a UIUserNotificationCategory. These categories have an identifier string and a map of UIUserNotificationActionContext to many UIUserNotificationActions. A UIUserNotificationActionContext comes in two flavors: default and minimal. The default context specifies which actions should be presented when the notification has an alert UI and supports a maximum of four actions; the minimal context specifies which actions should be presented when the notification has a banner UI or is on the lock screen and supports a maximum of two actions.  Once you’ve registered notification categories with your application, sending them via Parse Push is easy: simply pass the category identifier as the category option in your push. This feature is supported retroactively on all Parse SDKs, you don’t even need to upgrade! You may, however, want to check out the new UIApplicationDelegate method application:handleActionWithIdentifier:forRemoteNotification:completionHandler:

iOS 8 allows developers to define actions an end user can take when responding to a notification.

We look forward to seeing the great things you build with Parse and iOS 8!

Thomas Bouldin
September 15, 2014

The Dangerous World of Client Push

Push Notifications are one of the most effective ways for apps to increase user engagement and retention, notify customers of important information like sales and new products, or allow users communicate with each other. A messaging app, for example, could use push notifications to alert users to an incoming message from their friend in the app. But be careful, because the most obvious way to do this can expose you to a security risk.

One easy way to send push notifications to particular users from within an app is to send the pushes directly from the client code, for example by using PFPush on iOS. This is tempting, because you have all the information you need to send the push straight from within iOS, and the code to send the push is very simple. But as Bryan mentioned in his security series, clients can’t be trusted to send push notifications directly. Some nefarious hacker could modify the client code, changing the Objective-C or Java code to modify the push’s alert text, or to send pushes to people they shouldn’t be able to. The hacker could steal your app’s keys and send pushes himself without even using the app.

Sounds scary! Luckily, there is a simple way around this security vulnerability. In your app’s settings, under “Push Notifications”, there is a toggle that lets you disable “Client Push.” We highly recommend that you set this switch to “OFF,” and instead of sending the push notifications from the client, you should write Cloud Code functions that validate the user and the data to be pushed before sending them. Client Push is disabled by default, but this feature can still be useful for initial prototyping if you want to hook up push notifications without diving into Cloud Code as you iterate. But as you scale to a production app, you really should not have Client Push enabled.

Here is one example of how to port your pushes from the client into Cloud Code, starting with an example Client Push to a single user, stored in userObject. Here’s the old iOS code:

// WRONG WAY TO SEND PUSH - INSECURE!
PFQuery *pushQuery = [PFInstallation query];
[pushQuery whereKey:@"user" equalTo:userObject];
PFUser *user = [PFUser currentUser];
NSString *message = [NSString stringWithFormat:@"%@ says Hi!", user[@"name"]];

PFPush *push = [[PFPush alloc] init];
[push setQuery:pushQuery]; // Set our Installation query
[push setMessage:message];
[push sendPushInBackground];

And on Android:

// WRONG WAY TO SEND PUSH - INSECURE!
ParseQuery pushQuery = ParseInstallation.getQuery();
pushQuery.whereEqualTo("user", userObject);
ParseUser currentUser = ParseUser.getCurrentUser();
String message = currentUser.getString("name") + " says Hi!";

ParsePush push = new ParsePush();
push.setQuery(pushQuery); // Set our Installation query
push.setMessage(message);
push.sendInBackground();

To move these push calls to Cloud Code, first we would write a Cloud function, sendPushToUser, to verify that the user sending the push is allowed to send it and that the alert text is what it should be.

Parse.Cloud.define("sendPushToUser", function(request, response) {
  var senderUser = request.user;
  var recipientUserId = request.params.recipientId;
  var message = request.params.message;

  // Validate that the sender is allowed to send to the recipient.
  // For example each user has an array of objectIds of friends
  if (senderUser.get("friendIds").indexOf(recipientUserId) === -1) {
    response.error("The recipient is not the sender's friend, cannot send push.");
  }

  // Validate the message text.
  // For example make sure it is under 140 characters
  if (message.length > 140) {
  // Truncate and add a ...
    message = message.substring(0, 137) + "...";
  }

  // Send the push.
  // Find devices associated with the recipient user
  var recipientUser = new Parse.User();
  recipientUser.id = recipientUserId;
  var pushQuery = new Parse.Query(Parse.Installation);
  pushQuery.equalTo("user", recipientUser);
 
  // Send the push notification to results of the query
  Parse.Push.send({
    where: pushQuery,
    data: {
      alert: message
    }
  }).then(function() {
      response.success("Push was sent successfully.")
  }, function(error) {
      response.error("Push failed to send with error: " + error.message);
  });
});

 After you deploy this Cloud function, you can call the code from iOS like this:

[PFCloud callFunctionInBackground:@"sendPushToUser"
                   withParameters:@{@"recipientId": userObject.id, @"message": message}
                            block:^(NSString *success, NSError *error) {
  if (!error) {
     // Push sent successfully
  }
}];

And on Android:

HashMap<String, Object> params = new HashMap<String, Object>();
params.put("recipientId", userObject.getObjectId());
params.put("message", message);
ParseCloud.callFunctionInBackground("sendPushToUser", params, new FunctionCallback<String>() {
   void done(String success, ParseException e) {
       if (e == null) {
          // Push sent successfully
       }
   }
});

Now that you have moved the Push logic into Cloud Code, you can disable Client Push in your app settings, and spoil the hacker’s evil plan! We hope you learned how to use Cloud Code to secure your app’s push notifications, and that you will think twice before enabling Client Push for your app. As always, feel free to ask questions or let us know what you think on our new Help page.

Jamie
September 3, 2014