Multi-Tenancy

Axelor Open Platform has multi-tenancy support. The term multi-tenancy refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants. A tenant is a group of users who share common access with specific privileges to the software instance.

So now, a single instance of Axelor app can serve multiple tenants (users, companies etc).

Configuration

The default multi-tenancy implementation uses multiple databases per tenant and requires configuration from axelor-config.properties.

The database configuration keys with format db.<tenant-id>.<key> is used to configure database connections.

# enable multi-tenancy
application.multi-tenancy = true (1)

db.default.visible = false (2)
db.default.driver = org.postgresql.Driver
db.default.ddl = update (3)
db.default.url = jdbc:postgresql://localhost:5432/open-platform-demo-db
db.default.user = axelor
db.default.password =

db.db1.name = DB1 (4)
db.db1.hosts = host1,host1:8080 (5)
db.db1.driver = org.postgresql.Driver
db.db1.url = jdbc:postgresql://localhost:5432/open-platform-demo-db1
db.db1.user = axelor
db.db1.password =

db.db2.name = DB2
db.db2.hosts = host2,host2:8080
db.db2.driver = org.postgresql.Driver
db.db2.url = jdbc:postgresql://localhost:5432/open-platform-demo-db2
db.db2.user = axelor
db.db2.password =

db.db3.name = DB3
db.db3.hosts = host3,host3:8080
db.db3.driver = org.postgresql.Driver
db.db3.url = jdbc:postgresql://localhost:5432/open-platform-demo-db3
db.db3.user = axelor
db.db3.password =
1 required to enable multi-tenancy feature
2 the default database should be invisible as it’s a fallback
3 only default database supports DDL operations, schema for other tenants should be created manually
4 display name of the tenant
5 host name based filter, users can only access one tenant in case of hosts match

The default tenant is required and used for all unauthenticated requests.

Customization

We can override this default implementation by providing custom implementation of these two interfaces:

  • com.axelor.db.tenants.TenantConfig - an interface to provide tenant connection config values

  • com.axelor.db.tenants.TenantConfigProvider - an interface to obtain TenantConfig, may be from external resource

The TenantConfigProvider defines following methods to implement:

  • TenantConfig find(String tenantId) - find a TenantConfig instance for the given tenant id

  • List<TenantConfig> findAll(String host) - find all the TenantConfig for the given hostname

  • boolean hasAccess(User user, TenantConfig config) - check whether the given authenticated user can use the tenant

The custom implementation should be integrated from application module like this:

public class DemoModule extends AxelorModule {

    @Override
    protected void configure() {
        bind(TenantConfigProvider.class).to(MyTenantConfigProvider.class);
    }

}

It’s up to the MyTenantConfigProvider to decide how to resolve tenants (may be using jdbc connection to some external database).

Multi-threading

Any multi-threaded or multi-process task, should run with a special helper com.axelor.db.tenants.TenantAware.

Suppose you have following code that executes a task using a separate background process:

process task
final ExecutorService executor = Executors.newFixedThreadPool(numWorkers);
executor.submit(() -> {
  // work with database
});

It will not work as expected as the current tenant context is not available outside the current thread.

It can be fixed with following changes:

tenant aware process task
final ExecutorService executor = Executors.newFixedThreadPool(numWorkers);
executor.submit(new TenantAware(() -> {
  // work with database
}));

Now the task will have the current tenant context.

However, if the process is not started from a web request (directly or indirectly), you have to pass the desired tenant id manually. Otherwise, it will assume a default tenant.

tenant aware process task
final ExecutorService executor = Executors.newFixedThreadPool(numWorkers);
executor.submit(new TenantAware(() -> {
  // work with database
})
.tenantId("some-tenant"));

Same way, you have to change and tasks running in different threads:

threaded task
Thread task = new Thread(() -> {
  // work with database
});

task.start();

should be changed to:

tenant aware threaded task
Thread task = new TenantAware(() -> {
  // work with database
});

task.start();

The TenantAware is a subclass of java.lang.Thread.

if the thread or process is not started from a web request, we have to set the tenant id manually somehow using TenantAware#tenantId(String) method.

Authentication

Clients without session support (e.g. direct basic auth) should provide X-Tenant-ID header with every request to select a tenant.

Clients with session support should send X-Tenant-ID header with login request.

Limitations

Following features are disabled in multi-tenancy mode: