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 wait for tenants to subscribe before serving any end-user requests.
This guide is available for Node.js and Java.
Press v to switch, or use the toggle.
Prerequisites
Make sure you have the latest version of @sap/cds-dk installed:
npm update -g @sap/cds-dkJumpstart with an application
To get a ready-to-use bookshop application you can modify and deploy, run:
cds init bookshop --add sample
cd bookshopcds init bookshop --add java,tiny-sample
cd bookshopEnable Multitenancy
Now, you can run this to enable multitenancy for your CAP application:
cds add multitenancySee what this adds to your Node.js project…
Adds package
@sap/cds-mtxsto your project:jsonc{ "dependencies": { "@sap/cds-mtxs": "^1" }, }Adds this configuration to your package.json to enable multitenancy with sidecar:
jsonc{ "cds": { "profile": "with-mtx-sidecar", "requires": { "multitenancy": true } } }Adds a sidecar subproject at
mtx/sidecarwith this package.json:json{ "name": "bookshop-mtx", "dependencies": { "@sap/cds": "^7", "@sap/cds-hana": "^2", "@sap/cds-mtxs": "^1.9", "@sap/xssec": "^3", "express": "^4" }, "devDependencies": { "@cap-js/sqlite": ">=1" }, "scripts": { "start": "cds-serve" }, "cds": { "profile": "mtx-sidecar" } }If necessary, modifies deployment descriptors such as
mta.yamlfor Cloud Foundry and Helm charts for Kyma.
See what this adds to your Java project…
Adds the following to .cdsrc.json in your app:
jsonc{ "profiles": [ "with-mtx-sidecar", "java" ], "requires": { "multitenancy": true } }Adds the following to your srv/pom.xml in your app:
xml<dependency> <groupId>com.sap.cds</groupId> <artifactId>cds-feature-mt</artifactId> <scope>runtime</scope> </dependency>Adds the following to your srv/src/java/resources/application.yaml:
yml--- spring: config.activate.on-profile: true cds: multi-tenancy: mtxs.enabled: true sidecar.url: http://localhost:4005/Adds a sidecar subproject at
mtx/sidecarwith this package.json:json{ "name": "bookshop-mtx", "dependencies": { "@sap/cds": "^7", "@sap/cds-hana": "^2", "@sap/cds-mtxs": "^1.9", "@sap/xssec": "^3", "express": "^4" }, "devDependencies": { "@cap-js/sqlite": "^1" }, "scripts": { "start": "cds-serve", "build": "cds build ../.. --for mtx-sidecar --production && npm ci --prefix gen" }, "cds": { "profile": "mtx-sidecar" }
}
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 iAfter adding multitenancy, Maven build should be used to generate the model related artifacts:
mvn installTest-Drive Locally
For multitenancy you need some additional dependencies in the pom.xml of the srv directory. To support mock users in the local test scenario add cds-starter-cloudfoundry:
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-starter-cloudfoundry</artifactId>
</dependency>Now we can configure the spring-boot profile in the application.yaml to use mock users:
---
spring:
config.activate.on-profile: true
cds:
multi-tenancy:
sidecar.url: http://localhost:4005
mtxs.enabled: true
security.mock.users:
- name: alice
tenant: t1
roles: [ cds.Subscriber, admin ]
- name: bob
tenant: t1
roles: [ cds.ExtensionDeveloper, cds.UIFlexDeveloper ]
- name: erin
tenant: t2
roles: [ cds.Subscriber, admin, cds.ExtensionDeveloper, cds.ExtensionDeveloper, cds.UIFlexDeveloperUIFlexDeveloper ]CAP Java supports local multitenancy only with a file-based sqlite database. Therefore, you have to configure the SQLite datasource.
WARNING
By default the cds init command generates the project with a preconfigured H2 datasource. Therefore, the H2 dependency and the deploy command in the pom.xml should be adjusted to SQLite:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<scope>runtime</scope>
</dependency>Learn more about SQLite configuration
Configure the sidecar to use dummy authentication.
"cds": {
"[development]": {
"requires": {
"auth": "dummy"
}
}
}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/sidecarNoteworthy…
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.sqliteWith that, the server waits for tenant subscriptions, listening on port 4005 by default in development mode.
[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 watchNoteworthy...
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 ]cd srv
mvn cds:watch -Dspring-boot.run.profiles=trueNoteworthy...
The server starts as usual, with the difference that a persistent database is used automatically instead of an in-memory one:
2023-03-31 14:19:23.987 INFO 68528 --- [ restartedMain] c.s.c.bookshop.Application : The following 1 profile is active: "local-mtxs"
...
2023-03-31 14:19:23.987 INFO 68528 --- [ restartedMain] c.s.c.services.impl ServiceCatalogImpl : Registered service MtSubscriptionService$Default
2023-03-31 14:19:23.987 INFO 68528 --- [ restartedMain] c.s.c.services.impl.ServiceCatalogImpl : Registered service ExtensibilityService$Default
2023-03-31 14:19:23.999 INFO 68528 --- [ restartedMain] c.s.c.services.impl.ServiceCatalogImpl : Registered service CatalogService
2023-03-31 14:19:24.016 INFO 68528 --- [ restartedMain] c.s.c.f.s.c.runtime.CdsRuntimeConfig : Registered DataSource 'ds-mtx-sqlite'
2023-03-31 14:19:24.017 INFO 68528 --- [ restartedMain] c.s.c.f.s.c.runtime.CdsRuntimeConfig : Registered TransactionManager 'tx-mtx-sqlite'
2023-03-31 14:19:24.554 INFO 68528 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-03-31 14:19:24.561 INFO 68528 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-03-31 14:19:24.561 INFO 68528 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.71]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')Run
cds subscribe --helpto see all available options.
Noteworthy...
Be reminded that these commands are only relevant for local testing. For a deployed app, subscribe to your tenants through the BTP cockpit.
In the CLI commands, we use the pre-defined mock users
aliceanderinwith corresponding tenantst1andt2, see pre-defined mock users.The subscription 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 t1In response to each subscription, the server creates a new persistent tenant database per tenant, keeping tenant data 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 t1To unsubscribe a tenant, run:
shcds unsubscribe ‹tenant› --from http://localhost:4005 -u ‹user›Run
cds unsubscribe --helpto 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 tenant labels in server logs 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', 'cds.UIFlexDeveloper' ] },
carol: { tenant: 't1', roles: [ 'cds.Subscriber', 'admin', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper' ] },
dave: { tenant: 't1', roles: [ 'cds.Subscriber', 'admin' ], features: [] },
erin: { tenant: 't2', roles: [ 'cds.Subscriber', 'admin', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper' ] },
fred: { tenant: 't2', features: ... },
me: { tenant: 't1', features: ... },
yves: { roles: [ 'internal-user' ] }
'*': true //> all other logins are allowed as well
},
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. The 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 iOnce: add a deployment descriptor:
cds add mtacds add helmFreeze the npm dependencies for server and MTX sidecar:
npm update --package-lock-only
npm update --package-lock-only --prefix mtx/sidecarBuild and deploy:
mbt build -t gen --mtar mta.tar
cf deploy gen/mta.tar -fcds 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 ./chartSubscribe
Create a BTP subaccount to subscribe to your deployed application. This subaccount has to be in the same region as the provider subaccount, for example, us10.
See the list of all available regions.

In your subscriber account go to Instances and Subscription and select Create.

Select bookshop and use the only available plan default.

Learn more about subscribing to a SaaS application using the SAP BTP cockpit.Learn more about subscribing to a SaaS application using the btp CLI.
You can now access your subscribed application via Go to Application.

As you can see, your route doesn't exist yet. You need to create and map it first.
404 Not Found: Requested route ('...') does not exist.Leave the window open. You need the information to create the route.
Cloud Foundry
Use the following command to create and map a route to your application:
cf map-route ‹app› ‹paasDomain› --hostname ‹subscriberSubdomain›-‹saasAppName›In our example, let's assume our saas-registry is configured in the mta.yaml like this:
- name: bookshop-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: bookshop-${org}-${space}Let's also assume we've deployed to our app to Cloud Foundry org myOrg and space mySpace. This would be the full command to create a route for the subaccount with subdomain subscriber1:
cf map-route bookshop cfapps.us10.hana.ondemand.com --hostname subscriber1-myOrg-mySpace-bookshopLearn how to do this in the BTP cockpit instead…
Switch to your provider account and go to your space → Routes. Click on New Route.

Here, you need to enter a Domain and Host Name.

Let's use this route as example:
https://subscriber1-bookshop.cfapps.us10.hana.ondemand.com
- The Domain here is cfapps.us10.hana.ondemand.com
- The Host Name here is subscriber1-bookshop
Hit Save to create the route.
You can now see the route is created but not mapped to an application yet.

Click on Map Route, choose your approuter module and hit Save.

You should now see the route mapped to your application.

Kyma
Let's use this route as an example: https://my-account.c-abcdef.kyma.ondemand.com
- The Host here is my-account
Create an API Rule using the following code:
apiVersion: gateway.kyma-project.io/v1beta1
kind: APIRule
metadata:
name: <your-api-rule-name>
spec:
gateway: kyma-gateway.kyma-system.svc.cluster.local
host: my_account
service:
name: <your-release-name>-approuter
port: 8080
rules:
- accessStrategies:
- handler: allow
methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
path: /.*You've successfully created the route for your subscribed bookshop multitenant application. Refresh the browser tab of the bookshop application and log in.
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-keyTo achieve this, first bind your SaaS app to its required cloud services, for example:
cds bind -2 bookshop-auth
cds bind -2 bookshop-dbNow, 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-dbMake sure to stop any running CAP servers left over from local testing.
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. Run this in your project root:
cds watch --profile hybridAnd in another terminal:
cds watch mtx/sidecar --profile hybridLearn 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 devAdd Custom Handlers
MTX services are implemented as standard CAP services, so you can register for events just as you would for any application service.
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
})
})In the Java Main Project
For Java, you can also add handlers to the main app:
@After
private void subscribeToService(SubscribeEventContext context) {
String tenant = context.getTenant();
Map<String, Object> options = context.getOptions()
}
@On
private void upgradeService(UpgradeEventContext context) {
List<String> tenants = context.getTenants();
Map<String, Object> options = context.getOptions()
}
@Before
private void unsubscribeFromService(UnsubscribeEventContext context) {
String tenant = context.getTenant();
Map<String, Object> options = context.getOptions()
}SaaS Provisioning Service
The SaaS Provisioning service in SAP BTP sends specific requests to applications when tenants are subscribed or unsubscribed. For these requests, CAP Java internally generates CAP events on the technical service DeploymentService.
For a general introduction to CAP events, see Event Handlers.
Register event handlers for the following CAP events to add custom logic for requests sent by the SaaS Provisioning service. Each event passes a special type of EventContext object to the event handler method and provides event-specific information:
| Event Name | Event Context | Use Case |
|---|---|---|
DEPENDENCIES | DependenciesEventContext | Dependencies |
SUBSCRIBE | SubscribeEventContext | Add a tenant |
UNSUBSCRIBE | UnsubscribeEventContext | Remove a tenant |
You only need to register event handlers to override the default behavior.
Default behaviors:
- A new tenant-specific database container is created through the Service Manager during subscription.
- The tenant-specific database container is deleted during unsubscription.
WARNING
Note that by default a compatibility mode is enabled to ensure compatibility with the old MtSubscriptionService API. If this mode is enabled, the database container is not deleted during unsubscription by default. Refer to Multitenancy (Classic) > Unsubscribe Tenant for more information.
The following sections describe how to register to these events in more detail.
Subscribe Tenant
Subscription events are generated when a new tenant is added. By default, subscription creates a new database container for a newly subscribed tenant. This happens during the @On phase of the SUBSCRIBE event. You can add additional @On handlers to perform additional subscription steps. Note that these @On handlers should not call setCompleted(), as the event processing is auto-completed.
The following examples show how to register custom handlers for the SUBSCRIBE event:
@Before
public void beforeSubscription(SubscribeEventContext context) {
// Activities before tenant database container is created
}
@After
public void afterSubscribe(SubscribeEventContext context) {
// For example, send notification, ...
}Tenant Unsubscription
By default, the tenant-specific database container is deleted during offboarding. This happens during the @On phase of the UNSUBSCRIBE event. You can add additional @On handlers to perform additional unsubscription steps. Note that these @On handlers should not call setCompleted(), as the event processing is auto-completed.
The following example shows how to add custom logic for the UNSUBSCRIBE event:
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
// Activities before offboarding
}
@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
// Notify offboarding finished
}WARNING
If you are accessing the tenant database container during unsubscription, you need to wrap the access into a dedicated ChangeSetContext or transaction. This ensures that the transaction to the tenant database container is committed, before the container is deleted.
Skipping Deletion of Tenant Containers During Tenant Unsubscription
By default, tenant-specific resources (for example, database containers) are deleted during removal. However, you can register a customer handler to change this behavior. This is required, for example, in case a tenant is subscribed to your application multiple times and only the last unsubscription should remove its resources.
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
if (keepResources(context.getTenant())) {
context.setCompleted(); // avoid @On handler phase
}
}Returning Dependencies
The event DEPENDENCIES fires when the SaaS Provisioning service calls the getDependencies callback. Hence, if your application consumes any reuse services provided by SAP, you must implement the DEPENDENCIES event to return the service dependencies of the application. The event must return a list of all of the dependent services' xsappname values. CAP automatically adds dependencies of services to the list, for which it provides dedicated integrations. This includes AuditLog and Event Mesh.
TIP
The xsappname of an SAP reuse service that is bound to your application can be found as part of the VCAP_SERVICES JSON structure under the path VCAP_SERVICES.<service>.credentials.xsappname.
The following example shows this in more detail:
@Value("${vcap.services.<my-service-instance>.credentials.xsappname}")
private String xsappname;
@On
public void onDependencies(DependenciesEventContext context) {
List<Map<String, Object>> dependencies = new ArrayList<>();
dependencies.add(SaasRegistryDependency.create(xsappname));
context.setResult(dependencies);
}Returning a Database ID
When you've registered exactly one SAP HANA instance in your SAP BTP space, a new tenant-specific database container is created automatically. However, if you've registered more than one SAP HANA instances in your SAP BTP space, you have to pass the target database ID for the new database container in a customer handler, as illustrated in the following example:
@Before
public void beforeSubscription(SubscribeEventContext context) {
context.getOptions().put("provisioningParameters",
Collections.singletonMap("database_id", "<database ID>"));
}Logging Support
Logging service support gives you the capability to observe properly correlated requests between the different components of your CAP application in Kibana. This is especially useful for multi-tenant aware applications that use the MTX sidecar. Just enable the Cloud Foundry application-logs service for both, the Java service (see details in Observability > Logging) as well as for the MTX sidecar, to get correlated log messages from these components.
This can easily be seen in Kibana, which is part of the ELK Stack (Elasticsearch/Logstash/Kibana) on Cloud Foundry and available by default with the application-logs service.
Database Schema Update
When shipping a new application version with an updated CDS model, the database schema for each subscribed tenant needs an update. The database schema update needs to be triggered explicitly.
When the database schema update is triggered, the following CAP events are sent.
| Event Name | Event Context |
|---|---|
UPGRADE | UpgradeEventContext |
By registering custom handlers for these events, you can add custom logic to influence the deployment and upgrade process of a tenant. By default, the CAP Java SDK notifies the MTX Sidecar to perform any schema upgrade if necessary. -->
It's often desired to update the whole service in a zero downtime manner. This section doesn't deal with the details about updating a service productively, but describes tool support the CAP Java SDK offers to update tenants.
The following sections describe how to trigger the update for tenants, including the database schema upgrade.
Deploy Main Method
The CAP Java SDK offers a main method in the class com.sap.cds.framework.spring.utils.Deploy that can be called from the command line while the CAP Java application is still stopped. This way, you can run the update for all tenants before you start a new version of the Java application. This prevents new application code to access database artifacts that aren't yet deployed.
In order to register all handlers of the application properly during the execution of a tenant operation main method, the component scan package must be configured. To set the component scan, the property cds.multitenancy.component-scan must be set to the package name of your application.
The handler registration provides additional information that is used for the tenant upgrade, for example, messaging subscriptions that are created.
WARNING
While the CAP Java backend might be stopped when you call this method, the MTX Sidecar application must be running!
This synchronization can also be automated, for example using Cloud Foundry Tasks on SAP BTP and Module Hooks in your MTA.
The main method takes an optional list of tenant IDs as input arguments. If tenant IDs are specified, only these tenants are updated. If no input parameters are specified, all tenants are updated. The method waits until all deployments are finished and then prints the result.
The method returns the following exit codes
| Exit Code | Result |
|---|---|
| 0 | All tenants updated successfully. |
| 1 | Failed to update at least one tenant. Re-run the procedure to make sure that all tenants are updated. |
To run this method locally, use the following command where <jar-file> is the one of your application:
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher [<tenant 1>] ... [<tenant n>]For local development you can create a launch configuration in your IDE. For example in case of VS Code it looks like this:
{
"type": "java",
"name": "MTX Update tenants",
"request": "launch",
"mainClass": "com.sap.cds.framework.spring.utils.Deploy",
"args": "", // optional: specify the tenants to upgrade, defaults to all
"projectName": "<your project>",
"vmArgs": "-Dspring.profiles.active=local-mtxs" // or any other profile required for MTX
}In the SAP BTP, Cloud Foundry environment it can be tricky to construct such a command. The reason is, that the JAR file is extracted by the Java Buildpack and the place of the Java executable isn't easy to determine. Also the place differs for different Java versions. Therefore, we recommend to adapt the start command that is generated by the buildpack and run the adapted command:
sed -i 's/org.springframework.boot.loader.JarLauncher/org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && sed -i 's/-Dsun.net.inetaddr.negative.ttl=0/-Dsun.net.inetaddr.negative.ttl=0 -Dloader.main=com.sap.cds.framework.spring.utils.Deploy/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bashIf you use Java 8, you need to use the following command:
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bashTo run the command manually or automated, you can for example use Cloud Foundry Tasks on SAP BTP and Module Hooks in your MTA. To trigger it as part of a Cloud Foundry Task, login to the Cloud Foundry landscape using the Cloud Foundry command line client and execute:
cf run-task <application_name> "<command>"<application_name> needs to be replaced with the name of a Cloud Foundry application, typically the srv module of your CAP project. You can find the name for example in the section modules in your mta.yaml. <command> represents the adapted start command. The output of the command will be logged to the application logs of the application you have specified in <application_name>.
Behind the Scenes
With adding the MTX services, your project configuration is adapted at all relevant places. Configuration and dependencies are added to your package.json and an xs-security.json containing MTX-specific scopes and roles is created.
For the MTA deployment service dependencies are added to the mta.yaml file. Each SaaS application will have bindings to at least three SAP BTP service instances.
| Service | Description |
|---|---|
Service Manager (service-manager) | CAP uses this service for creating a new SAP HANA Deployment Infrastructure (HDI) container for each tenant and for retrieving tenant-specific database connections. |
SaaS Provisioning Service (saas-registry) | To make a SaaS application available for subscription to SaaS consumer tenants, the application provider must register the application in the SAP BTP Cloud Foundry environment through the SaaS Provisioning Service. |
User Account and Authentication Service (xsuaa) | Binding information contains the OAuth client ID and client credentials. The XSUAA service can be used to validate the JSON Web Token (JWT) from requests and to retrieve the tenant context from the JWT. |
If you're interested, use version control to spot the exact changes.
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.