Apple Push Notifications on Rails

The other night I submitted a new iPhone application to the Apple Store. The app, which I’ll speak about when, and if it gets approved, uses the new Apple Push Notification service available in iPhone OS 3.0. On the server side I have a Rails application that I am using to send the notifications to Apple. The problem I ran into was how.

Enter the APN on Rails gem. While searching I found one plugin for Rails that mostly worked for me, Sam Soffes’ apple_push_notification plugin. It was a great place to start, but I found that there were things that didn’t suite me. For starters, not having any tests is always a big turn off for me when it comes to any code. I also didn’t like that you didn’t need to save a notification in order to send it. That means you don’t have a record of what was sent and when. I also wanted to have devices stored separately from the notification. Finally, I wanted to be able to easily configure the plugin. Sam’s was using constants that would need to be changed when it hit production.

So, with all that said and done I took Sam’s great work, ripped it apart, and put it back together again, this time in gem form instead of a plugin, and here it is.

There are a few migrations, a few models, and a few Rake tasks, but here is the basic idea of how it works:

To get a better understanding of exactly how it works, and what it does, I highly recommend reading the RDOC.

There are a few things I still would like to add, for example, a controller to do CRUD for devices so iPhones can register with the Rails app. I’d also like to add a task that talks to Apple and finds out which devices are no longer accepting messages so they can be removed.

If you’d like to contribute, please feel free and for the project on GitHub:
http://github.com/markbates/apn_on_rails/tree

Again, a special thanks to Fabien Penso and Sam Soffes for their initial work on this project.

Tags: , , , , , ,

15 Responses to “Apple Push Notifications on Rails”

  1. Jerod Santo Says:

    Very cool Mark, thanks for releasing this!

    I have been using [[UIDevice currentDevice] uniqueIdentifier] to register unique IDs with my Rails app. Is the device token sufficient to replace my current unique string?

    I agree that adding a RESTful controller to the gem would be a nice addition. How are you aggregating device tokens currently?

  2. Mark Bates Says:

    Hey Jerod, glad you like it. Here is a trimmed down (ie, I took out app specific stuff) version of what I have in my main app delegate:

    http://gist.github.com/154957

    That’s how Apple recommends you set your app up for push notifications. It will automatically give the users the pop-up asking them about APN and install it in the systems settings.

    As you can see the call back gives you a NSData version of the device token, which is what you need to send to Apple with the notification message you want to push.

    I have a Device object that uses ObjectiveResource to talk to my Rails app and register the device token.

    Does that help?

  3. Jerod Santo Says:

    Mark-

    Yes, that helps. Thanks.

    Once installed and migrated, I assume we can safey modify the devices table without causing any problems. For instance, I’d like to associate each device to a User, which would require an extra field. No problems there?

    As far as adding support for removing devices based on Apple feedback, maybe take a look at this project on GitHub which seems to have done most of the work already (again, sans tests).

    http://github.com/jdg/Feedback/tree/master

  4. Mark Bates Says:

    Thanks for the heads up on the feedback project Jerod, it definitely looks like it is a good start to get me where I need to be. I’ll work on getting it incorporated.

    You should be able to add to a user_id column onto the devices table. I might make modifications to the table down the line, but user_id won’t be a column I add. Worse case scenario you might have to modify a future migration to avoid conflicts, but that’s pretty darn easy.

  5. Carson McDonald Says:

    Am I missing something or will this make an APN connection for any call to notifications? If that is so you may want to warn people not to call it for every message. I see there is a rake task but even that could cause issues if people try to call it every minute.

  6. Mark Bates Says:

    Hey Carson, not to worry it doesn’t make a call to Apple for every message. When you call APN::Notifications.send_notifications it finds all the unsent messages in the db. If there are messages that need to be sent, then it opens up one connection to Apple and pushes all the notifications through that. If there aren’t any messages, then it does nothing. Of course, it is possible to call that method after you create a new notification, but there’s not much I can do about that. Apple will shut those people down pretty quickly for constantly opening/closing connections.

    Does that answer your question?

  7. Mark Bates Says:

    Carson, I also just updated the README to be a bit more explicit about this:

    
      $ ./script/console
      >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX")
      >> notification = APN::Notification.new
      >> notification.device = device
      >> notification.badge = 5
      >> notification.sound = true
      >> notification.alert = "foobar"
      >> notification.save
    

    You can use the following Rake task to deliver your notifications:

    
      $ rake apn:notifications:deliver
    

    The Rake task will find any unsent notifications in the database. If there aren’t any notifications
    it will simply do nothing. If there are notifications waiting to be delivered it will open a single connection
    to Apple and push all the notifications through that one connection. Apple does not like people opening/closing
    connections constantly, so it’s pretty important that you are careful about batching up your notifications so
    Apple doesn’t shut you down.

  8. Carson McDonald Says:

    Mark, yep that change to the README is what I was thinking. People should be somewhat careful sending the notifications as it seems there is some threshold that gets you shut down.

    I’ve actually been thinking it would be nice if there was a module for something like RabbitMQ that would just keep a persistent connection open and let the user do the notifications using normal message queues and get removes via subscriptions.

  9. Mark Bates Says:

    Carson I would love to see a project like that! The question is will Apple let you keep a connection open constantly like that? I suppose if it gets dropped you could detect it and re-open it. I still fear that they might not be too thrilled about an ‘always-on’ connection.

  10. Fabien Penso Says:

    Apple asks for it, they said they had no problem with you keeping connected during WWDC. That’s what’s my (private) version of my Rails plugin is doing, using backgroundrb. I just had no time to release it to the public as I want to first run my service, then I’ll clean a bit of the code and release it on github.

  11. Mark Bates Says:

    Fabien, that’s good to know. I know they like it when you bulk deliver, but I didn’t think they would be good about having a persistent connection.

    With the latest version, 0.3.0, I’ve wrapped the connection stuff, so you could pretty easily have it run forever with something like this:

    I might put that in that version, a binary that you can run in the background that opens the connection and keeps polling for new notifications and delivers them.

  12. Corey M Says:

    Hi Mark, First up thanks in advance for this wonderful gem, its fantastic how easy it is to get up and running.

    I’m wondering if you have any benchmarks for large scale notifications (say 10000 devices on 5 min intervals). I’m wondering about the consequences of having a join to get to the device token when you could store the token as a 32 byte blob right on the notification. Some quick console hacking.

    >> token_in = “ffffffffdbc0ce239b3d15e96efe4525941ebb8d8b65bc241eceffffffffffff” #Masked for security
    => “ffffffffdbc0ce239b3d15e96efe4525941ebb8d8b65bc241eceffffffffffff”
    >> token_in_int = “0x#{token_in}”.to_i(16)
    => 111987304994859507620189752773925056744031439616140324133437380330181909811521
    >> token_in_int.size
    => 32
    >> token_in.size
    => 64
    >> token_out = token_in_int.to_s(16)
    => “ffffffffdbc0ce239b3d15e96efe4525941ebb8d8b65bc241eceffffffffffff”
    >> token_out.scan(/……../).join(” “) == token_in.scan(/……../).join(” “)
    => true

    I’m not sure how easy it is (or isn’t) to attach a byte array to an AR::Base but for an app we’re building we’re dealing with a lot of notifications to a lot of devices so I’m wondering if getting the token down to a 32 byte blob would make it easy to just tack it onto the notification and save a join in the query.

    Just a thought,

    Corey

  13. Mark Bates Says:

    Hi Corey, I’m glad you like the gem. I aim to please. :)

    I can’t say I have those types of benchmarks, as I’ve been working on a much smaller scale in terms of notifications. I designed the plugin to use normalized data, so I didn’t have to keep storing redundant info in the notifications table. You could easily pull it all back in one query, if you think that’s a bottleneck. What I can say is that pulling 10k records in 5 minutes with AR is totally doable, I don’t think that’s where any bottlenecks would occur. If you get some benchmarks I would love to see them.

    As for storing byte arrays to AR, I’m sure it’s possible, again it’s not something I’ve had to do before with AR. If you want to fork the project on GitHub and can come up with some great performance enhancements that make sense, I’ll gladly pull that back into the project. I love when others contribute and make the project better.

  14. Sam Soffes Says:

    Looks cool. Thanks for the kind words. Yours looks pretty cool.

  15. James N Says:

    Do you know if your plugin will allow for notifications to be sent for unlocked phones? I heard that it doesn’t always work with unlocked phones…any details on this subject? Thanks.