Multitenancy
Introduction & Overview
CAP has built-in support for multitenancy with the @sap/cds-mtxs
package.
Essentially, multitenancy is the ability to serve multiple tenants through single clusters of microservice instances, while strictly isolating the tenants' data. Tenants are clients using SaaS solutions.
In contrast to single-tenant mode, applications aren't serving end-user request immediately after deployment, but wait for tenants to subscribe.
This guide is not yet available for Java, but will be soon.
⓪ Prerequisites
Make sure you have the latest version of @sap/cds-dk
installed:
npm update -g @sap/cds-dk
① Jumpstart with an application
To get a ready-to-use Bookshop application that you can modify and deploy, run
cds init bookshop --add sample
cd bookshop
② Enable Multitenancy
Now, you can run this to enable multitenancy for your CAP application:
cds add multitenancy
See what this adds to your Node.js project…
Adds package
@sap/cds-mtxs
to your project:jsonc{ "dependencies": { "@sap/cds-mtxs": "^1" }, }
Adds these lines to your project's package.json to enable multitenancy with sidecar:
jsonc{ "cds": { "profile": "with-mtx-sidecar", "requires": { "multitenancy": true } } }
Adds a sidecar subproject at
mtx/sidecar
with this package.json:json{ "name": "<your app name>-mtx", "dependencies": { "@sap/cds": "^7", "@sap/cds-hana": "^2", "@sap/cds-mtxs": "^1.9", "@sap/xssec": "^3", "express": "^4", "passport": ">=0.6.0" }, "devDependencies": { "@cap-js/sqlite": ">=0" }, "scripts": { "start": "cds-serve" }, "cds": { "profile": "mtx-sidecar" } }
Modifies deployment descriptors such as
mta.yaml
for Cloud Foundry and Helm charts for Kyma.
Profile-based configuration presets
The profiles with-mtx-sidecar
and mtx-sidecar
activate pre-defined configuration presets, which are defined as follows:
{
"[with-mtx-sidecar]": {
requires: {
db: {
'[development]': {
kind: 'sqlite',
credentials: { url: 'db.sqlite' },
schema_evolution: 'auto',
},
'[production]': {
kind: 'hana',
'deploy-format': 'hdbtable',
'vcap': {
'label': 'service-manager'
}
},
},
"[java]": {
"cds.xt.ModelProviderService": { kind: 'rest', model:[] },
"cds.xt.DeploymentService": { kind: 'rest', model:[] },
},
"cds.xt.SaasProvisioningService": false,
"cds.xt.DeploymentService": false,
"cds.xt.ExtensibilityService": false,
}
},
"[mtx-sidecar]": {
requires: {
db: {
"[development]": {
kind: 'sqlite',
credentials: { url: "../../db.sqlite" },
schema_evolution: 'auto',
},
"[production]": {
kind: 'hana',
'deploy-format': 'hdbtable',
'vcap': {
'label': 'service-manager'
}
},
},
"cds.xt.ModelProviderService": {
"[development]": { root: "../.." }, // sidecar is expected to reside in ./mtx/sidecar
"[production]": { root: "_main" },
"[prod]": { root: "_main" } // for simulating production in local tests
},
"cds.xt.SaasProvisioningService": true,
"cds.xt.DeploymentService": true,
"cds.xt.ExtensibilityService": true,
},
"[development]": {
server: { port: 4005 }
}
},
…
}
TIP
You can always inspect the effective configuration with cds env
.
③ Install Dependencies
After adding multitenancy, install your application dependencies:
npm i
④ Test-Drive Locally
Before deploying to the cloud, you can test-drive common SaaS operations with your app locally, including SaaS startup, subscribing tenants, and upgrading tenants.
Using multiple terminals…
In the following steps, we start two servers, the main app and MTX sidecar, and execute some CLI commands. So, you need three terminal windows.
1. Start MTX Sidecar
cds watch mtx/sidecar
Noteworthy…
In the trace output, we see several MTX services being served; most interesting for multitenancy: the ModelProviderService and the DeploymentService.
[cds] - connect using bindings from: { registry: '~/.cds-services.json' }
[cds] - connect to db > sqlite { url: '../../db.sqlite' }
[cds] - serving cds.xt.ModelProviderService { path: '/-/cds/model-provider' }
[cds] - serving cds.xt.DeploymentService { path: '/-/cds/deployment' }
[cds] - serving cds.xt.SaasProvisioningService { path: '/-/cds/saas-provisioning' }
[cds] - serving cds.xt.ExtensibilityService { path: '/-/cds/extensibility' }
[cds] - serving cds.xt.JobsService { path: '/-/cds/jobs' }
In addition, we can see a t0
tenant being deployed, which is used by the MTX services for book-keeping tasks.
[cds|t0] - loaded model from 1 file(s):
../../db/t0.cds
[mtx|t0] - (re-)deploying SQLite database for tenant: t0
/> successfully deployed to db-t0.sqlite
With that the server waits for tenant subscriptions — listening on port 4005 by default in development mode. Actual databases deployments happen then.
[cds] - server listening on { url: 'http://localhost:4005' }
[cds] - launched at 3/5/2023, 1:49:33 PM, version: 7.0.0, in: 1.320s
[cds] - [ terminate with ^C ]
2. Launch App Server
cds watch
Noteworthy...
The server starts as usual, but automatically uses a persistent database instead of an in-memory one:
[cds] - loaded model from 6 file(s):
db/schema.cds
srv/admin-service.cds
srv/cat-service.cds
srv/user-service.cds
../../../cds-mtxs/srv/bootstrap.cds
../../../cds/common.cds
[cds] - connect using bindings from: { registry: '~/.cds-services.json' }
[cds] - connect to db > sqlite { url: 'db.sqlite' }
[cds] - serving AdminService { path: '/odata/v4/admin', impl: 'srv/admin-service.js' }
[cds] - serving CatalogService { path: '/odata/v4/catalog', impl: 'srv/cat-service.js' }
[cds] - serving UserService { path: '/user', impl: 'srv/user-service.js' }
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 3/5/2023, 2:21:53 PM, version: 6.7.0, in: 748.979ms
[cds] - [ terminate with ^C ]
3. Subscribe Tenants
In the third terminal, subscribe to two tenants using one of the following methods.
cds subscribe t1 --to http://localhost:4005 -u alice:
cds subscribe t2 --to http://localhost:4005 -u erin:
POST http://localhost:4005/-/cds/deployment/subscribe HTTP/1.1
Content-Type: application/json
{ "tenant": "t1" }
const ds = await cds.connect.to('cds.xt.DeploymentService')
await ds.subscribe('t1')
Noteworthy...
Be reminded that these commands are only relevant for local testing. For a deployed app, subscribe to your tenants through the BTP cockpit and create a route to the App Router for your tenant.
In the CLI commands, we use the pre-defined mocked users
alice
anderin
with corresponding tenantst1
andt2
. See the section on pre-defined mock users for more. Runcds subscribe --help
to see all available options.The subscribe request is sent to the mtx sidecar process (listening on port 4005)
The sidecar reacts with trace outputs like this:
log[cds] - POST /-/cds/deployment/subscribe ... [mtx] - successfully subscribed tenant t1
In response to each subscribe request, the server deploys a new persistent tenant database per tenant (hence tenant data is isolated):
log[cds] - POST /-/cds/deployment/subscribe [mtx] - (re-)deploying SQLite database for tenant: t1 > init from db/init.js > init from db/data/sap.capire.bookshop-Authors.csv > init from db/data/sap.capire.bookshop-Books.csv > init from db/data/sap.capire.bookshop-Books_texts.csv > init from db/data/sap.capire.bookshop-Genres.csv /> successfully deployed to ./../../db-t1.sqlite [mtx] - successfully subscribed tenant t1
To unsubscribe a tenant, run
cds unsubscribe <tenant> --from http://localhost:4005 -u <user>
. Runcds unsubscribe --help
to see all available options.
Test with Different Users/Tenants
Open the Manage Books app at http://localhost:4004/#Books-manage and login with different users alice
and erin
, respectively. To see requests served in tenant isolation, i.e., from different databases, change data in one tenant and check that it's not visible in the other one.
In the following example, Wuthering Heights (only in t1) was changed by Alice. Erin doesn't see it, though.
Use private/incognito browser windows to test with different tenants...
Do this to force new logins with different users, assigned to different tenants:
- Open a new private / incognito browser window.
- Open http://localhost:4004/#Books-manage in it → log in as
alice
. - Repeat that with
erin
, another pre-defined user, assigned to tenantt2
.
Note tenants displayed in trace output...
We can see the tenants indicated also in server's log output for incoming requests:
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 3/5/2023, 4:28:05 PM, version: 6.7.0, in: 736.445ms
[cds] - [ terminate with ^C ]
...
[odata|t1] - POST /adminBooks { '$count': 'true', '$select': '... }
[odata|t2] - POST /adminBooks { '$count': 'true', '$select': '... }
...
Pre-defined users in mocked-auth
How users are assigned to tenants and how tenants are determined at runtime largely depends on your identity providers and authentication strategies. The mocked
authentication strategy, used by default with cds watch
, has a few pre-defined users configured. You can inspect these by running cds env requires.auth
:
[bookshop] cds env requires.auth
{
kind: 'basic-auth',
strategy: 'mock',
users: {
alice: { tenant: 't1', roles: [ 'cds.Subscriber', 'admin' ] },
bob: { tenant: 't1', roles: [ 'cds.ExtensionDeveloper' ] },
alice: { tenant: 't1', roles: [ 'cds.Subscriber', 'admin', 'cds.ExtensionDeveloper' ] },
dave: { tenant: 't1', roles: [ 'cds.Subscriber', 'admin' ], features: [] },
erin: { tenant: 't2', roles: [ 'cds.Subscriber','admin', 'cds.ExtensionDeveloper' ] },
fred: { tenant: 't2', features: … },
me: { tenant: 't1', features: … },
yves: { roles: … },
'*': true
},
tenants: { t1: { features: … }, t2: { features: '*' } }
}
You can also add or override users or tenants by adding something like this to your package.json:
"cds":{
"requires": {
"auth": {
"users": {
"u2": { "tenant": "t2" },
"u3": { "tenant": "t3" }
}
}
}
}
4. Upgrade Your Tenant
When deploying new versions of your app, you also need to upgrade your tenants' databases. For example, open db/data/sap.capire.bookshop-Books.csv
and remove one or more entries in there. Then upgrade tenant t1
as follows:
cds upgrade t1 --at http://localhost:4005 -u alice:
POST http://localhost:4005/-/cds/deployment/upgrade HTTP/1.1
Content-Type: application/json
{ "tenant": "t1" }
const ds = await cds.connect.to('cds.xt.DeploymentService')
await ds.upgrade('t1')
Now, open or refresh http://localhost:4004/#Books-manage again as Alice and Erin → the removed entry is gone for Alice, but still there for Erin, as t2
has not yet been upgraded.
⑤ Deploy to Cloud
Deploy to Cloud Foundry / Kyma
In short, do the following, which is an excerpt from the deployment and SaaS deployment guides:
Once: add an app router and SAP HANA Cloud configuration. App Router acts as a single point-of-entry gateway to route requests to. In particular, it ensures user login and authentication in combination with XSUAA.
cds add approuter,hana --for production
npm i
Once: add a deployment descriptor:
cds add mta
cds add helm
Freeze the npm
dependencies for server and MTX sidecar:
npm update --package-lock-only
npm update --package-lock-only --prefix mtx/sidecar
Build and deploy
mbt build -t gen --mtar mta.tar
cf deploy gen/mta.tar -f
cds build --production
pack build bookshop-sidecar --path mtx/sidecar/gen \
--buildpack gcr.io/paketo-buildpacks/nodejs \
--builder paketobuildpacks/builder:base \
--env BP_NODE_RUN_SCRIPTS=""
helm upgrade --install bookshop ./chart
Subscribe to your tenants through BTP cockpit and create a route to the App Router for your tenant.
Learn more about deploying to Cloud Foundry or Kyma in the SaaS Deployment Guide.
Test-Drive with Hybrid Setup
For faster turnaround cycles in development and testing, you can run the app locally while binding it to remote service instances created by a Cloud Foundry deployment.
The following operations require service keys…
If you haven't created service keys for your services yet, do so as follows:
cf create-service-key bookshop-auth bookshop-auth-key
cf create-service-key bookshop-db bookshop-db-key
To achieve this, first bind your SaaS app to its required cloud services, for example:
cds bind -2 bookshop-auth
cds bind -2 bookshop-db
Now, bind the MTX sidecar to its required cloud services as well, for example:
cd mtx/sidecar
cds bind -2 bookshop-auth
cds bind -2 bookshop-db
By passing --profile hybrid
you can now run the app with cloud bindings and interact with it as you would while testing your app locally.
cds watch --profile hybrid
Learn more about Hybrid Testing.
Manage multiple deployments
Use a dedicated profile for each deployment landscape if you are using several, such as dev
, test
, prod
. For example, after logging in to your dev
space:
cds bind -2 bookshop-db --profile dev
cds watch --profile dev
⑥ Add Custom Handlers
You can add custom handlers to lifecycle events like deploy
, subscribe
, upgrade
. As the MTX services are implemented as standard CAP services, that follows the usual patterns as documented in the Node.js, or Java reference guides.
In the Sidecar Subproject
The preferred and most flexible way for both Node.js and Java projects is to add custom handlers in the sidecar project, implemented in Node.js.
cds.on('served', () => {
const { 'cds.xt.DeploymentService': ds } = cds.services
ds.before('subscribe', async (req) => {
// HDI container credentials are not yet available here
const { tenant } = req.data
})
ds.before('upgrade', async (req) => {
// HDI container credentials are not yet available here
const { tenant } = req.data
})
ds.after('deploy', async (result, req) => {
const { container } = req.data.options
const { tenant } = req.data
...
})
ds.after('unsubscribe', async (result, req) => {
const { container } = req.data.options
const { tenant } = req.data
})
})
About Sidecar Setups
The SaaS operations subscribe
and upgrade
tend to be resource-intensive. Therefore, it's recommended to offload these tasks onto a separate microservice, which you can scale independently of your main app servers.
Java-based projects even require such a sidecar, as the MTX services are implemented in Node.js.
In these MTX sidecar setups, a subproject is added in ./mtx/sidecar, which serves the MTX Services as depicted in the illustration below.
The main task for the MTX sidecar is to serve subscribe
and upgrade
requests.
Only when tenant-specific extensions are applied will the CAP services runtime request models from the sidecar.
Node.js projects can decide not to run the MTX services in a sidecar but rather embedded in the main app.
Next Steps
- See the MTX Services Reference for details on service and configuration options, in particular about sidecar setups.
- See our guide on Extending and Customizing SaaS Solutions.