Offline app with Meteor and Cordova

Whilst Meteor is such a terrific platform, it is relatively new (hit the 1.0 version only in the end of October 2014) and some resources/techniques are still to be explored. The official Cordova support is not even one-year old and this of course leads to lack of documentation.

I sensed that one of the biggest requests by the folks who are using Meteor + Cordova in their mobile projects was the ability to run their apps offline. In my first attempts I’ve set up a GitHub repository that worked offline and exposed the difference between normal and grounded collections. Today we will take it to the next level and create a proper mobile application.

Our project will be to build a “Contacts” app that stores name and e-mail, having the following requirements:

  1. The app shall have a web interface (not only mobile);
  2. The app shall use a schema to validate data client and server side;
  3. The app shall be able to insert/update/delete information online as well as offline;
  4. The app shall synchronize automatically when internet connection is detected;
  5. The app shall persist information offline even if it is closed.

1. Setting up the environment

Let’s get over it, them! To begin with, make sure you have Meteor correctly installed in your environment and Cordova properly set for your target platform (we will use Android today but iOS should behave similarly). After installing, let’s create our new project. In your terminal, type:

$ meteor create offlinecontacts
$ cd offlinecontacts

You will be able to navigate to the recently-created offlinecontacts/ folder and see the three automatic generated files in there.

Now let’s add some pretty standard packages (although not obligatory) just to show how they can integrate easily in this approach. In particular, the aldeed:autoform and aldeed:simple-schema packages can be a bit tricky when dealing with offline data. To style our app, let’s use meteoric (no-angular Ionic solution for Meteor). Once our routing logic is pretty straightforward, Iron Router will be our choice here. Finally, the only core package for this project is the amazing GroundDB – the piece that makes offline data possible.

Go to your terminal and add the following:

$ meteor add aldeed:autoform
$ meteor add aldeed:simple-schema
$ meteor add meteoric:ionic
$ meteor add meteoric:ionic-sass
$ meteor add meteoric:ionicons-sass
$ meteor add fourseven:scss
$ meteor add ground:db
$ meteor add iron:router

Almost there! The only thing missing is to add Android as a platform for this project.

$ meteor add-platform android

Done. Plug a USB cable on your phone and make sure it’s set up for development. If everything is ok, it’s time to test it coming to life in your device. Go ahead and type:

$ meteor run android-device

After a while you will see it in your phone. Note that Iron Router will complain with you once there is no route defined. Let’s fix that and organize our app.

2. Structuring the app

Meteor gives you a lot of freedom when designing your app structure but going further on that is beyond the scope of this tutorial. Let’s make things simple instead.

Delete all files automatically generated for you inside the offlinecontacts/ folder and create four others: client/, collections/, lib/ and server/. All code inside client/ will be sent only to the client and the same logic is applied to the server. The remaining two folders are visible to both.

Create now client/css/. This folder will contain all css used in the application. In our case, we will just add the dependency required by meteoric. Create a file called app.scss and  leave it there:

Great. Now go back to the client/ directory and create a contacts.html file.

This is the standard meteoric markup. Currently we don’t have anything useful to display but let’s stick with it for a moment. Time to configure our router and see if everything is ok. Go to your lib/ folder and create the router.js file.

Now the router shouldn’t complain anymore. Save your files and observe the changes both on browser and mobile. You shall see an empty screen apart from a header. Nice.

3. Creating the collection

Time for doing something more specific. Let’s create a Contacts collection and a schema for it. Inside your collections/ folder, create a contacts.js file with those lines:

This is far more interesting and has some tricks. Let’s go over them:

Lines 1-18 define the schema thanks to the aldeed:simple-schema package. We are doing some straightforward validations (limiting the name size, for example) and defining what fields are mandatory. At first sight seems that createdAt and lastUpdated will be optional, but that’s not the case. We will see why later.

Lines 20-22 create the regular Meteor collection and ground it in the case we are running on device. This tells the app to store everything this collection sees into the local storage. But we still have to get the data and test if it will persist even if offline.

4. Handling the subscriptions

Ok, we have our collection and it’ll be grounded with we are on mobile. Now we need to subscribe to the data we’ll be displaying and grounding. Our subscriptions will be on template-level, fact that gives us more control over the readiness of our data. Create a file called contacts.js inside your client folder:

The only different thing here is the line 4. Here, we will just subscribe if connected to the server. “Why?”, you would ask me. Because you will create a new file inside client/ called init.js:

And that’s the reason. If we are on the device, the subscription will be done during the startup. This of course leads to performance concerns but in order to have all data available offline you need to ground them first! So you will ground them when online and be able to play with the data whenever you want even without internet connection. Of course if you do have connection, we avoid subscribing to all the data at once.

This approach might be a huge problem for apps with large amounts of data but it’s up to you to decide if you want all the data inside your local storage. Luckly, raix (the author of the GroundDB package) already said that intelligent-subscriptions are on radar. I strongly recommend you to keep an eye to his GitHub repository and pay attention to every release.

You may have noticed that we are subscribing to a publication yet undefined. Let’s fix it. This will be on server:

We are saving some time and defining a publication for returning just one contact, which will be useful later. Don’t forget to get rid of the autopublish package that comes by default:

$ meteor remove autopublish

Last piece of puzzle is to actually render the data in our template. Go back to the contacts template of contacts.html and update the {{#ionContent}}:

Note the Template.subscriptionReady helper that will wait for all the subscriptions before rendering the list. I have used the sacha:spin package to render a spinner otherwise, but you can use any other that best suits you (meteoric comes with a default loader).

That sounds good. Go check your job so far. Of course you will not see any data, so open your browser console and type some dummy data like Contacts.insert({name: “Rafael”, email: “”}) and see how we are going (spare a time to appreciate the beauty of the reactivity happening on your device as well).

5. Adding and modifying data

So far we have de “R” but our app is lacking the “C”, “U” and “D”. In order to accomplish this, we will rely heavily on the autoform package – but bear in mind the idea behind would be the same regardless of which rendering engine you are using. Go back to client/contacts.html and add the following templates:

The use of <div> instead of {{#contentFor}} as the Ionic header is to avoid a overlapping bug I’ve seen in some devices. Aside from the {{> quickForm …}} clause, everything should seem straightforward. Let’s analyse then what’s is going on with it:

  • The schema has to be explicitly declared instead of attaching it directly to the collection. This is a feature of Collections2 package which we will not use;
  • The form type is normal. We will see later more about it but you can check the documentation first;
  • The createdAt and lastUpdated fields are omitted, although they were defined as optional by the schema. We will handle them later.

For now, change the contacts template again to include paths to create and edit contacts:

And adjust the router:

Surely some fixes have to be done. First we want to remove a contact when the red square is pressed. Then the contact data should populate the edit form when any click or touch is performed on the contact. Modify clients/contacts.js and add the following:

The only snippet that should sound different here is the lines 3-9. There we are asynchronously calling the removeContact method passing the contact’s id as parameter. Before having a look at it, let’s handle the form.  In the same file, add the following:

According to the right way to use the normal method of autoform, we need to define a onSubmit event hook attached to the form id. The functions are pretty straightforward. They gather all the information and pass to a method (in the edit case modifying it a bit in order to ease things server-side).

The main question here is why we need this tricky attempt. Turns out that GroundDB requires us to explicitly say which methods should be cached when offline (remember that methods run both on client and server). This way we can easily write our methods and cache them to work without connection and persist even if the application shuts down.

Go to collections/contacts.js and add the following:

This part should also bear no problem. Note in lines 4 and 11 we are validating the input against the schema defined. Lines 5 and 9 solve our problem with default values – if we are using methods in the end, let’s add the values there before saving/updating (but still we had to define it as optional otherwise the client-side validation wouldn’t let the form be submitted). Finally the lines 20-26 tells GroundDB to resume the methods in the client.

That’s pretty much all! Play around with the app (note: if you are experiencing a white screen when typing on Android, refer to this topic. It’s a bug that has been around for a while but has a simple solution, just clone the package into a packages/ folder at the root of your directory and does the changes stated). Remember that styling was not my concern in this tutorial, so the form might be a bit ugly (you may want to check the Ionic autoform package or style it better with Bootstrap).

If you are happy with your app’s functionalities, try now cutting your mobile connection off. Add, edit and remove data offline. Close the app and reopen it again to see if persists. Go online and see the sync happening. It’s alive 🙂

Note: GroundDB does the sync resolution for you but in a pretty naive way. According to the docs: “At the moment the conflict resolution is pretty basic last change recieved by server wins. This could be greatly improved by adding a proper conflict handler.” You may expect changes in the future.

You can check the repo for this app here. Clone and modify it if you want. Please feel free to give any feedback once this is more a research and any improvement/input will be highly valuable.

18 Comments on “Offline app with Meteor and Cordova

  1. Crisp and Precise! Thanks for writing this, I know there aren’t many articles written about handling offline behaviour on meteor mobile apps.

  2. the meteor add meteoric:ionic-sass , the meteor add meteoric:ionicons-sass not working,

      • there is some problem with their repo and they haven’t added some dependencies or something. I am downloading them as zip and compiling it myself.

        • i had the same problem.
          meteor update solved it

    • …in your app’s .scss file:

      @import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionic-sass/ionic’;
      Due to a current limitation of the Meteor packaging system, the above path may not exist the first time you run your Meteor app after installing this package. This will cause an error saying the file to import was not found. This may also occur if you run meteor reset. Restarting your app should fix this problem. See meteor/meteor#2606 and meteor/meteor#2796 for more info.


  3. Hi Rafael!

    Thank you for sharing your experience with us!

    If I understand well, your application only works offline when compiled as an ios or android native app. But it cannot work offline as a webapp, even after using ios’ “add to main screen” feature.

    If we wanted to make it work that way, I believe that it suffice to add the appcache module to the project, ( so that the app’s resources are stored in safari’s local storage. Do you think that’s the right way to go?

    Thanks in advance for your response!


    • You are right. Running as webapp will require you to cache the files. This repo might be more helpful, and yes appcache is the best alternative I see (although its limit is pretty small).

  4. Oi Rafael, você já teve alguma experiência usando o GroundDB em produção em um aplicativo com vários usuários? No geral, qual a sua percepção sobre ele?

  5. Oi Rafael, achei o seu blog por acaso na internet fazendo algumas pesquisas sobre o GroundDB. Tenho algumas perguntas sobre ele, se você puder me ajudar.. 🙂
    Você já teve alguma experiência usando o GroundDB em produção em um aplicativo com vários usuários? No geral, qual a sua percepção sobre ele?

  6. Hi Rafael,
    Thanks for nice article.
    I have one question. If I wanted to extend app for possibility to make a call to number in contact how could I do that? Is it difficult? Thanks in advance some hints.

  7. here’s how to solve the ionic missing file problem: Took me a while, from austingayler here:

    meteor remove fourseven:scss
    meteor remove meteoric:ionic-sass
    meteor remove meteoric:ionicons-sass
    meteor add fourseven:scss@2.0.0
    meteor add meteoric:ionic-sass
    meteor add meteoric:ionicons-sass

    Run Meteor
    Comment out the
    @import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionic-sass/_ionic’;
    @import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionicons-sass/_ionicons’;
    Let it rebuild -you should get an app with no styling
    Then comment back in and the project will load correctly

  8. I am not familiar with GroundDB yet, but I see you are using server method to save the data from the quickForm. Wouldn’t it be the same if you use the meteor way (saving the data locally and wait for data synchronization)?
    I am guessing it is mandatory but I cannot explain why…

    Also, Is there any reason for not using Iron Router waitOn method for data subscriptions ?

    Thanks for the tutorial !

  9. Hello Rafael.
    Thanks for sharing your experience! Mobility with Meteor is not described enough. I have one question about authentication. If you have existing user in web application how to authenticate from mobile? Did you have experince?

  10. Hi, nice to have some good examples for mobile, thanks.

    When I open a browser, android and iOS app at the same time the browser works fine because it is always connected to the server, but when I disconnect the android it only shows contacts added on this device, contacts added through the browser, even if previously displayed on the android, disappear. Is this intended behaviour, why doesn’t it download the data to local db for use when offline?

    The iOS device never shows any data. I think there is a bug with iOS because this happens on other apps as well, it cannot connect to the server even if I specify the -p or –mobile-server options. Can anyone help with this problem, I have tried several fixes but none of them work for me? Help greatly appreciated.

  11. I ended up changing the autoform.addformtypes() directly, since onSubmit is not called if you use {{# autoform.. and not {{> quickForm..

    Only problem persisted is right now if the app or smartphone is shut down and restarted, then some documents are missing. Now I am wondering whether to use Ground:DB Version 2 or try to solve this problem in 0.3.14?

Leave a Reply

Your email address will not be published. Required fields are marked *