Multi Tenant Setup using Adonis


#1

Hi guys,

New to Adonis and web development on a whole. I come from the desktop world primarily using .net.

I’ve never used laravel but I came across Adonis and it just felt right so i’m about to start a new project using adonis 4 but I just wanted some basic guidance on what any of the experienced guys here would recommend in terms of multi-tenancy setup.

I’m going the standard route where all the tables will have in a tenant_id field where neccessary. To minimise room for error where I may forget a where clause to filter the data, what would be a good way to setup a kind of global filter that functions transparently. Should that be a middleware, a hook or something else?

I’ll be using jwt authentication and I will store the tenant_id as part of the payload of the token so I will know what the tenant should be for filter selects and for inserts.

I hope what i’m asking is clear, looking forward to any help you guys can provide.

Thanks.


#2

Can you explain how tenancy is determined? Is it different domains/subdomains?


#3

In this case each tenant will have their own sub domain. We weren’t gonna go with sub domains at first but we’ve decided to go in that direction now.


Get a reference to Request outside controller?
#4

What about this approach:

class MagicRequestHelper {
   getTenant() {
      let host = // ...not sure how to get host name...
      let tenant = await Tenant.query().where('host', '=', host).first()
   }
}

class Multitenant {
   register(Model, settings={}) {
      let _query = Model.prototype.query
      Model.prototype.query = () => {
         return _query().where('tenant_id', '=', MagicRequestHelper.getTenant().id)
      }
   }
}

class MyModel extends Model {
   static boot() {
      super.boot()
      this.addTrait(Multitenant)
   }
}

That would allow you to perform queries as normal, with the tenant_id filter already applied. Personally I think I’d rather do it explicitly, but you could do it this way.

I might prefer something where I could do:

MyModel.forCurrentTenant().where('foo', '=' , 42)

#5

Let’s break it down into small chunks

  1. We need to identify the tenant
  2. Scope all queries to that tenant only.

Node.js ( the way it’s designed ) doesn’t allow store global references. More here Per-request global context accessible everywhere

Identify the tenant

The best place for that is to make use of a middleware for that. Whatever way you want, you can pull that information from the current HTTP request context.

Example:

class IdentifyTenant {
  async handle (ctx) {
    // assuming subdomain is used
    ctx.tenant = ctx.subdomains.name
  }
}

module.exports = IdentifyTenant

Scope all queries

One way is to write a method on a base model and pass it the current tenant and in return it gives you a query scoped to that tenant.

class Posts extends Model {
  forTenant (id) {
    return this.query().where('tenant_id', id)
  }
}

Inside controller

class PostsController {
  index (ctx) {
    return Post.forTenant(ctx.tenant).fetch()
  }
}

Happy to discuss more, when you start implementing and have some real time problems


#6

Thanks a lot guys you’ve given me a good start. I’ll let you know how i progress.

Kirk