Let’s suppose you need to insert a piece of data (let’s say, a post) in your database.

Pretty common operation. You could do something like this:

Template.newPost.events({
  'click .button': function (event) {
    event.preventDefault();
    var postContent = $('.postContent').val();
    Posts.insert({postContent: postContent});
  }
});

Thanks to Meteor’s whole “database everywhere” thing, calling Posts.insert() on the client will work just as well as calling it on the server.

There’s one caveat though: once you remove the insecure package (more about this in our article about Meteor security), database modifications from the client are not allowed (and they shouldn’t!).

So if you want your update to go through, you’ll need to set the proper permissions using an allow callback:

Template.newPost.events({
  'click .button': function (event) {
    event.preventDefault();
    var postContent = $('.postContent').val();
    Posts.insert({postContent: postContent});
  }
});

Posts.allow({
  insert: function (userId, doc) {
    return true;
  }
})

Using Methods

So far so good. But if you’re familiar with Meteor, you know there’s another way to accomplish exactly the same thing, by using a Meteor Method.

Template.newPost.events({
  'click .button': function (event) {
    event.preventDefault();
    var postContent = $('.postContent').val();
    Meteor.call('newPost', {postContent: postContent});
  }
});

Meteor.methods({
  newPost: function (post) {
    Posts.insert(post);
  }
})

Not only do both techniques do the same thing (insert a post in the database), they even do it in the same amount of lines of code. So why are there two ways, and more importantly which one should you use?

Don’t Trust The Client

First, let’s look at the pros and cons of doing our insert() on the client.

I’m sure most of your users are wonderful people who wouldn’t harm a fly and would never even dream of hacking your app.

Still, it’s not all unicorns and rainbows out there, and as a general principle you probably shouldn’t trust data coming from the client.

So the problem with allowing Posts.insert() on the client is that you’re opening the door to people entering not just posts, but any kind of data in your database.

All they’ll have to do is open up their browser console and type:

Posts.insert({foo:'bar'});

Or even worse, what if they decide to sow mayhem by inserting a well-formatted post, but assign it to another user?

Posts.insert({author: 'Sacha', postContent: 'Meteor sucks!'});

Allow & Deny

So instead of simply allowing all inserts wholesale, we can do a few simple security checks first.

For example, we can make sure no extra properties besides postContent are included in the operation by getting a list of the document’s property keys, removing postContent, and then verifying that the resulting array is indeed empty:

Posts.allow({
  insert: function (userId, doc) {
    return _.without(_.keys(doc), 'postContent').length === 0;
  }
})

Collection Hooks

The only problem is that Allow/Deny are all or nothing operations. You can allow or deny the insert, but you can’t do something like, say, adding a timestamp.

You could do this on the client, but then we’re back to square one: we have no way of controlling the validity of that timestamp. After all, we don’t want people to be inserting posts dated January 27, 2067 (Note: if you do happen to be reading this post from the year 2067, please substitute an appropriately far away date. Also, is Meteor 1.0 out yet?).

So instead, we can use an insert hook that will modify the document on the server before the insert takes place.

Meteor doesn’t provide that feature natively, but the great collection-hooks package does:

if (Meteor.isClient) {
  Template.newPost.events({
    'click .button': function (event) {
      event.preventDefault();
      var postContent = $('.postContent').val();
      Posts.insert({postContent: postContent});
    }
});
}

if (Meteor.isServer) {}
  Posts.before.insert(function (userId, doc) {
    doc.createdAt = new Date();
  });

There’s something very elegant about having only a single Post.insert() in your code, with all the boring validation and sanitization logic happening behind the scenes, even though this pattern does also present a few drawbacks.

UPDATE: If you’d like to learn more about collection hooks, you can check out this article.

Denial Not Allowed

The Allow/Deny pattern is also very easy to get wrong, exposing security holes in the process.

Allow/Deny lets you do any kind of update from the client. So you can simply $set a field, but you can also $push to it, $unset it, $addToSet it, and so on.

Yet this added flexibility has a cost: it can become a challenge to maintain a schema or rules about what is allowed in which fields.

Let’s consider a practical example: upvoting posts.

Introducing MeteorPad

You can see the full code for this example and play around with the console commands over at MeteorPad, the awesome Meteor playground built by our friends from MadEye.

Every time a user upvotes a post, we’ll add their _id to a upvoterIds array on the post document.

Template.post.events({
  'click .upvote': function (event) {
    event.preventDefault();
    Posts.update({_id: Session.get("currentPostId")}, {
      $addToSet: {upvoterIds: Meteor.userId()}
    });
  }
});

Of course, we don’t want to let users add other people’s _ids to that array and game the system. So our allow callback might end up looking something like this:

Posts.allow({
  update: function(userId, doc, fieldNames, modifier) {
    return modifier.$addToSet.upvoterIds === userId;
  }
});

If the user is trying to add their own user _id to the array, things will proceed just fine. In any other case, the allow callback will fail and the write won’t go through.

You can see for yourself by typing this in your browser console:

Posts.update({_id: Session.get("currentPostId")}, {$addToSet: {upvoterIds: "1337"}})
// > update failed: Access denied

But wait, don’t give yourself that pat on the back just yet. Can you guess what will happen if someone opens up their browser console and types the following code?

Posts.update({_id: Session.get("currentPostId")}, {
  $addToSet: {upvoterIds: Meteor.userId()}, 
  {$set: {title: "Hello World!"}}
})

That’s right! By only checking the $addToSet modifier, we’ve opened the door to people inserting whatever they want in our database, including overwriting other fields!

Of course, it’s always possible to write tighter, better allow/deny code. In fact, we’re hoping this post helps you do just that!

But hopefully, this example helps illustrate that client-side operations can sometimes be tricky to get right, and that small mistakes can have big consequences.

Conclusion

Client-side operations are nice in theory, but they can also be really hard to get right. What’s more, splitting your code between the client, allow/deny callbacks, and hooks often ends up making it harder to keep track of all the moving parts.

Even when using something like the excellent Collection2 package to help enforce a schema and reject invalid writes, there’s just too much that can go wrong when trusting the client.

So there’s your answer: client-side operations are fine during the initial prototyping phase, but for a real-world production app, Meteor methods will usually prove a safer approach!

P.S. Part two of our sync/async in Meteor & JavaScript series is coming soon! Sign up for our newsletter below to be sure not to miss it :)