Multi tenant implementation

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:

Hey @matthewtalma,

I’m ashamed to say, I’ve never used Parse.CoreManager. I thought I’d do a quick search in the docs, but couldn’t find any reference to it? That aside…

Could you explain in a bit more detail what you mean when you say it works on the client side, but you’re not receiving the header in the your cloud code?

Is this in a single requests (i.e. headers are set on client side), but not present in cloud code, or are you talking about two different scenarios?

Are you using the SDK on your client side or using something like axios to make your requests?

Hi @woutercouvaras - I found some information on Parse.CoreManager here. You are able to use it to set a custom header on the Parse SDK and it sends it with every request which is very handy.

I have confirmed that on every request to the Parse server, my new header “tenant” is being sent. When I do this request from the client:

const Order = Parse.Object.extend("Order");
const query = new Parse.Query(Order);
await query.find()

I get all the headers in the Parse.Cloud.beforeFind('Order' function:

accept:'*/*'
accept-encoding:'gzip, deflate, br'
accept-language:'en-US,en;q=0.9,it;q=0.8,la;q=0.7'
connection:'keep-alive'
content-length:'215'
content-type:'text/plain'
host:'localhost:1337'
origin:'http://mtalma.localhost:9010'
tenant:'test'
referer:'http://test.localhost:9010/'
sec-ch-ua:'" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"'
sec-ch-ua-mobile:'?0'
sec-ch-ua-platform:'"macOS"'
sec-fetch-dest:'empty'

However, when I make a cloud code function call:

const response = await Parse.Cloud.run("findOrders")

In the Parse.Cloud.beforeFind('Order', I am only able to get these headers:

accept:'*/*'
connection:'close'
content-length:'257'
content-type:'text/plain'
host:'localhost:1337'
user-agent:'node-XMLHttpRequest, Parse/js3.3.0 (NodeJS 14.18.1)'

If I inspect the request object in the cloud code function, I am able to see the all the headers (including tenant). I am blocked on this currently as our code base makes use of both techniques for requesting data from the Parse server.

Hi @matthewtalma,

Is that 2nd request (where you’re calling the cloud function), made on the client side too?

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!