5

Use case

In schema based multi-tenant applications there normally is one database with multiple schemas. One schema is the main where the common application data is stored and one for each tenant of the application. Everytime a new customer is registered to the system, a new isolated schema is automatically created within the db. This means, the schema is created at runtime and not known in advance. The customer's schema is named according to the customer's domain. When a request enter the system the user is validated and a schema is selected using the data on the main schema. And then most/all subsequent database operations go to the tenant specific schema. As you can see the schema we want to use is only known at run time.

Problem

How to select schema at runtime? We are using postgres connector. We should be able to switch schemas at runtime.

Another problem is how to run migrations for different tenants?

Notes:

The db-schema needs to be set in a request-scoped way to avoid setting the schema for other requests, which may belong to other customers. Setting the schema for the whole connection is not an option.

user3170450
  • 375
  • 3
  • 20

2 Answers2

3

Hello from the LoopBack team

LoopBack 4 does not offer first-class support for multi-tenancy yet. I opened a GitHub issue to discuss different possible solutions, see loopback-next#5056.

One option is to tweak the model-specific Repository class to set the schema based on the current user. This requires a small enhancement to be implemented in @loopback/repository, therefore this solution will not currently work out of the box. I am cross-posting the relevant part of my comment below, see the GitHub issue for full context.

Lightweight tenant isolation using schemas

In this setup, the authentication layer and all tenants share the same database name and use the same credentials (database user) to access the data. We have 1+N schemas defined in the database: the first schema is used by the authentication layer, plus we have one schema for each tenant. All database queries will use the same LB datasource and thus share the same connection pool.

Implementation wise, we need to tweak the way how a LB4 model is registered with a datasource. Instead of creating the same backing juggler model for all users, we want to create tenant-specific juggler models.

Conceptually, this can be accomplished by tweaking the Repository constructor.

export class ProductRepository extends DefaultCrudRepository<
  Product,
  typeof Product.prototype.id
> {
  constructor(
    @inject('datasources.db') dataSource: juggler.DataSource,
    @inject(SecurityBindings.USER) currentUser: UserProfile,
  ) {
    super(
      // model constructor
      Product, 
      // datasource to use
      dataSource, 
      // new feature to be implemented in @loopback/repository:
      // allow repository users to overwrite model settings
      {schema: currentUser.name},
    );
  }
}
Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99
  • What if we get the model also runtime (for specific schema), then will we be able to get the repo also runtime for the same within the same schema? – user3170450 Apr 07 '20 at 11:30
  • I am afraid I don't understand what do you mean by getting the model at runtime? – Miroslav Bajtoš Apr 09 '20 at 07:15
  • @MiroslavBajtoš Is there any progress on this issue? I browse the PR of [Ramond's example](https://github.com/strongloop/loopback-next/tree/master/examples/multi-tenancy), but I couldn't catch the final result. Did you take the final decision for enabling multi-tenancy on LB? – mbnoimi Feb 10 '21 at 06:24
  • @mbnoimi I am afraid multi-tenancy hasn't been a priority for us. I think Raymond's example should provide a good starting point for building your own multi-tenant application. AFAIK, we don't have anything newer or better. – Miroslav Bajtoš Feb 10 '21 at 13:03
  • @MiroslavBajtoš Sad to hear that. Unfortunately there are many things in LB I'm struggling with! Do you've any roadmap or milestone to follow ([milestones](https://github.com/strongloop/loopback-next/milestones) in Github are outdated) just to know what's going on. – mbnoimi Feb 10 '21 at 16:42
0

you can create the DefaultCrudRepository ( Default implementation of CRUD repository using legacy juggler model and data source) at runtime which needs two parameter

  1. Datasource
  2. Model

Now instantiate Datasource with required settings includes that schema which you want to use Then provide your model and data source to DefaultCrudRepository instance like below:-

const ds = new PostgresDataSource({
  connector: 'postgresql',
  host: 'some host',
  port: 'some port',
  user: 'some user',
  password: 'password',
  database: 'database',
  schema: 'schema for that particular tenant',
});
const repo =  new DefaultCrudRepository(SomeModel, ds);

and after that use this repository to execute find, create and other methods. There is a sample implementation for this https://github.com/hitesh2067/dynamic-schema-example

I have passed the schema as query param but you can use any other way to provide the schema

Updated Solution:-

Now that Loopback has provided Middleware and better Context separation for the application context and request context. we can use that to connect to any datasource dynamically (hoping that connector is installed in the package.json )and bind it to Application context then temporarily bind that binding to the datasource which is pointed by UserRepository (or any of your multi tenant Repo) in request context.

Example for this is written on https://github.com/dev-hitesh-gupta/loopback4-multi-tenant-multi-datasource-example

Implementation will be like

const tenant = await this.getTenant(requestCtx);
if (tenant == null) return;
const tenantData = await this.tenantRepo.findById(tenant.id);
requestCtx.bind(CURRENT_TENANT).to(tenantData);
const tenantDbName = `datasources.multi-tenant-db.${tenantData.id}`;
// adding datasource if not present
if (!this.application.isBound(tenantDbName)) {
  const tenantDb = new juggler.DataSource({
    name: tenantDbName,
    ...tenantData.dbConfig,
  });
  this.application.bind(tenantDbName).to(tenantDb).tag('datasource');
}
// Pointing to a default datasource in request context
requestCtx
  .bind('datasources.multi-tenant-db')
  .toAlias(tenantDbName);
  • 2
    Please note the example app will create a new datasource instance for each incoming request. This will in turn create a new pool of connections to PostgreSQL database. As a result, database connections are not reused across requests. This may become a performance issue. – Miroslav Bajtoš Apr 07 '20 at 07:11
  • @MiroslavBajtoš Are you talking about parallel requests? – Hitesh Gupta Apr 07 '20 at 11:20
  • 1
    It does not matter whether requests are coming in parallel. UserService is defined as transient, LB creates a new instance for each incoming request. Put `console.log` (or `console.trace`) to the constructor to see for yourself. – Miroslav Bajtoš Apr 09 '20 at 07:14
  • @MiroslavBajtoš I have done some changes regarding the issues you told and checked DB connections its working fine for me now. Please review once. 1. I know datasource setting is a singleton so for this purpose we can use this data source separately from others so the application won't crash for others. 2. I know you are building the feature for this but we need this thing urgently with loopback 2.0.0. link for code https://github.com/dev-hitesh-gupta/dynamic-schema-example – Hitesh Gupta Apr 29 '20 at 12:35
  • @MiroslavBajtoš May you please take a look into Hitesh's new modifications. Does his code work fine (doesn't create a new db instance for every incoming request)? – mbnoimi Feb 10 '21 at 18:09
  • Sorry folks, I don't have bandwidth to look into your questions. – Miroslav Bajtoš Feb 11 '21 at 13:07
  • @HiteshGupta As Miroslav said I can see a new instance created for every query (then it destroyed after finishing the query) which mean it's easy to fall in the bottleneck so the solution you've suggested just a temporary one and unreliable. – mbnoimi Feb 15 '21 at 06:47
  • @mbnoimi I know it's temporary as lb4 is working on it that's why temporary solution we needed urgently so I implemented it As far as connection concern we need a pool of connection from multiple DBs that will be defined runtime for example:- request A need to connect to DB A and request B need to connect to DB B and that is decided runtime only in request and we don't know How many DBs are there as the user can define in runtime for either we need a pool implementation to connect multiple databases and add new connection when require which is long implementation need framework support. – Hitesh Gupta Mar 25 '21 at 07:34
  • Unfortunately I left LoopBack for good. It's an unreliable framework, the core team don't care about many opened bugs and feature requests. I tested NestJS and I'm very happy with it. – mbnoimi Mar 25 '21 at 11:09