Working with Hostnames in a Dynamically Scaled Environment

… or how Chef and Route53 helped humanize machine names.

Working with servers in a virtualized environment is a funny thing. They all have names like i-01234567, ec2-123-45-67-89.compute-1.amazonaws.com, and so on. And that works! Servers seem just as happy if you tell them their memcache server is at 10.2.3.4 as if you had said it’s at memcache3.parse.com. Sadly, we poor humans working with these machines are not as flexible. I have no idea if the machine at i-abcd9876 is running memcache or nginx, or just sitting there imagining rainbows. I need some help figuring out who’s doing what.

We use Chef here at Parse to manage what software runs on each of our machines, so adding a cookbook and a recipe to every machine is something that’s pretty easy to do. We were able to take advantage of a combination of EC2 tagging and the Chef Route53 cookbook to make a small change that made a world of difference to how easy it is to keep track of all of our servers.

Launching a new server happens in two ways: by hand or using an AWS auto-scaling group. When launching instances by hand, we get to assign a node name; when the auto-scaling group launches an instance, it needs to figure out what it should be named. Our solution for making sure hosts have human-usable names must accommodate both of these scenarios. The first is easier, so let’s walk through that first.

This code snippet first loads up the route53 cookbook and our AWS credentials. It then figures out the fully qualified domain name for our server (using a special subdomain indicated by the int_domain attribute to keep our publicly accessible names like www.parse.com safe from accidents), and creates the record in Route53. Nice and simple, right?

# register the dns name in route53 based on the hostname
include_recipe "route53"
aws_creds = Chef::EncryptedDataBagItem.load("passwords", "aws")
full_nodename = "#{node.name}.#{node[:route53][:int_domain]}"
route53_record "create CNAME record" do
  name                   full_nodename
  value                  node[:ec2][:public_hostname]
  type                   "CNAME"
  zone_id                node[:route53][:zone_id]
  aws_access_key_id      aws_creds["aws_access_key_id"]
  aws_secret_access_key  aws_creds["aws_secret_access_key"]
  ttl                    600
  action :create
end

Chef recipes are supposed to be idempotent, meaning that you can run it multiple times and you should get the same result. What this really means is that if the CNAME record already exists for this host, we shouldn’t do anything. The route53 cookbook LWRP route53_record takes care of this for us with a simple check:

record = zone.records.get(name, type)
if record.nil?
  create
  Chef::Log.info "Record created: #{name}"
elsif value != record.value.first
  record.destroy
  create
  Chef::Log.info "Record modified: #{name}"
end

Try and retrieve the record. If it doesn’t exist, create it. If it’s different from what it should be, fix it (this happens when you stop and start an instance and its IP address changes). If it already exists and is correct, do nothing.

Now that we have manual host naming under control, managing auto-scaling groups becomes simple; all we need to do is figure out an alternate source for node.name. In the AMI we give the auto-scaling group, we include a record of the role with which the AMI was created. This role is the prefix for the hostname we want assigned. That prefix is used by the following snippet as an alternate source for node.name.

zone = r53.zones.get(zone_id)
records = zone.records.all!
# get an array with all the records that start with $prefix
matches = Array.new
records.each do |r|
  match = r.name.match /#{prefix}[0-9]+/
  if match.to_s.length != 0
    matches << match
  end
end
# find the number of the last one (they come sorted) and increment
num = matches.last.to_s.match /[0-9]+/
incr = num.to_s.to_i + 1
puts "#{prefix}#{incr}"

With these recipes and the route53 cookbook in place, Chef verifies and corrects the DNS entries for all of our servers automatically with every run, ensuring that we can safely use human-readable host names to reference all of our machines.

Ben Hartshorne
February 28, 2013

Reproducing the Macintosh Boot Beep from JavaScript Cloud Code

Like many of us here at Parse, I’ve always had an interest in computer-generated audio. So when I first read Andy Hertzfeld’s account of how the original Macintosh “boot beep” was invented, I was fascinated. And since he provided the original Motorola 68000 assembly language source code that actually generated the beep, I knew it was only a matter of time until I would have to dissect it and get it working myself.

Recently, Stanley Wang launched a great new feature in Cloud Code. The new Buffer class from Node.js lets you modify binary data from within JavaScript. For his blog post, I created a Cloud Code function to generate a GIF file, just to give an example of one of the things you could do with a Buffer. Well, generating a small audio file is another.

As with most sound APIs, the Macintosh primarily provided a buffer filled with Linear PCM samples. Basically, the values in the buffer are “samples” arranged in chronological order. Each sample indicates the amplitude of the audio waveform at that point in time. After the audio in the buffer is finished playing, it starts over again at the beginning. By changing the contents of the buffer, different sounds can be played.

The simplest standard file format for PCM data is the WAVE file. WAVE files aren’t used as commonly as MP3s because they are uncompressed, and therefore can be quite large. However, the lack of compression makes them very easy to generate. So, for a modern implementation of the Macintosh Boot Beep, it makes sense to generate the PCM data and output it as a WAVE file, which can be played with most audio players. For creating an output file of arbitrary size, it’s helpful to create a wrapper around Buffer that will let you append data to it, and grow the buffer as necessary.

var _ = require('underscore');
var Buffer = require('buffer').Buffer;

/**
 * Lets you fill a Buffer without having to know its size beforehand.
 */
var BufferWriter = function() {
  this._size = 0;
  this._buffer = new Buffer(100);
};

_.extend(BufferWriter.prototype, {
  buffer: function() {
    return this._buffer.slice(0, this._size);
  },

  writeUInt8: function(value) {
    this._reserve(this._size + 1);
    this._buffer.writeUInt8(value, this._size);
    this._size = this._size + 1;
  },

  writeInt16LE: function(value) {
    this._reserve(this._size + 2);
    this._buffer.writeInt16LE(value, this._size);
    this._size = this._size + 2;
  },

  writeUInt16LE: function(value) {
    this._reserve(this._size + 2);
    this._buffer.writeUInt16LE(value, this._size);
    this._size = this._size + 2;
  },

  writeUInt32LE: function(value) {
    this._reserve(this._size + 4);
    this._buffer.writeUInt32LE(value, this._size);
    this._size = this._size + 4;
  },

  writeUTF8: function(str) {
    this._reserve(this._size + str.length * 6);
    this._size = this._size + this._buffer.write(str, this._size);
  },

  /**
   * Resizes the backing buffer to ensure it can hold at least size bytes.
   */
  _reserve: function(size) {
    var current = this._buffer.length;
    while (size >= current) {
      this._buffer = Buffer.concat([this._buffer, new Buffer(current)],
                                   current * 2);
      current = this._buffer.length;
    }
  }
});

Then, with a little knowledge of the WAVE format, it’s easy to create a class that will let you “record” a predetermined amount of audio into a file.

/**
 * Creates a helper to write a WAVE file to a Buffer.
 * @param {Number} seconds - The length of the WAVE file to write.
 * @param {Number} sampleRate - The sample rate in Hz, such as 44100 for a CD.
 * @param {Number} bitsPerSample - Either 8 or 16.
 */
var WaveWriter = function(seconds, sampleRate, bitsPerSample) {
  this._sampleRate = sampleRate;
  this._bitsPerSample = bitsPerSample;
  this._writer = new BufferWriter();
  this._samples = Math.round(this._sampleRate * seconds);

  var numChannels = 1;
  var subChunk2Size = this._samples * numChannels * bitsPerSample / 8;
  var chunkSize = 36 + subChunk2Size;
  try {
    this._writer.writeUTF8("RIFF");
    this._writer.writeUInt32LE(chunkSize);
    this._writer.writeUTF8("WAVE");
  } catch (e1) {
    console.error("Got an exception while writing WAVE header.");
    throw e1;
  }

  try {
    // Sub-chunk 1
    var byteRate = 36 + sampleRate * numChannels * bitsPerSample / 8;
    var blockAlign = numChannels * bitsPerSample / 8;

    this._writer.writeUTF8("fmt ");
    this._writer.writeUInt32LE(16);
    this._writer.writeUInt16LE(1);
    this._writer.writeUInt16LE(numChannels);
    this._writer.writeUInt32LE(sampleRate);
    this._writer.writeUInt32LE(byteRate);
    this._writer.writeUInt16LE(blockAlign);
    this._writer.writeUInt16LE(bitsPerSample);
  } catch (e2) {
    console.error("Got an exception while writing subchunk 1.");
    throw e2;
  }

  try {
    // Sub-chunk 2
    this._writer.writeUTF8("data");
    this._writer.writeUInt32LE(subChunk2Size);
  } catch (e3) {
    console.error("Got an exception while writing subchunk 2.");
    throw e3;
  }
};

_.extend(WaveWriter.prototype, {
  /**
   * Write a single sample to the WAVE file.
   * @param {Number} sample The amplitude of the sample, from -1 to 1.
   * @return {Boolean} false if the file is already full, true otherwise.
   */
  writeSample: function(sample) {
    if (this._samples === 0) {
      return false;
    }
    // Clamp values out of range.
    if (sample < -1.0) {
      sample = -1;
    }
    if (sample > 1.0) {
      sample = 1;
    }
    if (this._bitsPerSample === 16) {
      this._writer.writeInt16(Math.floor(32767 * sample));
    } else if (this._bitsPerSample === 8) {
      try {
        this._writer.writeUInt8(Math.floor(-127.5 * sample + 127.5));
      } catch (e) {
        console.error("Got an exception while writing a sample.");
        console.error("Sample is " + Math.floor(-127.5 * sample + 127.5));
        throw e;
      }
    }
    this._samples = this._samples - 1;
    return true;
  },

  /**
   * Fills the rest of the file with zero-amplitude samples.
   */
  close: function() {
    while (this._samples > 0) {
      this.writeSample(0.0);
    }
  },

  /**
   * Returns the underlying buffer with the contents of the WAVE file.
   */
  buffer: function() {
    return this._writer.buffer();
  },
});

Now comes the fun part. It’s time to translate the original boot beep code into JavaScript. This might strike you as a crazy thing to do. It probably is, but don’t let that stop you. To achieve maximum accuracy, I translated the original assembly language instructions directly into JavaScript. The hardest part of a direct translation is that JavaScript lacks any goto construct. Fortunately, all of the branching done in the original code was pretty simple, and translated nicely into _.times or _.each. It’s possible that evaluating _.times and passing it a function object might even be faster running in V8 in Chrome on a MacBook Pro than a single DBRA instruction was on the original Macintosh, although that’s purely conjecture. Compare to the original source.

/**
 * Adapted from http://folklore.org/projects/Macintosh/more/BootBeep.txt
 * A direct translation of the m68k asm into JavaScript.
 * @returns {Buffer} a Buffer which  corresponds to the audio buffer on the
 * original Macintosh, repeated for the duration of the boot beep. The Buffer
 * contains a series of words, where the lower byte of each word is the
 * amplitude of the audio signal at this point.
 */
var bootbeepASM = function() {
  d3 = 40;  // D3 contains the duration: 40 is boot beep

  var output = new Buffer(0);
  var buffer = new Buffer(2 * 5 * 4 * 19);

  var a0 = 0;  // get sound buffer address
  var a1 = 0;

  console.log("Initializing buffer.");

  // repeat 74 byte sequence 5 times
  _.times(5, function() {
    // 4 byte table to fill buffer with
    _.each([0x06, 0xC0, 0x40, 0xFA], function(d1) {
      _.times(19, function() {         // repeat 19 times
        buffer.writeUInt16LE(d1, a0);  // move in a byte
        a0 += 2;                       // bump to next buffer location
      });
    });
  });

  console.log("Filtering buffer " + d3 + " times.");

  // OK, now filter it for a nice fade -- repeat the filtering process D3 times
  _.times(d3, function() {
    a0 = 0;         // point A0 to start of buffer
    a1 = a0 + 146;  // point A1 74 values in
    // process 74 samples
    _.times(74, function() {
      var d2 = buffer.readUInt8(a1);  // get 74th value
      a1 += 4;                        // bump to 76th value
      var d1 = buffer.readUInt8(a1);  // make it a word value (lol, no)
      d2 += d1;                       // add to the accumulator
      a1 -= 2;                        // point at 75th value
      d1 = buffer.readUInt8(a1);      // get it
      d2 += d1;                       // add to the accumulator
      d2 += d1;                       // count it twice

      d2 = Math.round(d2 / 4);        // divide it by 4
      buffer.writeUInt8(d2, a0);      // store it away
      a0 += 2;                        // bump to next value
    });

    // 296 values to copy
    _.times(296, function() {
      buffer.writeUInt8(buffer.readUInt8(a0 - 74), a0);
      a0 += 2;
    });

    // Only the first 370 words is actually used by the Mac audio buffer.
    output = Buffer.concat([output, buffer.slice(0, 2 * 370)]);
  });

  return output;
};

You’ll notice the JavaScript version skips over the part where it should “wait for blanking”. What is that about? Well, if you search the web for the details of the original Mac’s audio hardware, you’ll see that the 370 sample audio buffer gets flushed every time the screen refreshes! This is important, because it tells us the “sample rate” — the rate at which the samples should be output. The screen refreshes at 60.15 Hz, so the sample rate for our output WAVE file should be close to 370 * 60.15 = 22255.5 Hz, about half the quality of a CD. For compatability, let’s round this to 22050 Hz, exactly half CD quality.

Furthermore, the docs make it clear that each sample is a word, but only the (unsigned) high-order byte of the word is used for audio. In fact, the low-order bytes are used for disk IO! Ah, the perils of early computing. We forget how good we have it these days. See the warning:

Warning:  The low-order byte of each word in the sound buffer is used to
          control the speed of the motor in the disk drive. Don't store
          any information there, or you'll interfere with the disk I/O.

Knowing the sample rate, we can easily create a function to take the output of bootbeepASM and convert it into an actual WAVE file.

/**
 * Runs the bootbeepASM methods and then converts the audio data to a WAVE file.
 * @returns {Buffer} a Buffer containing the contents of a WAVE file.
 */
var bootbeep = function() {
  // 22050 Hz is an approximation of the Mac's 370 * 60.15 Hz = 22255.5 Hz.
  var writer = new WaveWriter(1.5, 22050, 8);
  var samples = bootbeepASM();
  console.log("Copying samples to WAVE.");
  var i = 0;
  var sample = 0;
  do {
    if ((i * 2) < samples.length) {
      sample = (samples.readUInt8(i * 2) / 128) - 1.0;
    }
    i++;
  } while (writer.writeSample(sample));
  writer.close();
  return writer.buffer();
};

Parse.Cloud.define("bootbeep", function(request, response) {
  response.success(bootbeep().toString("base64"));
});

At this point, you could use the same trick as with the GIF example to create a file on-the-fly and add it to the html of the page using an HTML5 <audio> tag with a “data:” url. I’ve pre-populated such a control below so you can hear the output of this function yourself.

To see how close the output is, just search for any video of a Macintosh or Macintosh Plus booting, and compare for yourself. Pretty close, right? Admittedly, this isn’t the most practical code example in the world, but it does show just how innovative you can get with a little JavaScript Cloud Code. And don’t forget, we’re always hiring, just in case you want to help us push these boundaries even further.

Bryan Klimt
February 27, 2013

IDEO Brings Elmo and Cookie Monster to Your Smartphone

Image of Elmo Calls on iPhone 5As anyone raising children in today’s technology filled world will tell you, kids are learning to use the newest gadgets almost as quickly as their parents. And, adults aren’t the only ones enchanted by the proliferation of smartphones and tablets; iPads topped children’s Christmas wish lists in 2012 and scientists are working to learn more about educational apps for children. With all of these new avenues opening for children’s apps, it’s no surprise that even well-loved classics like the Sesame Street muppets are popping up in the app-mosphere.

Parse-powered apps Elmo Calls and Cookie Calls are two innovative apps that take advantage of both characters that children love, Elmo and Cookie Monster from Sesame Street, as well as technology they enjoy, namely their parents’ smartphones. We sat down with Dominique Yahyavi of IDEO, the firm that created the apps, to find out more about how Parse plays into these highly rated apps.

 

Tell us about IDEO and your work there.

IDEO is a global design and innovation firm. We work in the Toy Lab within IDEO, which is a small group of toy inventors that design physical and digital toy prototypes.

How did the Toy Lab find its way to using Parse?

We needed to find a backend service to help us prototype quickly, and after some research we found that Parse was the best option.

What can you tell us about the apps IDEO has built on Parse?

As a part of IDEO, a human-centered design and innovation firm, the Toy Lab believes that design needs to stem from real people, real needs, and real desires. With these insights we’ve built iPhone apps for kids that delight and support enriched family interactions. We’ve created great apps through our partnerships with Sesame Street and Fisher-Price – bringing great characters and classic toy play to life on the iPhone. Two of our popular Sesame Street apps, Elmo Calls and Cookie Calls, are built using Parse. And we’re planning on building more apps on Parse soon.

What would you say have been the key benefits to using Parse for your apps?

Parse has allowed us to focus on the user-experience and leave the backend to them. It’s been a great experience so far. Also, Parse is always pushing their platform forward, and we feel that it is a huge benefit to be working with a company that is innovating at such a fast rate.

Parse has provided our apps with a reliable and scalable backend, and has made the development process much smoother.

Do you have plans to use Parse in the future?

We’ve built Elmo Calls and Cookie Calls on Parse, and plan to build more Sesame Street apps on Parse soon.

Do you feel that using Parse has decreased your development time at all?

Parse has definitely decreased development time by at least half.

What Parse features do you find most useful or do your customers love most?

Sending targeted push notifications has been very useful. We use push notifications to notify users of important days such as Elmo’s birthday! We also use push notifications to let users know of new call packs and promotions.

Do you use any other Parse features, such as Parse Data or Social?

We use Data to store all of the video and audio calls for Elmo Calls and Cookie Calls on Parse, and all the data along with them.

One last question; what do you love most about Parse?

What we love most about Parse is the people. Everyone at Parse has been so quick to offer help and assistance with any need of ours. They really care about their customers and the apps on their platform, and we feel that we’re all part of one big team, it’s great.

Courtney Witmer
February 26, 2013

Importing JSON

We’ve supported importing data from a CSV for a while. Now we’re happy to announce you can also import from JSON! CSV is still good for getting simple data types from a spreadsheet into Parse, but more complicated data types like Pointers and GeoPoints can be imported using JSON.

This also means you can import the same format that we export, allowing you to copy data from one app to another easily.

The JSON file can either contain a JSON array of objects in the REST format such as:

[
  {
     "playerName" : "Sean Plott",
     "score": 1337
   }
]

or a JSON Object with a results field:

{
  "results" : [
     {
        "playerName" : "Sean Plott",
        "score": 1337
     }
  ]
}

For more information on importing data read the Data Importing Guide. For more information on the REST Object format, read the Objects section of the REST guide.

Shyam Jayaraman
February 25, 2013

Bytes in the Cloud

We launched Cloud Code earlier to help you write backend logic more easily. Since then, we have added features to enable more use cases. Today, we are adding support for a JavaScript Buffer Cloud Module for manipulating bytes in Cloud Code, performing string encoding, and Base64-encoding binary data. This API is a subset of the Node.js buffer API. The following example converts a Base64-encoded string to binary bytes, appends a unicode smiley character, and then interprets the resulting bytes as a UTF-8 encoded string.

var Buffer = require('buffer').Buffer;

Parse.Cloud.define('smile', function(request, response) {
  var base64String = 'UGFyc2U=';
  var buffer1 = new Buffer(base64String, 'base64');
  var buffer2 = new Buffer(' \u263A', 'utf8');
  var buffer3 = Buffer.concat([buffer1, buffer2]);
  response.success(buffer3.toString('utf8'));
});

Calling this Cloud Function returns the following JSON: {"result":"Parse ☺"}

Parse engineer Bryan Klimt decided to do something even more fun with the Buffer Cloud Module. He wrote a Cloud Function to construct a gradient sphere image, and displayed the image using the Parse JavaScript SDK. The Cloud Function manipulates bytes in Buffer objects to construct the gif image, and then returns the image as a Base64-encoded string.

var _ = require('underscore');
var Buffer = require('buffer').Buffer;

Parse.Cloud.define('gif', function(request, response) {
  var fgcolor = request.params.fgcolor || [0, 0, 0];
  var bgcolor = request.params.bgcolor || [255, 255, 255];

  var magic = new Buffer('GIF89a', 'utf8');
  var logicalScreenDescriptor = new Buffer([64, 0, 64, 0, 0xF6, 0, 0]);

  var palette = new Buffer(128 * 3);
  _.times(128, function(i) {
    var red = Math.floor((i/127) * fgcolor[0] + (1 - i/127) * bgcolor[0]);
    var green = Math.floor((i/127) * fgcolor[1] + (1 - i/127) * bgcolor[1]);
    var blue = Math.floor((i/127) * fgcolor[2] + (1 - i/127) * bgcolor[2]);
    palette.writeUInt8(red, i * 3);
    palette.writeUInt8(green, i * 3 + 1);
    palette.writeUInt8(blue, i * 3 + 2);
  });

  var imageDescriptor = new Buffer([0x2C, 0, 0, 0, 0, 64, 0, 64, 0, 0, 7]);

  var pixels = new Buffer(64 * 66);
  _.times(64, function(row) {
    pixels.writeUInt8(65, row * 66);  // 65 bytes per row.
    pixels.writeUInt8(0x80, row * 66 + 1);  // Clear compression table.
    _.times(64, function(column) {
      var distance = Math.sqrt((row - 32) * (row - 32) + (column - 32) * (column - 32));
      var color = Math.max(0, 127 - Math.floor((distance / 32) * 127));
      pixels.writeUInt8(color, row * 66 + column + 2);
    });
  });

  var trailer = new Buffer([0x3B]);
  var data = Buffer.concat([magic, logicalScreenDescriptor, palette, imageDescriptor, pixels, trailer]);

  response.success(data.toString('base64'));
});

The client browser code uses the Parse JavaScript SDK to call the Cloud Function, and show the Base64-encoded image on the web page.

<html>
  <head>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="http://www.parsecdn.com/js/parse-1.2.1.min.js"></script>
    <script>
      $(function() {
        Parse.initialize("APP_ID", "JS_API_KEY");
        Parse.Cloud.run("gif", {
          fgcolor: [0, 0, 255],
          bgcolor: [255, 255, 255]
        }, {
          success: function(result) {
            var url = "data:image/gif;base64," + result;
            $("#image").html('<img src="' + url + '" />')
          },
          error: function(error) {
            console.error(error);
          }
        });
      });
    </script>
  </head>
  <body>
    <div id="image"></div>
  </body>
</html>

Opening the web page shows the image generated by Bryan’s Cloud Function, as shown below.

sphere

Stanley Wang
February 22, 2013

Webcast Recap: Best Practices of Incorporating Email into Your Apps Using SendGrid and Parse

Thanks to everyone who attended today’s webcast, “Best Practices of Incorporating Email into Your Apps Using SendGrid and Parse,” with Parse CEO, Ilya Sukhar, and SendGrid Developer Evangelist, Elmer Thomas. For anyone who would like a second look, or just in case you missed it, the full video is below.

In the webcast, Ilya and Elmer detailed the 10 best practices for using email for your mobile app. For the demo, Ilya showed how to send transactional emails from the Parse-powered AnyPic using SendGrid’s powerful API.

The sample code Ilya reviewed can be found on GitHub. We’ve also posted the slides on SlideShare.

Ashley Smith
February 20, 2013

Award-Winning Quote Melody Helps Users Find Inspiration

quoteposterfacebookIf you want proof that Parse lets you build an app quickly, here it is! Quote Melody, created by new developers Gintarė Žitkevičiūtė and Aleksandr Pasevin, was not only built in one day during the Facebook London Hack 2012, it also won the Best Open Graph App award at the event.

We spoke with co-founder Gintarė about her experiences using Parse to build the award-winning app.

Tell us a little bit about yourself and Aleksandr.

Aleksandr and I both come from a creative background. Aleksandr started out in graphic design and worked for several years creating designs for clients such as Bentley, Casio and the Terrance Higgins Trust. I studied art critique and business management for my BA, creative entrepreneurship for my MA and worked for several years with a range of art projects and creative companies. We have both been working with interdisciplinary art projects and started our first creative media outlet, www.artpit.org, in Eastern Europe.

We started to code just six months ago. I attended an Apps for Good course where I got an initial grasp of HTML and CSS. We both signed up for a Freeformers TechJam, which is an immersive learning workshop around how easy, quick and inexpensive it is to build things on the web. The emphasis is on enabling non-tech people to make things by remixing what is already out. This led to us building our first app in two days.

Just a month after working with Freeformers we entered the Facebook World Hackathon in London, where we built Quote Melody in one day and it won the best Open Graph App Award. It was amazing, given we were competing against 150 professional developers.

Tell us about your award-winning app.

Quote Melody lets people collect their favorite quotes and discover unique playlists based on each quote they like. It’s a social network and music discovery tool based on quotes and inspiring images. We started from MVP and iterate as we go.

What inspired you to develop Quote Melody?

We came up with the idea during the Facebook World Hackhaton. Facebook had recently opened it’s new offices in London’s Covent Garden and they have amazing quote posters all around it. One quote in particular, “What would you do if you weren’t afraid,” inspired us to build Quote Melody.

And how did you find Parse? What made you decide to use it?

We started to play with Parse at Freeformers bootcamp and we have used it for all of our digital creations since then. It’s an amazing tool for people who are new to the tech world, allowing you to build really quickly and make ideas happen! Without Parse we would never have won at the Facebook Hackathon and built Quote Melody so quickly!

How is Parse used in Quote Melody?

We use Parse for all of our backend. We save data from Facebook login and we even use code snippets to save some of the data we need for statistics. For example, we track and save to Parse how many people come to our app from each story shared on the Facebook ticker.

What are the benefits you have found while working with Parse that keep you coming back?

First of all, the Javascipt SDK. We don’t need to learn PHP or any other server side language! Also, there’s really great documentation and examples; most services are hard to use just because they don’t provide those. We also love how the data we save is organized and how easy it is to analyze and even explain it to clients. It just makes sense so quickly and easily! If we would need to choose just a few words about Parse, we would say it is quick, flexible, efficient, has well structured syntax and provides rapid communication.

It’s great to hear that you’re making such great use of the platform! What would you say is your favorite thing about it?

We love that Parse is a tool that can be used by creative people and early adopters rather than just ‘techies’ and professionals. We also like that the Parse community is getting bigger and it’s getting easier to find answers to problems.

We’d really like to emphasise that you don’t need to be a coder or even a tech person to make great things on the web.  Simple and cheap tools, like Parse, are out there for everyone to use so the most important thing is creativity and diversity of ideas.

Read coverage of the event by Apps Junction and the video of award ceremony as well as coverage by Freeformers. Gintarė and Aleksandr also visited the Wired UK conference to share their story here.

Courtney Witmer
February 20, 2013

Register for the Webcast: Getting Started with Parse

After our last Parse webcast, there were great questions about our functionality, pricing model, and more. Since we want to help you build more with Parse, we’re excited to announce a recurring, “Getting Started with Parse,” webcast which will serve as a great introduction to what we have to offer. Whether you’re curious about what Parse Push is and how we bill you for your usage or are still a little unclear about why you should make the switch to Parse, sign up for the webcast and we’ll help to answer any questions you may have.

A few things we’ll cover:

  • Parse overview including a discussion of Parse Social, Parse Data, Parse Push, and Cloud Code.
  • Advanced push targeting using the Parse Push console.
  • Live code demo of how to get started with PFLogInViewController.
  • Q&A with folks from the Parse Engineering and Parse Sales teams.

View the re-cap of the Getting Started with Parse webcast from March 6, 2013 at 10:30 AM PT here.

Ashley Smith
February 19, 2013

Pushing the Envelope on Push Targeting

Many Parse users want to target push notifications using the creation date or the modification date of installations. This is useful if, for example, you are trying to reach everyone who installed your app in the last week, or to send a love note to users who have been with you for more than a year, so we’ve made it even easier to do. In the push console, you can now create constraints based on the ‘createdAt’ and ‘updatedAt’ attributes of the installation objects:

date_push_console

We hope this feature helps you reach your intended audience, so give it a try!

Andrew Wang
February 15, 2013

Find All The Things

We know that many of our users have been building some great search functionality into their apps, so today we’re making it easier to build scalable search features. A lot of search features are easy to build in ways that will work fine when your app is small during development, but will perform poorly once you launch and your app scales. Nobody wants to see their app getting timeout errors on launch day, so we’re constantly building new features to support more efficient and scalable functionality for our users.

Today we’re announcing another one of those features: “All” queries. You can save a list of keywords in an array field in a Parse Object, and then build a Parse Query to look up the objects that match all members of a list of search terms. In fact, this works not only for arrays of string keywords, but for any data type that can be stored in an array field of a Parse Object. We’ve implemented direct support for this in our iOS & OS X, Android, JavaScript SDKs, and our REST APIs, and we’ll be releasing new Windows SDKs with support for it in the next few days.

For example, on iOS or OS X you can use them like this:

PFObject *object = [PFObject objectWithClassName:@"Friends"];
[object setObject:@"Alice" forKey:@"name"];
[object setObject:@[@"chocolates", @"flowers", @"candy hearts"] forKey:@"gifts"];
[object save];

object = [PFObject objectWithClassName:@"Friends"];
[object setObject:@"Bob" forKey:@"name"];
[object setObject:@[@"candy hearts", @"flowers", @"wine"] forKey:@"gifts"];
[object save];

object = [PFObject objectWithClassName:@"Friends"];
[object setObject:@"Charles" forKey:@"name"];
[object setObject:@[@"candy hearts", @"wine", @"chocolates"] forKey:@"gifts"];
[object save];

PFQuery *query = [PFQuery queryWithClassName:@"Friends"];
[query whereKey:@"gifts" containsAllObjectsInArray:@[@"chocolates", @"candy hearts"]];
NSArray *friends = [query findObjects];

We hope this feature will make it easier for you to develop scalable search functionality. So, go ahead and download the latest SDKs and try out this new feature. We’re excited to see all the things you’ll build with it.

Brad Kittenbrink
February 13, 2013

Archives

Categories

RSS Feed Follow us Like us