Multi tenant implementation

Actually, now that I look at it, I can see that your user-agent is “node”, which tells me this is probably done server side. Is that right?

If so, this would explain it, as you’ve probably only add the header (via CoreManager) on the client side and not on the server.

Regardless, I would strongly suggest that you not don’t call a cloud function with Parse.Cloud.run, from another cloud function as this makes a network request. It’s going to be a lot slower than simply calling the function directly on the server.

Here’s an example of how I’ve set my code up to get around this - in case you’re interested:

In my cloud.ts file, I expose a getPages function as per normal and you can see that I’m importing it from a file called service:

import * as pageService from './page-service'

Parse.Cloud.define('fetch-tenant-by-id', citizenService.fetchTenantById)

The service is really just a place where I marshal requests and format the response from my methods in a standard way:

import * as citizen from '../lib/citizen'

export const fetchTenantById = async (request: CloudRequest): Promise<CloudResponse> => {
  await validateTenant(request)
  if (!request.params.tenantId) {
    throw new Error(CLOUD_PARAMS_MISSING)
  }

  const tenant = await citizen.fetchTenantById(request.params.tenantId)

  return {
    data: tenant
  }
}

If you look in one of my earlier replies above, where I showed you my tenant validation code, you can see me using the same method just mentioned above, without the need for making a network request. Here it is again (from my validation method):

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

I hope this helps? If I’ve made incorrect assumptions about where you’re running the code from, my apologies :smiley: (Just thought making an assumption and giving a bit more detail might help save some time).

Hi @woutercouvaras - the call to the cloud code function is being done from the client side also.

In the cloud code function, the request param has all the headers, however, when the beforeFind is triggered it contains the reduced headers. I suppose since the actual query is being run inside the cloud code function, the headers of beforeFind reflect that. I wonder if there is a way to pass the tenant to the beforeFind trigger in this case. Or perhaps there’s another approach.

I don’t use the SDK on my client side so I don’t really have too much insight.

I use Vue/Nuxtjs and the SDK doesn’t play nicely with the reactive nature of Vue, so I use the rest api, using axios to do all the requests and I just send my tenant id along with those requests.

Are you willing to share the code of your findOrders function?

@woutercouvaras - Ah I see. Yes - that makes a ton of sense. We are prototyping rebuilding our client side with NUXT and I am making the same design decision to use the rest API and not importing the Parse SDK.

I’ve done a bit more investigation on this. It seems when the cloud code function is executing on the server, it uses the Parse SDK to make a call to itself when you query. Therefore, the headers do not contain the data of the original Client to Server call, but rather the headers for the Server to Server call.

This means that I can use the headers in the client to server call to limit the query for the given tenant, but this would result in me having to edit 300+ Cloud Code functions.

Hey @matthewtalma,

Yes, I played around with using the SDK but there were two main pain points:

  1. If you’re using the SSR that Nuxt offers, the SDK doesn’t play nicely with this. I was either getting the SDK to work on the server side and not the client side or visa versa, but never both. Maybe one can get it right, but then you still have the following pain point…
  2. I found myself jumping through too many hoops to get it to play nicely with Vue (from a reactivity point of view - lots of additional work to manage state and keep it all in sync).

Since I switched to the using the rest api, it’s been smooth sailing and I haven’t looked back. I’ve done some basic tests with gql too, but still need to circle back to that.

I know you might ditch the SDK, but I’m still trying to understand the problem you’re facing. Something isn’t holding up.

If your cloud function is getting the headers and you can get your tenant id, then you can either set the tenant in a tenantValidation function (as per my previous examples) or surely you can sipmly set it as a filter on your query. So, as an example, if this is your findOders cloud function, without the tenant validator I used above):

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

  const orders = await new Parse.Query('Order')
    .equalTo('tenant', req.headers['x-tenant'])
    .find()

  return orders
}

If you don’t want to explicitly set the tenant like this, you’ll need to use something similar to the validateTenant function I showed in earlier examples, where you set the tenant into the req.query paramter. Doing this will give you the sort of experience I think you’re expecting. e.g.:

// grab the tenant from the header, 
// look it up and then add the Parse object as the value to your query

req.query.equalTo('tenant', tenant)

I might still be missing something, but hopefully this helps.

All the best with the prototype!

@matthewtalma I must apologize! I clearly misunderstood you about the missing tenant header. I’ve just encountered the same problem you described :slight_smile:

I’ve just implemented a work around. It might not be the best solutions, but it seems to be doing the trick for now.

In case you’re interested, in my examples above, I had my validateTenant method as a requirement ahead of all my method calls (including the beforeFind, etc calls for the cloud functions).

Long story short, I’ve created a preFlight utility that does the tenant validation and straight after it also sets the headers again, seeing as they get lost after this point for internal calls.

Again, maybe not the most elegant, but it’s working.

Here’s my preFlight method

export const preFlight = async request => {
  await validateTenant(request)
  if (request.headers['x-tenant']) {
    Parse.CoreManager.set('REQUEST_HEADERS', {
      'x-tenant': request.headers['x-tenant']
    })
  }
}

And it works in the the standard beforeFind hooks:

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

and also in my own code - this (the internal Site query) happens to be where I encountered the same issue as you:

export const searchModel = async (request: CloudRequest): Promise<CloudResponse> => {
  await utils.preFlight(request)

  if (request?.params?.filter?.site) {
    const site = await new Parse.Query('Site').equalTo('objectId', request.params.filter.site).first()
    request.params.filter.site = site
  }

  return {
    data: {
      ...(await utils.searchModel(request.params))
    }
  }
}

I hope this might be of some help to you (or anyone else that might encounter similar issues).

Sorry again for misunderstanding you :smiley:

Cheers

Hi,

It’s pretty easy to do that, simply by creating multiple apps. See this comment: Multi tenant implementation - #24 by woutercouvaras

This works if you don’t need cloud functions. Sadly, Cloud Code is created as a singleton, so your cloud code will always connect to the last (or first, I can’t remember), app that is defined.

This is why we’re looked into the tenant-id option.

So far I have try everything, but only this code share here in this reply are working well.

Hi @woutercouvaras ,

Just would like to know: how could I get the mongodb connection or Parse.Query/Parse.Object by app?

Thanks so much.

Hi @xeoshow,

If I’m understanding you correctly (seeing as you’re talking about “apps”), check out this comment I made early on: Multi tenant implementation - #15 by Manuel

You’ll see in that example that one can configure the apps separately, and because you defined it, you can connect to the relevant app - e.g:

{
      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}`
  }

Please bear in mind that I’ve never used the SDK on the client side…I only ever used REST. When using rest, you can simply make all your requests to the relevant app url - e.g. ${config.parse.serverUrl}/customer-one/Classes/Page?where={"slug":"page-one"}. This “just works”.

Having said that, in the threads above, you’ll also see that I mention that Cloud Code will only run against your last defined app. This is sad and means that this solution is really viable if you need any kind of custom logic. This is why we went for the tenant id approach.

If I’ve misunderstood you, please explain in a bit more detail what you mean.

Hi @woutercouvaras ,

Thank you v much for the reply. I have seen your solution which is great, and just think if we could set the relationship for the app instance to the cloud function context, should can resolve the problem of “Cloud Code will only run against your last defined app”, since our different apps will have different databases, make things seems more complex.

BTW, we are also using the rest api.

If we expose the cloud function behind the rest api with the customerPath as part of the rest url (via express), is it possible a feasible solution for Cloud Code multi-tenancy?

Thanks so much again.

Sadly it won’t work. Take a look at these two links. It’s got to do with the way that Cloud Functions was originally designed - i.e. as a singleton, so only one instance of it can and does exist. It would be PERFECT if this was not the case, and it worked as you (and I) were hoping it would :smiley:

I hope this helps.

Understood.
IMHO, in the Cloud Code, mostly we will just use the Parse.Object and Parse.Query, if we could get the db connection session, and if there is a way to create the new Parse.Query or Parse.Object with the db connection session parameter, seems we could resolve most cases for CRUD?
And if there is a way to get the db connection session by appId, then seems feasible? And since the standard parse server rest api could support the multiple instances with the correct db session, I think there should be a way doing this…
Thanks a lot.

If you’re only using the Parse.Object and Parse.Query, then you probably don’t need cloud code at all. In that case, the apps setup that I mentioned here will give you db separation and it all works with the rest api.

I hope you get it all working the way you want. Good luck!

Hi @woutercouvaras ,

Thanks so much, we will need do some custom logic in the Cloud Code. For example, query table A and judge from the results for querying table B. Seems the standard rest api could not meet such requirements.

The core thing is: is there any way to get the db connection session and with that, we could create the Parse.Object and Parse.Query? I am just start investigating the source code, while did not find the related info yet …

From everything I’ve read (which includes snippets by the core team), it doesn’t look like what you’re after is possible. I can think of potential hacks that involve writing your own middleware that runs before parse to determine a few initial bits of state/data and then initialize the relevant app, based on that. You’d have to weight it up to figure out if the cost is worth it.

You could try to use an aggregation query to achieve what you want, or just do two separate queries. I think making two network requests is should be fine (from a user experience point of view), esp if the result set is going to be small (which, at the very least, the result from first lookup should be, right?).

We’ve opted to use the tenant-id as we get all the flexibility that Parse offers and get to continue using Cloud Code, which we need for all our custom logic. Sure the data is not split into separate dbs, but in our case, that’s okay.

1 Like

Hi @woutercouvaras,

If we are going to replace all the cloud functions with the express router rest api, how should I do with the sessionToken validation things? Also, the beforeFind/afterFind, etc triggers, will they work as expected?

Thanks a lot.

Hi @xeoshow,

I’m not sure I quite follow you, but I’ll have a go anyway.

Even though it’s possible to run multiple Parse Apps on a single parse server, if the application you’re building relies on Cloud Code, then it’s best to think of your Parse App as a single app. i.e. You cannot make use of the “multiple parse apps” functionality.

So, either you’ll run a parse instance per app that you’re building (or tenant that you create) or you’ll need to implement the multi tenancy as detailed in the thread above.

There is one other thing you could try, something that one of the core members mentioned (I think it was @davimacedo) in another thread somewhere, but something that I did not try, which is…

You could run your own middleware that does some checks before the parse app gets instantiated. i.e. Determine the tenant based on headers, referrer, etc, then instantiate the relevant Parse app, based on this info. I opted not to do this because of the sheer amount of work at a time when I don’t know if people will want what I’m building :slight_smile:

Sorry if I’ve misunderstood you.

1 Like

@woutercouvaras Thanks so much !

Just would like to ask another question: if not considering the limitation of hardware, what is the max number of app could be supported in the Parse Server?

Thanks a lot.