1

We are using Loopback 4 in our SaaS Application. We are stuck in one case.

We are trying to have every user his own separate database. So when a user logs in the application we want to create a dynamic datasource, and that's what we have done. But the problem is how to link repositories to that dynamically created datasources.

One solution we tried is to change this.datasource in the repository on each user request through the controller, but when multiple users request at the same time the datasource value gets changed. It's a humble request, please help us.

I know I may have not explained correctly.

Inyourface
  • 532
  • 4
  • 12

1 Answers1

2

We are discussing different ways how to implement tenant isolation in the GitHub issue loopback-next#5056. We also provide an example multi-tenant application, see its README. The pull request loopback-next#5681 is implementing a generic pooling service that can be used to implement tenant isolation too.

I am afraid your question does not provide enough details to allow me to give you a specific solution, so I'll quote code snippets from my Datasource-based tenant isolation proposal.


To make it easy to inject the tenant-specific datasource, let's keep the same datasource name (binding key), e.g. datasources.tenantData, but implement dynamic resolution of the datasource value. The idea is to rework the datasource class scaffolded by lb4 datasource into a Provider class.

import {inject} from '@loopback/core';
import {juggler} from '@loopback/repository';

const config = {
  name: 'tenantData',
  connector: 'postgresql',
  // ...
};

export class TenantDataSourceProvider implements Provider<TenantDataSource > {
  constructor(
    @inject('datasources.config.tenant', {optional: true})
    private dsConfig: object = config,
    @inject(SecurityBindings.USER)
    private currentUser: UserProfile,
  ) {}

  value() {
    const config = {
      ...this.dsConfig,
      // apply tenant-specific settings
      schema: this.currentUser.name
    };

    // Because we are using the same binding key for multiple datasource instances,
    // we need to implement our own caching behavior to support SINGLETON scope
    // I am leaving this aspect as something to figure out as part of the research
    const cached = // look up existing DS instance
    if (cached) return cached;

    const ds = new TenantDataSource(config);
    // store the instance in the cache
    return ds;
    }
}

export class TenantDataSource extends juggler.DataSource {
  static dataSourceName = 'tenant';
  // constructor is not needed, we can use the inherited one.
  // start/stop methods are needed, I am skipping them for brevity
}

There are different ways how to implement caching of per-tenant datasources. Ideally, I would like to reuse Context for that. It turns out this is pretty simple!

We want each tenant datasource to have its own datasource name and binding key. To allow repositories to obtain the datasource via @inject, we can implement a "proxy" datasource provider that will be resolved using one of the name datasources.

export class TenantDataSourceProvider implements Provider<TenantDataSource> {
  private dataSourceName: string;
  private bindingKey: string;

  constructor(
    @inject('datasources.config.tenant', {optional: true})
    private dsConfig: object = config,
    @inject(SecurityBindings.USER)
    private currentUser: UserProfile,
    @inject.context()
    private currentContext: Context,
    @inject(CoreBindings.APPLICATION_INSTANCE)
    private app: Application,
  ) {
    this.dataSourceName = `tenant-${this.currentUser.name}`;
    this.bindingKey = `datasources.${this.dataSourceName}`;
  }

  value() {
    if (!this.currentContext.isBound(this.bindingKey)) {
      this.setupDataSource();
    }
    return this.currentContext.get<juggler.DataSource>(this.bindingKey);
  }

  private setupDataSource() {
    const resolvedConfig = {
      ...this.dsConfig,
      // apply tenant-specific settings
      schema: this.currentUser.name,
    };
    const ds = new TenantDataSource(resolvedConfig);
    // Important! We need to bind the datasource to the root (application-level)
    // context to reuse the same datasource instance for all requests.
    this.app.bind(this.bindingKey).to(ds).tag({
      name: this.dataSourceName,
      type: 'datasource',
      namespace: 'datasources',
    });
  }
}

export class TenantDataSource extends juggler.DataSource {
  // no static members like `dataSourceName`
  // constructor is not needed, we can use the inherited one.
  // start/stop methods are needed, I am skipping them for brevity
}

The code example above creates per-tenant datasource automatically when the first request is made by each tenant. This should provide faster app startup and possibly less pressure on the database in the situation when most tenants connect use the app only infrequently. On the other hand, any problems with a tenant-specific database connection will be discovered only after the first request was made, which may be too late. If you prefer to establish (and check) all tenant database connection right at startup, you can move the code from setupDataSource to a boot script and invoke it for each known tenant.


See also the following comment:

Mind sharing how these data sources are injected into repositories since this.bindingKeys are being dynamically generated?

The idea is to bind a static datasource key to TenantDataSourceProvider, which will resolve to one of the dynamically-created datasources.

For example, in the app constructor:

this.bind('datasources.tenant').toProvider(TenantDataSourceProvider);

Then you can inject the datasource the usual way, for example:

@inject('datasources.tenant')
dataSource: TenantDataSource
Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99