Multi Tenant Setup using Adonis

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.

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

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.

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)

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

4 Likes

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

Kirk

@kirkjones What did you end up with as a solution in the end?

I’m asking because I’m building a multi-tenant solution at the moment however my ‘tenant’ is based on accounts therefore determined by user authentication.

@virk - Is this model query syntax still relevant? I’m getting a Post.forTenant is not a function error.

Has it been replaced by static query scopes? If so, then what is the alternative now?