Multitenancy and Meteor

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).

1 Like

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.

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?

How are you doing this?

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});
  }
});

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;    
    }
  }
});
5 Likes

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.

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.

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.

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.

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.

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:

Unless you have a good reason to make it a separate thing I’d stick within Meteor. It sounds like something you should break into a package, so it can be used by other apps you write, or published to Atmosphere for others to use. Meteor already has plenty of oAuth support. Check out the accounts-facebook|twitter|google code for examples. I’m not sure why SAML would be much different, but maybe it does need to be totally stand alone.

I’m thinking about using https://stormpath.com for my authentication, I believe they also handle SAML and CAS and more, so might be really useful for you.

Hi guys,

I am new to meteor and would be really thankful about how you guys are doing with your saas implementation?

The last framework I used (django) had a very elegant multitenant-solution
using subdomains and postgres schemas. So basically while programming your database-access you did not even have to think about tenants. The module would take care of this for you.

Did you guys have to go thru a lot of trouble implementing your secure saas environment?
What do you consider best practise?

I’d be happy to hear your feedback.

Hey guys, I’ve mostly got my multi-tenant app done.

We’re trying to integrate meteorhacks:cluster. We sometimes get 404 errors for POST http://my.meteor.app/.../226/yc02r4ly/xhr_send requests.

Has anyone else successfully configured a multi-tenant app with meteorhacks:cluster so it can scale?

I’ve also run into another roadblock with multitenancy: server side rendering.

In publication functions, this.connection.httpHeaders.host (which is normally set to mytenant.127.0.0.1.xip.io:3000 in local development) is localhost:3000 in a server context.

1 Like

https://www.mongodb.com/presentations/securing-mongodb-to-serve-an-aws-based-multi-tenant-security-fanatic-saas-application

Appears the semi-official best practise with Mongo is to use a separate database (same server) for each tenant.

-Easy deployment for new tenants…
-Easy security with different encryption key for each tenant.
-Easy to extract a tenant’s data when they don’t pay their bill.

Worth watching if you are interested in SAAS with Mongo.

5 Likes

This will put into perspective the trade-offs among different tenanting solutions

3 Likes

Hi @babrahams and others…

How are you managing direct calls to the url and routing ?

Say you have a request to http://cake.domain.com/

This will load the tenantId, and populate the session.

If on the other hand the user makes a direct call to the url through - i.e. browser URL bar (as opposed to through the app) - then session variable isn’t always in context and doesn’t have the tenantId data at all.

Trying to get this to work with FlowRouter. Namely cake.domain.com/foo,

foo's route doesn’t have the session data, when called directly; but if navigated through the app it does.

Subdomain and routing with Meteor is proving interesting :unamused:

Thank you.