Multitenancy and Meteor

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.

Yeah, that’s where it gets a bit tricky. I just live with a lot of round-trips to the server and a lot of loading spinners when someone hits a route directly from the browser URL bar.

Once the app’s client side state is all loaded up via a hideous web of reactivity (where results from the server trigger another round-trip, the results of which trigger another round-trip, etc.), then navigating to new routes within the app is nice and snappy (as the client app state is all in place). But that first start up time is a bit grim …

Glad you’ve confirmed my madness wasn’t alone @babrahams :smile:

There has to be a more elegant solution for this …

As at the moment; it’s the same call from .startup on the client, and them am re-using it in the FlowRouter’s trigger to make sure. Not very pretty :frowning:

Yeah, it’s not an elegant solution, but it’s easy and it works. I’m currently happy enough with the time from initial html/js bundle load to round trips finished / page fully rendered. That said, if anyone comes up with something slicker, I’m all ears. :slight_smile:

1 Like

I thought I’d post in this thread as it’s been useful so far to me in developing my multi-tenant strategy.

I looked at DB-per-tenant but it seems to complex and expensive right now. So I’ve been taking the approach which is quite similar to @babrahams I believe but simpler. I store the tenantId in the user doc but never let it onto the client. All of my publish functions look like this:

Meteor.publish("data", function() {
    var tenantId = Meteor.users.findOne({_id: this.userId}).tenantId;
    return MyCollection.find({tenantId: tenantId}, {fields: {tenantId: 0}});
}); 

I might need to start deducing the tenant from subdomain though. My issue is that if each tenant can have multiple clients invited into the system, how do I allow a user to be added by two different tenants? So joe@blogs.com for tenant A is different to joe@blogs.com for tenant B.

My current thinking is that if I prepend the tenantId to their email and store that in meteor.users as their username that will solve the issue. Then when logging in on a tenants subdomain I believe I would just need to use the setCustomSignupOptions in Account UI to generate their username from their email and tenantId.

Any feedback is most welcome!!

1 Like

It’s a bit late but that’s exactly what http://keycloak.jboss.org/ does. You define tenants (realms) on which you can enable almost any existing authentication protocol. Your application just talks oAuth2 with keycloak (JWT also supported).

This is a great approach. I currently pass the tenant ID from the subscription on the client, but will have to take a look at this approach of just grabbing it on the server instead.

I have two tenant-related items stored on the user object:

currentOrg is a string representing the organization that the user is currently logged into. This is used as a variable for all publications.

tenants is an array (actually, really should be an object) of all the organizations a user has access to.

Then I use the Roles package to manage permissions within each tenant by matching the Roles group to the tenant ID.

When I create a new user, I first check if the email address already exists, and if so just append the tenants credentials to their user profile and send a separate welcome email.

I know there’s ways to make it better, but that seems to be working for now. Like you, I’d like to better understand how to setup Nginx to use subdomains.

Hope that helps!

The approach I use is very similar to that of @allenfuller.

Note: you can get away without using nginx at all to manage subdomains. Just make sure your DNS has a wildcard subdomain record like:

*       IN      A       101.102.103.104

Then do this on the client. (That’s a link to post earlier in this thread detailing how I get the tenant from the subdomain using client side code and a method call.)

I admit, this is not a particularly elegant approach, and requires more round trips than anyone would like, but it does work.

Interesting approach Allen. So are you saying you’re doing this?

  • Client signs up for tenant A - receives regular Enrolment mail.
  • Client signs up for tenant B - receives a “you now have access to tenant B with your MyProduct login/password” mail.

And then when logging in it checks the subdomain to then see which tenant is the currentOrg for the user?

Yep! And then I added a small switchOrg feature to the user’s menu so they can move back and forth:

<template name="navMain">
  <ul class="nav navbar-nav nav-main hidden-sm hidden-md hidden-lg">
   [ ... ]
  </ul>
  <ul class="nav navbar-nav navbar-right nav-desktop hidden-xs">
    <li role="presentation" class="dropdown">
      <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{currentUser.emails.[0].address}} <span class="caret"></span></a>
      <ul class="dropdown-menu" role="menu">
        <li><a href="{{pathFor 'user'}}">My Profile {{roles}}</a></li>
      {{#if multiOrgs}}
        <li>
          <a href="#" data-toggle="modal" data-target="#switchAccounts">Switch Accounts</a>
        </li>
        <li>
          <span class="navbar-text">(Currently in {{currentOrg}})</span>
        </li>
      {{/if}}
        <li class="logout"><a href="#">Logout</a></li>
      </ul>
    </li>
  </ul>
</template>

And the helper:

Template.navMain.helpers({
  currentOrg: () => {
    return Meteor.user().profile.currentOrg;
  },
  // Checks if the user is a user for more than one organization, or a global admin
  multiOrgs: () => {
    let userId   = Meteor.userId(),
        orgs     = Meteor.users.findOne({_id: userId}, {fields: {organizations: 1}}),
        admin    = Roles.userIsInRole(userId, 'admin', Roles.GLOBAL_GROUP),
        orgCount = orgs.length;
    if (orgCount > 1 || admin) {
      return true;
    } else {
      return false;
    }
  }
});

I hesitate to share code as I’m still pretty new to this myself. In fact, I think my whole approach started with what I read from @babrahams on this thread!

1 Like

Cool. I have been thinking of the tenant separation as a very hard security line and doing everything to make sure someone from one tenant can never see another but your approach offers a lot more flexibility and has got me thinking. I’ll post back here again after I have a chance to try it out.

1 Like

How would this work on a mobile application if the domain is localhost?

I’m currently using the subdomain method that @babrahams laid out above but now I’m second guessing using subdomains vs having the organization (aka tenant) identifier in the pathname instead based on this article: Building a SaaS App? You should probably stick to a single subdomain. - The Clubhouse Blog

For my app, I’m imagining that one Meteor.user may belong to several organizations and it’d be ideal if they could switch between them easily without having to log in to each. I have a user model similar to the one @allenfuller describes here.

A few questions:

  1. Are there any big benefits or downsides in Meteor with having the organization identifier in the subdomain (e.g. https://acmeco.myapp.com) vs the pathname (e.g. https://myapp.com/acmeco)?

  2. If I use subdomains, does anyone have any tips on redirecting to a subdomain? In my sign up flow, I have a create organization step (/signup/organization), after which I’d like to redirect the user to their organization’s subdomain without making them log in. My attempts so far with Iron Router have not been successful.

  3. If I use the pathname, e.g. myapp.com/:orgName, what’s the best way to handle routing with Iron Router so that on all logged in routes other than / the organization identifier is in the URL? e.g. https://myapp.com/acmeco/project/1.

Appreciate any insights y’all have. Special thanks to @allenfuller and @marklynch for helping me get this far. :slight_smile:

1 Like

This is an old topic, but I just wanted to chime in that I had been tinkering with this myself and found working with sub-domains is possible. I use nginx to front-end a single meteor project with a wildcard letsencrypt SSL certificate. My example app uses https://*.domain.devel.buzzledom.com and the underlying logic determines which domain I am trying to access and serves data accordingly.

I also use accounts:password, which means accounts are the same across sub-domains. By passing the localStorage.getItem('Meteor.loginToken') across sub-domains, I can authenticate via Accounts.loginWithToken(loginToken).

Also worth mentioning is that I figured out how to serve the proper favicon.ico per sub-domain from the filesystem, by virtue of the most powerful WebApp.connectHandlers. The static HTML simply points the the URL for favicon that is being handled by the server

  <link rel="icon" sizes="16x16 32x32" href="/favicon/ico?v=2">

and on the server I have this code that looks up the countries flag in /private/flags/ and servers the proper flag favico if it exists

WebApp.connectHandlers.use('/favicon/ico', (req, res, next) => {
  res.writeHead(200, {
    'Content-Type': 'image/x-icon',
  });
  const domain = req.headers.host.split('.').reverse().pop();
  var country = countryList.getName(domain);
  if(country != undefined) {
    country = country.replace(/ /g,'-');
    if (country in COUNTRY_NAME_MAP) country = COUNTRY_NAME_MAP[country];
    try {
      const ico = Assets.getBinary(`ico/${country}.ico`);
      res.end(ico, 'utf8');
    } catch (err) {
      console.log(`error ${err.message}`);
      res.end(Assets.getBinary('flags/unknown.ico'), 'utf8');
    }
  } else {
    res.end(Assets.getBinary('flags/unknown.ico'), 'utf8');
  }
});
8 Likes

I am currently considering implementing multi-tenancy in Meteor by separating the database for each tenant. I’ve read some useful discussions so far, but is it still challenging to separate the database for each tenant?

What is your tenant? How would you define it?

Let’s say, you own a SaaS which is being licensed to clients to server their clients. You need each of your clients to have their own tenant with tenant level authentication (confidentiality with you outside the circle) for them to server their own clients?
Is this the case? Or do you have complex security/confidentiality requirements such as SOX or military or medical?

2 Likes

I agree with paulishca. Separate databases for separate users is a DevOps nightmare. If it’s a very small app with very high-end, custom, specialized users… then they should have separate apps.

True multi tenancy from the SaaS perspective, where multiple users / organizations are using the same app for their own purposes, is best served with an ID on all data keyed to the organization and also baked into authentication.

6 Likes