Multi-tenant¶
| One instance | Serves every tenant with the same APIs, schemas, hooks and permissions. |
| One database per tenant | Of the type of the instance ; the connection string lives in a row of a tenants table. |
| Name the tenant | crm::acme in the path, or the header x-am-tenant-username: acme. |
| Tokens | A person of a tenant gets a token which works for that tenant only. |
- 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_acmeandcrm_globex. - One instance named
crmserves both. Its own connection string points tocrm, the structure database, which holds the tables every tenant has:customers,orders,users. - The tenants live in a table
tenantsof 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.encryptionto that column in its schema, and save the rows through the APIs of the table. API Maker decrypts it with the default secret when it connects. - For Oracle tenants, add columns for the username and the password of each tenant, and if a tenant connects with a privilege, a column for it:
SYSDBA,SYSOPER,SYSASM,SYSBACKUP,SYSDG,SYSKMorSYSRAC. A tenant without a privilege connects as a plain user of its database.
- Tenants can use different database servers: here Acme is on
db-1and Globex ondb-2.
2. Point the default secret to that table¶
instanceName,databaseName,collectionName,usernameColumnandconnectionStringColumnare required. A request of the instance fails with an error naming the missing keys otherwise.- A tenant must match its username and the
find. Withfind: { active: true }, settingactivetofalsesuspends a tenant. One table can also keep the tenants of several instances apart, for example withfind: { db_kind: 'pg' }. - Each multi-tenant instance chooses its own entry, so two instances can use two tenants tables.
- A request never names the entry: the instance knows it. The tenant is all a request names.
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.
- Or with the x-am-tenant-username header.
- The rules:
- The instance looks the tenant up in its own tenants table, the entry chosen as its Connection String Multi Tenant. One header therefore serves every multi-tenant instance, even instances with different tenants tables.
- An empty header names no tenant: the structure database.
- When the path and the header both name a tenant, the tenant of the path is used.
- A tenant the table does not have, or which the
findleaves out: 400,Unable to find tenant with username 'acme'. crm::or::acme: 400,Please provide valid multi tenant instance name.- A tenant named for an instance which is not multi-tenant changes nothing: that instance has one database for every tenant.
Custom APIs¶
- Call a custom API with the tenant header. It reads the tenant in
g.req.params, and the APIs it calls withg.sys.dbandg.sys.systemwork on the database of that tenant: the header is passed on to them. Each instance they call finds the tenant in its own tenants table.
- Another tenant, or the structure database, on purpose:
- Queries and table meta run in the database of the tenant too:
- A custom API with
enableCachingkeeps its answers per tenant. Reset them for one tenant:
| Reset the cache of a custom API for one tenant | |
|---|---|
- A custom API whose
authProvidersname a token generator on a users table of the instance needs a token of the tenant of the request, see Users of a tenant.
Hooks¶
- The pre and post hooks of a request of a tenant read it in
g.req.params.tenantUsername. - The APIs they call work on the database of that tenant, whether the request named the tenant in the path or with the header.
| Post hook : an audit row in the database of the tenant | |
|---|---|
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 (for PostgreSQL, one per tenant and database). The next requests of the tenant reuse the pool.
- A name the tenants table does not know opens no 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.
- 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.
Caching¶
- The cached answers of a multi-tenant instance are kept per tenant: a tenant never gets the cached answer of another tenant, nor of the structure database.
- A request naming its tenant in the path and one naming it with the header share their cached answers.
- A write for one tenant drops the cached answers of that tenant. A write to the structure database drops those of every tenant.
- The tables of an instance which is not multi-tenant are read by every tenant: a write made for any tenant drops the cached answers of all of them.
- Custom APIs with caching keep their answers per tenant as well. A write to the database of a tenant drops the cached answers of the custom APIs reading that table, for that tenant.
- To reset the cache of a table by hand:
| Reset the cache of a table | |
|---|---|
Users of a tenant¶
- Users can live in a users table inside each tenant database, like
crm.usersabove. - 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 header:
|
- API Maker reads Alice from the users table of
crm_acme, and the token it returns is made foracme:- Requests of
acmeaccept it, as long as Alice is in the users table ofcrm_acme. - A request for
globexis refused with 401:Tenant from x-am-user-authorization and x-am-tenant-username should be same.Even whenglobexhas a user with the same username and password. - A request without a tenant, on the structure database, is refused with 401.
- A refreshed token keeps the tenant, whatever tenant the refresh call names.
- Requests of
- 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.tenantUsernamewith 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 pools of that tenant, for PostgreSQL the pools of all its databases. The cached answers of the instance and of the tenants table are cleared, so the next request of the tenant reads its row again and connects to the new database.
- Until then, the servers keep using the pools they have.
- It also accepts an array of
{ instanceName, username }, and answers each item:{ success: true, data: true }, orsuccess: falsewith the error: an unknown instance (404) or an instance which is not multi-tenant (400).
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.