Multi tenant implementation

What you seem to be trying to do has a multitude of approaches, each with different pros and cons. Which approach is right for you depends on the characteristics of the service you intend to build, for example considering:

  • scale
  • security context
  • efficiency
  • robustness / reliability

These are business decisions, so from a technical point of view there won’t be a right/wrong answer.

understood -
All I’m trying to do is have an install that has mutitenancy - but the information out there is very varied.
What mutiple approaches are you refering to ?

There are multiple architectural and technological approaches to achieve a similar outcome. Specifically, you would be looking for a solutions architect to design an infrastructure and technology solution that aligns with your product requirements - which you would have to formulate of course.

The simplest solution / proof concept for multi-tenancy would be 2 separate databases, 2 separate parse server instances and bind them to 2 different endpoints, for example:

  • External customer1.example.com routes to internal http://server1/parse
  • External customer2.example.com routes to internal http://server2/parse

Yes that’s what I’ve got in my diagram above. The binding is done dynamically via an xpress server . My concern with this (and your) suggestion is resources.

1- I’d need mutiple machines (or a really big one) that can run mongo efficiently for complex tasks.
2- How would I create new instances when a new tenant sights up?

Thanks

This depends on how you want to set this up. If you use a cloud provider, they have their own APIs to manage the infrastructure. The example above that I gave is also not an efficient solution, but merely a proof of concept. There is a lot of know-how from different disciplines that goes into creating - and maintaining - a commercially viable solution.

That is not to discourage you. You could start with a simple proof of concept and go from there as you identify bottlenecks along the way. Keep in mind that the know-how required goes beyond this forum and is in many aspects not related to Parse Server specifically.

Hi folks,

I’ve got a somewhat similar question. I’d like have a parse instance per customer. A friend suggested that I simply create a totally new instance of parse (i.e. a totally separate and full set of all files) per customer. I guess this is a workable option, but I was wondering if I can use a single copy of my parse code and simply create separate parse application instances in the parse server setup?

I understand from what I’ve read in a few different places online that cloud code is not designed to be “isolated” in a multi-tenancy type situation, but I’m happy quite happy to share as single set of cloud functions between customers as they will be locked down and only editable by ourselves, not the customers. In fact, I would prefer this :slight_smile: (Unless of course this implies that cloud coud would only be able to access a single app’s data?)

TL;DR:
My questions:

  • Will this approach work? Is it just a case of figuring it out? or
  • Can parse servers share a single code base but perhaps need to run in separate processes? or
  • Do I need to treat each parse server as totally separate? i.e. totally separate copy of files and separate parse setup?

Thanks in advance :smiley:


This is what I currently have setup:

const customers = [
  {
      appName: 'Customer One',
      customerPath: 'customer-one',
      databaseURI: 'mongodb://localhost:27017/customerOne',
      serverURL: `${config.parse.serverUrl}/customer-one`,
      publicServerURL: `${config.parse.serverUrl}/customer-one`,
      masterKey: 'masterCustomerOne',
      appId: 'Customer One App',
      cloud: `${__dirname}/${config.parse.cloudFunctions}`
  },
  {
      appName: 'Customer Two',
      customerPath: 'customer-two',
      databaseURI: 'mongodb://localhost:27017/customerTwo',
      serverURL: `${config.parse.serverUrl}/customer-two`,
      publicServerURL: `${config.parse.serverUrl}/customer-two`,
      masterKey: 'masterCustomerTwo',
      appId: 'Customer Two App',
      cloud: `${__dirname}/${config.parse.cloudFunctions}`
  }
]

const customerInstances = []
for (const customer of customers) {
  const instance = new ParseServer(customer)
  customerInstances.push(instance)
}

const apps = customers.map((customer) => {
    return {
        serverURL: serverURL: customer.serverURL,
        masterKey: customer.masterKey,
        appId: customer.appId,
        appName: customer.appId
    }
})

const parseDashboard = new ParseDashboard(
    {
        apps,
        users: [
            {
                user: config.parse.adminUser,
                pass: config.parse.adminPassword,
            },
        ],
    },
    {
        allowInsecureHTTP: !process.env.NODE_ENV || process.env.NODE_ENV !== "production"
    },
);

I start this up locally and…

  • In the terminal, I get a warning that the 2nd server is not running
  • In the dashboard, I can see both apps registered, but only the the first is usable (expected, seeing as the terminal output already mentioned that the 2nd is not running)
  • I can interact with the cloud functions via the rest api and I can add records via the js console.

I’ve already burnt a few hours in reading online to figure out if this approach will work, but I can’t seem to find anything.

How do you plan to deploy the servers themselves? Docker? K8S? Manually?

I can imagine a situation where you build a custom parse-server based docker image that basically just accepts a “MASTER_KEY” and “CUSTOMER_NAME” from the environment and hard codes everything else including the cloud code. Then you use the docker image to spin one or more instances for each customer as they are coming in.

Then if you need to modify the cloud code, update it, release a new version of your docker image and start upgrading the customers to the new version.

I can imagine having this working in K8S pretty easily and nicely.

thoughts?
Martin

1 Like

Thanks Martin!

I was going to just deploying them manually (mostly because I’ve already got 2 parse servers running this way and I already have some infra related script [deployments, etc, etc] setup).

I’ve never used docker for anything in production, but if getting multiple sites running from a single parse code base is not an possible, it might be worth looking into it. I was just hoping that I could handle it this way, so that the server runs nice and lean and that my setup is essentially config driven.

Thanks for taking the time to reply and for your suggestion.

Can’t you just add a TENANTID to each object ?

Hi Zak,

Thanks, I did consider this, seeing as one could add a beforeSave check to ensure that each object always gets a tenantId. The problem though, ensuring that a tenant id is always provided when query for data. Because there’s now built in way to enforce this, it leaves tenant data a bit exposed for my liking (along with a bunch of other complexities around ownership, permissions, etc).

I know you were asking questions around this topic. May I ask what you settled on?

The separate instances will work really well for me. I just need to figure out if I can do it in my preferred way. If not, I will have to reconsider things.

I’m using the rest api - An have a custom api wrapper around it that added tenant { type: string, id: string}

then my get / get all makes the search eg : =where { “owner.id”: currentTenant ID } …

Could have used pointer but didn’t know about it before i built this :slight_smile:

For now it works for what I need

Thanks Zak,

When you say wrapper are you talking middle ware as suggested by @davimacedo above or have you gone the proxy route you were talking about?

I’m going to hit the ground running with 20+ customers and hope to grow it into the hundreds, so figuring this out is pretty important for me :slight_smile:

I’ll be honest, implementing some form of tenantId was my first thought, but I’ve grown to quite fond of the idea of total separation.

I came across this stackoverflow post and, much like the guy in the comments, this doesn’t make any sense to me, but I’ve tried it and my initial tests show that it works.

I’m going to continue with a few more tests and will post my findings/results here. If this is all it takes, this is amazing :smiley:

So far I’ve tested creating a user, using the Dashboard and the JS Console for each app.

The users are stored correctly, per app. I’ve double checked my db and indeed, each user is stored in a separate db! Winning!! Or so I thought…

Sadly, the cloud code does not work. Actually, that statement is not entirely true. Let me clarify what I mean by"does not work"… Cloud code does run and one can perform queries against the db, however the “X-Parse-Application-Id” has no effect. The cloud code has access to the db of the last app defined. I remembered reading something about a limitation with cloud code in this regard, so when searching for that comment again.

For those interested, see this comment on and this one github.

It’s such a pity about this limitation. If cloud code isolation could work in the same way as the parse server instances, this would be such an amazing solution for a totally isolated multi-tenant setup.

I’d really like to avoid running umpteen instances of parse, so I’m going to look into a combination of a tenant id and roles. Yesterday, I learned that there is in fact a beforeFind trigger (beforeFind, beforeSave and beforeDestroy). I knew about the beforeSave, but not the rest. I think this means one could require something like a ‘X-Tenant’ header for all queries and handle things that way.

Anyway, I’ll explore this option and report back for those interested.

@woutercouvaras I’m wondering if you had any success going down the multi tenant header route. I’m also looking into this as it seems like a feasible approach. However, I’m not sure how to get access to headers in the beforeFind trigger. The request object does have a headers param but these seem to be a subset of the ones sent in the request.

Hi @matthewtalma!

Happy to share what I’m currently doing. It’s still a work in progress and not my first prize, but it seems to be doing the trick.

Here’s an example of what I have in my cloud.ts file:

Parse.Cloud.beforeFind('Page', async req => {
  await validateTenant(req)
})

My validateTenant method looks something like this:


export const validateTenant = async req => {
  if (!req.headers['x-tenant'] && !req.master) {
    throw new Parse.Error(102, 'Missing tenant id.')
  }

  if (req.headers['x-tenant']) {
    const tenant = await citizen.fetchTenantById(req.headers['x-tenant'])

    if (!tenant) {
      throw Error('Invalid tenant')
    }

    req.headers.tenant = tenant

    if (req.query) {
      req.query.equalTo('tenant', tenant)
    }
  }

  return req
}

This allows the master key to still be used, which is obvsiously convenient for returning all data in specific situations.

You’ll see that it sets the request.query to the tenant, which (I’m sure you know), but essentially filters all the data for the tenant.

You will of course need to set a tenant id on each record (I’ve used pointers for this).

This might not be the best approach, but it’s working for the moment.

I hope this helps. Shout if you have any questions.

If you come up with any improvements, please do share :smiley:

Oh, one last comment…

This app is not live yet and while developing, I’ve just been using the actual db id as the tenant id, but I will likely implement some sort of api key or uuid to make things a little more secure an in line with common practice.

I’ve not given it too much thought yet, but just thought I would highlight the point :slight_smile:

Great - this makes a lot of sense. My approach is very similar and I’ve gotten it to a point where I am able to send the header from the client with setting it at Parse initialization like so:

Parse.initialize("app_id", "js_key");
Parse.serverURL = Portal.server;
Parse.CoreManager.set("REQUEST_HEADERS", { 'Tenant':  'tenantId' });

This seems to work fine for queries that are executed on the client directly with const query = new Parse.Query('Order'). However, with my cloud code calls, I am not receiving the custom header in the beforeFind trigger on the server. In fact, the headers object seems to have only a few items. Have you encountered this?

Note, I am seeing the customer ‘tenant’ header in the cloud code function.

Alternatively on newer Parse Servers you can use:

Parse.Cloud.beforeFind('Page', async req => {
  // normal cloud clode logic here
}, validateTenant)

Just a way you can pass reoccurring logic to cloud functions easily :blush:

I didn’t know that, thanks @dblythy :slight_smile: