Skip to content

Multi-tenant

Multi-tenant (v1.20.0+)

  • A multi-tenant instance serves many customers (tenants) with one set of APIs. Every tenant has its own database, of the same type as the instance.
  • The APIs, schemas, hooks, settings and permissions of the instance are defined once and serve every tenant. Customers share the servers and the APIs, not the databases.
  • The instance keeps its own connection string. That database is the structure database: a request without a tenant works on it.
  • Supported for MongoDB, MySQL, MariaDB, SQL Server, PostgreSQL and Oracle instances.

Example: a CRM with two customers

  • A CRM is sold to two companies, Acme and Globex. Each one gets its own PostgreSQL database: crm_acme and crm_globex.
  • One instance named crm serves both. Its own connection string points to crm, the structure database, which holds the tables every tenant has: customers, orders, users.
  • The tenants live in a table tenants of another instance, catalog, one row per customer.

1. Keep your tenants in a table

  • Use a table of any instance, with one row per tenant: a username column and a connection string column.
  • The table needs an active schema in API Maker.
  • To keep connection strings encrypted, add conversions.encryption to that column in its schema. API Maker decrypts it with the default secret when it connects.
  • For Oracle tenants, add columns for the username, password and privilege of each tenant.
1
2
3
4
[
    { "username": "acme",   "connection_string": "postgresql://crm:secret@db-1:5432/crm_acme",   "active": true },
    { "username": "globex", "connection_string": "postgresql://crm:secret@db-2:5432/crm_globex", "active": true }
]
  • Tenants can use different database servers: here Acme is on db-1 and Globex on db-2.

2. Point the default secret to that table

import * as T from 'types';

let Secret: T.ISecretType | any = {
    // ... common and your other keys
    multiTenant: {
        crm: <T.IMultiTenantSecretObj>{
            instanceName: 'catalog',                 // instance of the tenants table
            databaseName: 'public',
            collectionName: 'tenants',
            usernameColumn: 'username',
            connectionStringColumn: 'connection_string',
            find: { active: true },                  // optional, added to the tenant lookup

            // Oracle only
            // oracleDBUsernameColumn: 'db_username',
            // oracleDBPasswordColumn: 'db_password',
            // oracleDBPrivilegeColumn: 'db_privilege',
        },
    },
};
module.exports = Secret;

3. Mark the instance

  • Open the instance and check Is Multi Tenant Structure Instance.
  • In Connection String Multi Tenant, choose the entry of the secret, for example multiTenant.crm.

4. Name the tenant in every request

  • In the path, after the instance name, with two colons.
1
2
3
GET /api/gen/admin/crm::acme/crm/customers      # the customers of Acme, from crm_acme
GET /api/gen/admin/crm::globex/crm/customers    # the customers of Globex, from crm_globex
GET /api/gen/admin/crm/crm/customers            # no tenant: the structure database
  • Or with two headers. Send both of them, or none.
x-am-tenant-username: acme
x-am-tenant-secret-path: multiTenant.crm
  • Custom APIs read the tenant of the request in g.req.params.tenantUsername.

What API Maker does

  • Reads the row of the tenant from the tenants table, decrypts its connection string if needed, and opens a connection pool for that tenant. The next requests of the tenant reuse the pool.
  • A new tenant needs no restart and no deployment: its first request reads its row. Onboarding a customer is creating its database and adding one row to the tenants table.
  • The tenant is part of every cache key, so tenants never get each other's cached answers.
  • Logs record the tenant of each call.
  • The databases panel and the API testing page let you pick a tenant to work with its data.

Users of a tenant

  • Users can live in a users table inside each tenant database, like crm.users above.
  • Add a DB token generator on that table of the multi-tenant instance, on the Token Generators page, for example crm_users_token.
  • A user gets a token with the token API and the tenant headers:
1
2
3
4
5
POST /api/system-api/admin/token
x-am-tenant-username: acme
x-am-tenant-secret-path: multiTenant.crm

{ "name": "crm_users_token", "u": "[email protected]", "p": "PASSWORD" }
  • API Maker reads Alice from the users table of crm_acme, and the token it returns is made for acme:
    • Requests of acme accept it, as long as Alice is in the users table of crm_acme.
    • A request for globex is refused with 401: Tenant from x-am-user-authorization and x-am-tenant-username should be same.
    • A request without a tenant, on the structure database, is refused with 401.
    • A refreshed token keeps the tenant.
  • A token made without a tenant, or from a users table in another instance, is not tied to a tenant: it is checked against the users table of the tenant each request names. To keep such a caller to its own tenant, compare g.req.params.tenantUsername with the tenant of the caller in a pre hook.

After a tenant moves

  • When the connection string of a tenant changes, call the Multi tenant instance updated system API. Every server drops the pool of that tenant and the cache of the instance is cleared.
await g.sys.system.multiTenantInstanceUpdated({ instanceName: 'crm', username: 'acme' });
  • It also accepts an array of { instanceName, username }.

Good to know

  • A schema change must run on every tenant database: plan migrations for all of them.
  • Each API Maker process keeps one pool per active tenant. Size the connection limits of your database servers for it.