Multitenancy and Meteor


#1

Hello Meteor dev’s. I was hoping to begin a conversation regarding architectural approaches to Multitenancy in Meteor. This conversation has been brought up in the meteor-talk group and stackoverflow. Various packages, such as mizzao:partitioner tackle the problem, and I found a meteor multisite example project on GitHub.

As Marcel Onck pointed on the google group, “Multi-tenancy is pretty common nowadays with SAAS being promoted so strongly”. I haven’t seen any common strategies for tackling it in the Meteor framework. I’m seeing developers trying, but the approaches are pretty unique. The GitHub project uses publication overloading. Developers are tackling the multitenancy problem at all levels on the stack.

Fundamentally, all of these strategies fall in the camp of using a single database with filtered queries (a more pure multitenancy solution by definition), or running separate processes and instances for each tenant (the “less elegant” approach).

I like the mizzao:partitioner package, but my SaaS project can’t depend on authentication for partitioning. Any site visitor should be able to be recognized as belonging to a tenant group based solely on the url they were directed to my app, so I couldn’t use that package.

I tried creating a Hostnames collection to store each tenant’s customizations (link colors, branding, etc.) and that was going well until I considered subscriptions for my Posts collection. There is an edge case where if you visit two tenant’s sites as the same time, subscriptions based off of the site Hostname were incorrectly rendering since each site kept overriding the subscription parameters (a very strange behavior to observe in practice).

At this juncture, I’m leaning towards simply using the less elegant solution of having a unique Meteor process and Mongo database for each tenant. It seems like Meteor wasn’t built to handle the multitenacy problem, or I haven’t thought about it the right way for it to seem so.

If anyone else would like to chime in, I’d appreciate some fresh perspectives.


Best way to architect a multi-tenant SaaS?
Using Multiple Domains With One App
Guide Chapter: Multi-tenancy
How can develope a SAAS application in meteor?
Multiple apps or multiple databases?
#2

I have the same problem and I share the same ideas. Currently I have in mind use the less elegant solution too (an process for each consumer).
I think that the first thing to do, is allow a way to connect with another database in runtime, like:

function onLogin (){
Mongo.connect(‘mongodb://…clientDbUrl…’);
}

Simple idea, but allow us to have, at least, one database for each consumer.


#3

I’ve x-posted to Reddit, and there’s been some good discussion. Since I originally posted, I had re-considered my architecture. Ultimately, having client-specific builds seems to carry more baggage then benefit. With pure multitenancy, release management is simpler and architecturally far more cost efficient.

So we’ve had to make a couple changes to support multiple tenants with a shared database and application instance. There have been two key pieces for my multitenancy implementation:

  1. The definition of a Tenant collection to store tenant-specific metadata (with each associated collection storing a tenantId), and

  2. Publication overloading (e.g., putting dynamic content in the publication name) based off of the tenantId.

var tenants = Tenants.find();
tenants.observeChanges({
      added: function (tenantId, tenant) {
        Meteor.publish("someOtherCollection-" + tenantId, function () {
          return SomeOtherCollection.find({tenantId: tenantId});
        });
      }
});

This is much different from:

Meteor.publish("someOtherCollection", function (tenantId) {
  return SomeOtherCollection.find({tenantId: tenantId});
});

Because you won’t be able to simultaneously view two tenant instances. Your subscriptions will collide and things will get weird.


Guide Chapter: Multi-tenancy
#4

Hi, thanks for this, this is very useful information.

Would you be able to explain to me a little bit more about the publication overloading. What is the need for this over simply using the tenantId in the find()?

Many thanks


#5

Here’s a use case (albeit a corner one) for why you must do publication overloading.

Let’s suppose I have a multitenant application shows a list of blog posts. On the homepage of each site, there are a list of blog post links, which will take you to a new page for that blog post. In this app, all tenants share a common database and and they share this publication function:

Meteor.publish('posts', function (tenantId) {
   return Posts.find({tenantId: tenantId});
}

Here’s the use case:

  1. Load website for a tenant (tenant-1.mymeteorapp.com). The posts subscription loads all posts for tenant-1.
  2. Load website for another tenant (tenant-2.mymeteorapp.com). The posts subscription loads all posts for tenant-2, and through DDP, refreshes the data in our browser table for tenant-1.

#6

Oh I see, so because the publication is called ‘posts’ on both tenants, when a 2nd user accesses tenant-2 the posts being published are all changed to a tenant-2 and this is shown to tenant-1.

So what does the subscription look like on the client end? Do you get the tenantId from the user object or url?


#7

To clarify, if you’re only browsing one tenant at a time, it’s not a problem. For my use case, we want our tenants to be able to view one another’s instances in the same browser. Ultimately, the issue stems to the fact that publications aren’t bound to each tab.

I have a publication that uses HTTP headers to identify the host, which is associated with a document in a Tenant collection. Once the client loads their tenant object, I subscribe to that client’s specific publication (see the first code sample in Post #3).

Using iron:router, a route might look something like this:

if (Meteor.isClient) {
  Meteor.subscribe('currentTenant');
}

...

this.route('index', {
  path: '/',
  template: 'index',
  waitOn: function () {
    if (Tenants.findOne()) {
      return Meteor.subscribe("posts-" + Tenants.findOne()._id);
    }
  }
}

I should really make a repo for this to more clearly illustrate the concepts discussed in this thread.


#8

I’ve built a multi-tenant site where the look feel and content are determined by the FQDN or an ID present in the path. I’ve never encountered this issue you are describing. I would look at your pub/sub logic a little bit closer, it sounds like your defining the data to publish globally, rather than per client.


#9

I’ve also built a multi-tenant site that serves entirely different data via pub/sub based on the FQDN and/or ID values in the path. After reading the above, I went and had a careful check, opening two tabs and browsing through equivalent routes for two different tenants, changing routes so that subscriptions were changed, etc… There was no problem with pub/sub content colliding across tabs. The reason for this, I think, is that the subdomains are different for different tenants and each subdomain requires a separate login to the app, even when browsing different tenants’ subdomains using the same Meteor user. (Note: I’m running a single app instance, with a single db, both of which are used by all tenants).


#10

Thanks for the replies! I am going to go back and double check things. It’s good to hear that others have had good success with multitenancy. My app’s publications aren’t limited to authenticated users, so I’ll investigate more to see if that could’ve been part of my issue.


#11

I haven’t fully decided which way to go with separating my app. But the different tenants will be entirely isolated and users will only belong to one tenant. I am thinking of just basing it on logged in user. I have a tenantId being published to the userData and is available on the client, but how do I store it and pass it to the publication? It seems I can’t access Meteor.user().tenantId in the publications file. I get an error about it needing to be in a method?


#12

How are you doing this?


#13

Try:

Meteor.publish("postsOrWhatever", function (tenantId) {
  check(tenantId, String);
  var user = Meteor.users.findOne({_id: this.userId});
  var tenantId = user && user.tenantId;
  if (tenantId) {
    return PostsOrWhatever.find({tenantId: tenantId});
  }
});

#14

This is a ripped down version of what’s in my main.js file in the client:

if (!Session.get('tenantId')) {
  var hostnameArray = document.location.hostname.split('.'), subdomain;
  if (hostnameArray[1] === 'mydomainname' && hostnameArray[2] === 'com') {
    subdomain = hostnameArray[0];  
  }
  if (subdomain) {
    Meteor.call('findTenantBySubdomain', subdomain, function(err, res) {
      var tenantId = res;
      if (tenantId) {
        Session.set('tenantId', tenantId); 
      }
    });
  }
}

It’s not pretty, but it works. Note: subdomains are unique to tenants, so we can get the tenant _id value by finding the right document by subdomain:

Meteor.methods({
  "findTenantBySubdomain" : function (subdomain) {
    check(subdomain, String);
    var tenant = Tenants.findOne({subdomain: subdomain});
    if (tenant) {
      return tenant._id;    
    }
  }
});

#15

I would only add that you may want to put the domain > tenant lookup inside a client side startup callback, so that it’s guaranteed to run before the rest of your route, helper, and XYZ rendering code.

Also, if you’re using Iron Router, make sure to make proper use of waitOn() and ready() for your tenant based pub/sub data to come into context, in order to prevent re-rendering flashes. That’s not really a tenant specific thing, but generally useful and nice if you don’t want the page to pop from generic to tenant specific.


#16

If you’re using a user based tenant lookup instead of a FQDN based lookup, then the startup approach isn’t what you want, since a user may or may not be logged in when they hit the site.


#17

To switch gears for a moment, if I may, for those of you that have implemented multitenancy, what is your approach for authentication? Each tenant of my application has an existing authentication protocol (e.g., SAML, CAS, etc). I was considering building a separate app, purely in Node.js, that would serve as a federation. Then my meteor app would be OAuth client, and the federation would exist as a OAuth Provider and act as a bridge to the various authentication strategies my tenants use (SAML, CAS, etc). Would that be feasible? Thanks in advance to various commenters, the discussion has been quite valuable.


#18

I haven’t gone too far down the path of supporting existing authentication protocols. In the next few weeks, I’m going to give tenants a way to use LDAP to let their users authenticate. I’ve got as far as hacking an existing LDAP package to support this (babrahams:accounts-ldap), but haven’t integrated it into anything beyond a test app yet.

I’m not even sure that this is a good path to go down – hacking existing LDAP, CAS, SAML meteor packages, as required, to support mutli-tenancy. I do like the sound of the federated approach that you’ve outlined. If you have success with that, I’d really like to hear about it.


#19

We just forked the account-facebook package to support FB app cred’s per tenant. Wasn’t too hard, and we wanted to stick with as much Meteor out of the box candy as possible.

Does it have to be pure node? I assume its just some server side code that the client needs to make calls to.


#20

It doesn’t have to be pure node. However, it seems complex enough to exist as it’s own subsystem in the architecture. In my app, Tenant #1 and Tenant #2 might be both using SAML, but doing so through via different SAML SP’s. Plus if I wanted to offer any of my tenant’s additional services (separate meteor apps) in the future, I wouldn’t want the functionality tethered to the first app. So far, I’ve been looking at npm OAuth Providers, and I’m not too impressed with what I’ve found so far… :confused: