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

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 multithreaded or multiprocess tasks should be wrapped to enable execution within a context-aware environment.

Suppose you have the 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. Also, it will not set current admin running the process.

It can be fixed with the following changes:

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

Now the task will have the current tenant and current user context.

The ContextAware* system will run the task inside a new transaction by default. If this is not the expected behavior, you can disable it with .withTransaction(false) before starting the thread.

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

tenant aware process task
final ExecutorService executor = Executors.newFixedThreadPool(numWorkers);
executor.submit(ContextAware.of().withTenantId("some-tenant").withUser(AuthUtils.getUser("admin")).build(
  () -> {
    // work with database
  }
));

The 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 Thread(ContextAware.of().build(
  () -> {
    // work with database
  }
));

task.start();

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

Schema generation is disabled in multi-tenancy mode.