Skip to content
On this page

Deploy as Multitenant SaaS Application

This guide explains how to deploy CAP multitenant SaaS applications to SAP BTP Cloud Foundry environment or Kyma environment, and how SaaS Customers would subscribe to them.

Intro & Overview

Software-as-a-Service (SaaS) solutions are deployed once by a SaaS provider, and then used by multiple SaaS customers subscribing to the software.

SaaS applications need to register with the SaaS Provisioning Service to handle subscribe and unsubscribe events. In contrast to single-tenant deployments, databases or other tenant-specific resources aren't created and bootstrapped upon deployment, but upon subscription per tenant.

CAP includes the MTX services, which provide out-of-the-box handlers for subscribe/unsubscribe events, with automatic creation of SAP HANA database containers or SAP Event Mesh instances.

If everything is set up, the following graphic shows what's happening when a user subscribes to a SaaS application:

saas-overview.drawio

  1. The SaaS Provisioning Service sends a subscribe event to the CAP application.
  2. The CAP application delegates the request to the MTX services.
  3. The MTX services use Service Manager to create the database tenant.
  4. The CAP Application connects to this tenant at runtime using Service Manager.

Prepare for Production

The detailed procedure is described in the Deploy to Cloud Foundry guide.

sh
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples/bookshop
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples/bookshop

To start with this guide, you fulfill all Prerequisites and went through Prepare for Production, or instead run this command to fast-forward:

sh
cds add hana,xsuaa,approuter,mta --for production
cds add hana,xsuaa,approuter,mta --for production

Add MTX

To enable MTX services for your project, run the following command:

sh
cds add mtx --for production
cds add mtx --for production

This modifies the modules and resources in mta.yaml for multitenant deployment.

Learn more about MTX services.

Build & Deploy

Cloud Foundry Environment

Consult the Deploy to Cloud Foundry guide for an overview of MTA-based deployment.

sh
mbt build -t gen --mtar mta.tar
mbt build -t gen --mtar mta.tar

Learn more about Build & Assemble

sh
cf deploy gen/mta.tar
cf deploy gen/mta.tar

Learn more about Deploy to Cloud Foundry.

Kyma

Consult the Deploy to Kyma guide for an overview.

  1. To add the Helm Chart to your project, run the following command:

    sh
    cds add helm
    cds add helm
  2. To Build Approuter Image, run the following command:

    sh
    pack build bookshop-approuter \
        --path app \
        --buildpack gcr.io/paketo-buildpacks/nodejs \
        --builder paketobuildpacks/builder:base \
        --env BP_NODE_RUN_SCRIPTS=
    pack build bookshop-approuter \
        --path app \
        --buildpack gcr.io/paketo-buildpacks/nodejs \
        --builder paketobuildpacks/builder:base \
        --env BP_NODE_RUN_SCRIPTS=
  3. To Build CAP Node.js Image, run the following command:

    sh
    pack build bookshop-srv \
        --path gen/srv \
        --buildpack gcr.io/paketo-buildpacks/nodejs \
        --builder paketobuildpacks/builder:base \
        --env BP_NODE_RUN_SCRIPTS=
    pack build bookshop-srv \
        --path gen/srv \
        --buildpack gcr.io/paketo-buildpacks/nodejs \
        --builder paketobuildpacks/builder:base \
        --env BP_NODE_RUN_SCRIPTS=

    Learn more about Building & Pushing Images

  4. Follow the steps mentioned in Deploy using CAP Helm Chart and configure your Helm Chart.

  5. Add backend destinations for Approuter using the backendDestinations property:

    yaml
    backendDestinations:
      srv-api:
        service: srv
      mtx-api:
        service: srv
    backendDestinations:
      srv-api:
        service: srv
      mtx-api:
        service: srv
  6. To deploy your Helm Chart, run the following command:

    sh
    helm upgrade --install bookshop ./chart
    helm upgrade --install bookshop ./chart

Subscribe

Create a BTP subaccount to subscribe to your deployed application. This subaccount has to be in the same region, for example, us10.

Global Account view

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

Select bookshop and use the only available plan default. Inst

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.

Go to Application. subscribed bookshop

As you can see, your route doesn't exist yet. You need to create and map it first.

Leave the window open. You need the information to create the route.

404 Not Found: Requested route ('<prefix>-bookshop.cfapps.eu10.hana.ondemand.com') does not exist.
404 Not Found: Requested route ('<prefix>-bookshop.cfapps.eu10.hana.ondemand.com') does not exist.

Cloud Foundry

Switch to your provider account and go to your space → Routes Route Screen

Click New RouteNew Route wizard

Use the broken route to fill the fields. For the following explanations let's use this route as example: https://my-account.cfapps.acme.com

  • As Domain, use the part that usually starts with cfapps... and ends with .com

    In our example, the domain is: cfapps.acme.com

  • As Host Name, use the part between https:// and .cfapps

    In our example, host name is: my-account

  • Click Save

You can now see the route is created but not mapped yet (none). Overview

Click Map RouteMap Route

Select bookshop, which is the App Router application we just deployed, and save. Overview with new mapped route

Kyma

Use the broken route to fill the fields. For the following explanations let's use this route as example: https://my-account.c-abcdef.kyma.ondemand.com

  • As Host, use the part between https:// and c-abcdef

    In our example, host is: my-account

Create an API Rule using the following code:

yaml
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: /.*
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.

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.

ServiceDescription
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.

Appendix — Configuration

SaaS Service Dependencies

If you require SAP reuse services, that need subscriptions, use cds.mtx.dependencies in your package.json and pass an array of their xsappnames.

The SaaS Provisioning service (getDependencies handler) needs this information.

json
"mtx": {
  "dependencies": ["xsappname-1", "xsappname-2"]
}
"mtx": {
  "dependencies": ["xsappname-1", "xsappname-2"]
}

Maven Dependencies

Multitenancy support is available as optional application feature of the CAP Java SDK. It's already included when you use the cds-starter-cloudfoundry dependency. Otherwise, you can add the following Maven dependency to apply the feature:

xml
<dependency>
    <groupId>com.sap.cds</groupId>
    <artifactId>cds-feature-mt</artifactId>
</dependency>
<dependency>
    <groupId>com.sap.cds</groupId>
    <artifactId>cds-feature-mt</artifactId>
</dependency>

TIP

Having added this dependency to your project, multitenancy support becomes only active when additional services are configured as described in Configuring Required Platform Services and Adding the MTX Sidecar Module.

Local Development and Testing

You can run your application locally with the MTX sidecar and SQLite database in multitenant mode. To do that, add the necessary dependencies and configure MTX sidecar to your project to enable multitenancy and set the following configuration in a new spring profile in your application.yaml file:

yaml
spring:
  config.activate.on-profile: local-mtxs
cds:
  multi-tenancy:
    mtxs:
      enabled: true
    sidecar:
      url: http://localhost:4004
spring:
  config.activate.on-profile: local-mtxs
cds:
  multi-tenancy:
    mtxs:
      enabled: true
    sidecar:
      url: http://localhost:4004

Add mock users with the tenants you want to use.

Add SQLite JDBC driver to your POM:

xml
<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <scope>runtime</scope>
</dependency>

The default project template for CAP does not include it by default. H2 database is not supported in this mode.

WARNING

If you use the default-env.json file to use services from Cloud Foundry for local development, set the property cds.environment.local.defaultEnvPath to a directory that doesn't contain this file. Then, the SQLite database generated by the local sidecar will be used instead. It's best to do that with the separate Spring profile to keep both options available.

Start the MTX sidecar as described in the Multitenancy guide and keep it running. For example, use command cds watch mtx/sidecar in the root directory of your project or cds watch in the directory of the sidecar in a separate terminal. Adjust the default port in the application.yaml file, if sidecar is running on different port.

Use subscription endpoints of CAP Java or endpoints of the sidecar directly to create tenant databases. Your CAP Java application will pick up SQLite files created by the sidecar.

TIP

If you have configured the location of SQLite files in the MTX sidecar and CAP doesn't pick up the files automatically, use the configuration option cds.multiTenancy.mock.sqliteDirectory.

Configuring Platform Services

To enable multitenancy on SAP BTP, three services are involved:

Only when these services are bound to your application, the multitenancy feature is turned on. You can either create and configure these services manually. See section Developing Multitenant Applications in SAP BTP, Cloud Foundry Environment for more details. The following sections describe how to configure and bind these services by means of an mta.yaml file.

XSUAA

A special configuration of the application's XSUAA service instance is required to enable authorization between the SaaS Provisioning service, CAP Java application, and MTX sidecar.

The service can be configured in the mta.yaml by adding an xsuaa resource as follows:

yaml
resources:
  [...]
  - name: xsuaa
    type: com.sap.xs.uaa
    parameters:
      service-plan: application
      path: ./xs-security.json
      config:
        xsappname: <appname>
resources:
  [...]
  - name: xsuaa
    type: com.sap.xs.uaa
    parameters:
      service-plan: application
      path: ./xs-security.json
      config:
        xsappname: <appname>

Choose a value for property xsappname that is unique globally.

Also, you have to create an Application Security Descriptor (xs-security.json) file, which must include the scope mtcallback. The mtcallback scope is required by the onboarding process.

You can also use a custom scope name by configuring it. Use the following application configuration properties: cds.multitenancy.security.subscription-scope

An example xs-security.json file looks like this:

json
{
  "tenant-mode": "shared",
  "scopes": [
    {
      "name": "$XSAPPNAME.mtcallback",
      "description": "Multi Tenancy Callback Access",
      "grant-as-authority-to-apps": [
        "$XSAPPNAME(application, sap-provisioning, tenant-onboarding)"
      ]
    },
    {
      "name": "$XSAPPNAME.cds.ExtensionDeveloper",
      "description": "Extend CAP applications via extension projects"
    }
  ],
  "role-templates": [
    {
      "name": "ExtensionDeveloper",
      "description": "Extension development",
      "scope-references": [
        "$XSAPPNAME.cds.ExtensionDeveloper"
      ]
    }
  ]
}
{
  "tenant-mode": "shared",
  "scopes": [
    {
      "name": "$XSAPPNAME.mtcallback",
      "description": "Multi Tenancy Callback Access",
      "grant-as-authority-to-apps": [
        "$XSAPPNAME(application, sap-provisioning, tenant-onboarding)"
      ]
    },
    {
      "name": "$XSAPPNAME.cds.ExtensionDeveloper",
      "description": "Extend CAP applications via extension projects"
    }
  ],
  "role-templates": [
    {
      "name": "ExtensionDeveloper",
      "description": "Extension development",
      "scope-references": [
        "$XSAPPNAME.cds.ExtensionDeveloper"
      ]
    }
  ]
}

In this example, the grant-as-authority-to-apps section is used to grant the mtcallback scope to the applications sap-provisioning and tenant-onboarding. These are services provided by SAP BTP involved in the onboarding process. The ExtensionDeveloper scope and role can be used to grant SaaS Customers the capabilities to extend the application, if the extensibility feature is enabled.

Learn more about extending SaaS applications.

It isn’t necessary to have the security configuration in a separate file. It can also be added to the mta.yaml file directly.

❗ Warning

The mtcallback scope must not be exposed to any business user, for example, using a role template. Else a malicious user could update or even delete the artifacts of arbitrary tenants.

Service Manager

A service instance of the Service Manager (service-manager) is required for CAP Java to create and use database containers per tenant at application runtime. It doesn’t require special parameters and can be added as a resource in mta.yaml as follows:

yaml
resources:
  [...]
  - name: service-manager
    type: org.cloudfoundry.managed-service
    parameters:
      service: service-manager
      service-plan: container
resources:
  [...]
  - name: service-manager
    type: org.cloudfoundry.managed-service
    parameters:
      service: service-manager
      service-plan: container

SaaS Provisioning Service

A service instance of the SaaS Provisioning service (type saas-registry) is required to make your application known to the SAP BTP Provisioning Service and to register the endpoints that should be called when tenants are added or removed. The service can be configured as a resource in mta.yaml as follows. See section Register the Multitenant Application to the SaaS Provisioning Service for more details.

yaml
resources:
  [...]
  - name: saas-registry
    type: org.cloudfoundry.managed-service
    parameters:
      service: saas-registry
      service-plan: application
      config:
        appName: <app display name>
        xsappname: <appname>
        appUrls:
          getDependencies: ~{srv/url}/mt/v1.0/subscriptions/dependencies
          onSubscription: ~{srv/url}/mt/v1.0/subscriptions/tenants/{tenantId}
          onSubscriptionAsync: true
          onUnSubscriptionAsync: true
          onUpdateDependenciesAsync: true
          callbackTimeoutMillis: 3600000
    requires:
      - name: srv
resources:
  [...]
  - name: saas-registry
    type: org.cloudfoundry.managed-service
    parameters:
      service: saas-registry
      service-plan: application
      config:
        appName: <app display name>
        xsappname: <appname>
        appUrls:
          getDependencies: ~{srv/url}/mt/v1.0/subscriptions/dependencies
          onSubscription: ~{srv/url}/mt/v1.0/subscriptions/tenants/{tenantId}
          onSubscriptionAsync: true
          onUnSubscriptionAsync: true
          onUpdateDependenciesAsync: true
          callbackTimeoutMillis: 3600000
    requires:
      - name: srv

It’s required to configure the parameters:

  • appName: Choose an appropriate application display name.
  • xsappname: Use the value for xsappname you configured at your UAA service instance.
  • appUrls: Configure the callback URLs used by the SaaS Provisioning service to get the dependencies of the application and to trigger a subscription. In the above example, the property ~{srv/url} is used, which is provided by the srv module. See section Configuring the Java Service for more details. If you use different module and property names for your CAP Java backend module, you have to adapt these properties here accordingly.

TIP

Make sure to choose the asynchronous subscription (onSubscriptionAsync: true) and unsubscription mode (onUnSubscriptionAsync: true). Always keep callbackTimeoutMillis in sync with cds.multitenancy.provisioningservice.pollingTimeout (default: 10min).

Adding the MTX Sidecar

This section describes how to use the cds-mtxs Node.js module and add the MTX sidecar microservice to the mta.yaml file.

To define the dependencies and start command, create a subfolder named mtx/sidecar, and within that folder create a file named package.json :

json
{
  "cds": {
    "requires": {
      "[production]": {
        "db": "hana-mt",
        "auth": "xsuaa"
      },
      "[development]": {
        "db": "sqlite",
        "auth": "dummy"
      },
      "cds.xt.ModelProviderService": "in-sidecar",
      "cds.xt.DeploymentService": true,
      "cds.xt.SaasProvisioningService": true,
      "cds.xt.ExtensibilityService": true
    }
  },
  "devDependencies": {
    "sqlite3": "^5"
  },
  "engines": {
    "node": "^18"
  },
  "scripts": {
    "start": "cds run",
    "build": "npm install && cds build ../.. --for mtx-sidecar && cd gen && npm install"
  }
}
{
  "cds": {
    "requires": {
      "[production]": {
        "db": "hana-mt",
        "auth": "xsuaa"
      },
      "[development]": {
        "db": "sqlite",
        "auth": "dummy"
      },
      "cds.xt.ModelProviderService": "in-sidecar",
      "cds.xt.DeploymentService": true,
      "cds.xt.SaasProvisioningService": true,
      "cds.xt.ExtensibilityService": true
    }
  },
  "devDependencies": {
    "sqlite3": "^5"
  },
  "engines": {
    "node": "^18"
  },
  "scripts": {
    "start": "cds run",
    "build": "npm install && cds build ../.. --for mtx-sidecar && cd gen && npm install"
  }
}

Note that it's required to activate the ModelProviderService in the sidecar application so that tenant-specific CDS models are served at runtime. In contrast, ExtensibilityService is only required for projects that allow custom extensions at runtime.

Activate multitenancy in the main application's .cdsrc.json. If required, activate extensibility and/or feature toggles as well:

json
{
  "requires": {
    ...
    "multitenancy": true,
    "extensibility": true,
    "toggles": true,
    ...
  }
}
{
  "requires": {
    ...
    "multitenancy": true,
    "extensibility": true,
    "toggles": true,
    ...
  }
}

Learn more about MTX services and their configuration.

Next, add the required dependencies by calling the below command in the mtx/sidecar directory:

sh
npm add @sap/cds @sap/cds-mtxs @sap/xssec hdb express passport
npm add @sap/cds @sap/cds-mtxs @sap/xssec hdb express passport

TIP

The cds-mtxs module requires @sap/cds version 6 at least.

Because the MTX sidecar needs to know the base CDS model and features, you additionally need to configure the build task by means of the .cdsrc.json file in the root folder of your project:

json
{
    "requires": {
        "multitenancy": true,
        "extensibility": true,
        "toggles": true
    },
    "build": {
        "target": ".",
        "tasks": [
            {
                "for": "java"
            },
            {
                "for": "mtx-sidecar"
            },
            {
                "for": "hana"
            }
        ]
    },
    "hana": {
        "deploy-format": "hdbtable"
    }
}
{
    "requires": {
        "multitenancy": true,
        "extensibility": true,
        "toggles": true
    },
    "build": {
        "target": ".",
        "tasks": [
            {
                "for": "java"
            },
            {
                "for": "mtx-sidecar"
            },
            {
                "for": "hana"
            }
        ]
    },
    "hana": {
        "deploy-format": "hdbtable"
    }
}

This configuration relates to the default project folder layout with app, db, srv, fts and mtx/sidecar. If you have a different layout, please adapt the configuration accordingly.

TIP

Make sure to run cds build with @sap/cds-dk version 6 at least.

A detailed description of this configuration file can be found in section Build Configuration.

Now, add the mtx-sidecar module to your mta.yaml file:

yaml
modules:
  [...]
  - name: mtx-sidecar
    type: nodejs
    path: mtx/sidecar
    build-parameters:
      builder: custom
      build-result: gen
      commands:
        - npm run build
    parameters:
      memory: 256M
      disk-quota: 512M
    requires:
      - name: xsuaa
      - name: service-manager
    provides:
      - name: sidecar
        properties:
          url: ${default-url}
modules:
  [...]
  - name: mtx-sidecar
    type: nodejs
    path: mtx/sidecar
    build-parameters:
      builder: custom
      build-result: gen
      commands:
        - npm run build
    parameters:
      memory: 256M
      disk-quota: 512M
    requires:
      - name: xsuaa
      - name: service-manager
    provides:
      - name: sidecar
        properties:
          url: ${default-url}

The mtx-sidecar module requires the XSUAA and Service Manager services. Also the sidecar exposes its own URL (note that ${default-url} is a placeholder for the own URL) which is required to configure the service module. The Cloud MTA Build Tool executes the build script which is provided in mtx/sidecar/package.json to build the MTX Sidecar application.

Configuring the Java Service

To make use of the previously mentioned services and the MTX sidecar in your CAP Java multitenant application, you need to enhance the srv module in the mta.yaml file:

yaml
modules:
  [...]
  - name: srv
    type: java
    path: srv
    parameters:
      [...]
    requires:
      - name: service-manager
      - name: xsuaa
      - name: saas-registry
      - name: sidecar
        properties:
          CDS_MULTITENANCY_MTXS_ENABLED: true
          CDS_MULTITENANCY_SIDECAR_URL: ~{url}
      - name: app
        properties:
          CDS_MULTITENANCY_APPUI_URL: ~{url}
          CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "."
    provides:
      - name: srv
        properties:
          url: '${default-url}'
modules:
  [...]
  - name: srv
    type: java
    path: srv
    parameters:
      [...]
    requires:
      - name: service-manager
      - name: xsuaa
      - name: saas-registry
      - name: sidecar
        properties:
          CDS_MULTITENANCY_MTXS_ENABLED: true
          CDS_MULTITENANCY_SIDECAR_URL: ~{url}
      - name: app
        properties:
          CDS_MULTITENANCY_APPUI_URL: ~{url}
          CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "."
    provides:
      - name: srv
        properties:
          url: '${default-url}'
  • The environment variable CDS_MULTITENANCY_MTXS_ENABLED sets application property cds.multitenancy.mtxs.enabled and is used to activate streamlined MTX (default is classic MTX).
  • CDS_MULTITENANCY_SIDECAR_URL sets the application property cds.multitenancy.sidecar.url. This URL is required by the CAP Java runtime to connect to the MTX Sidecar application and is derived from the property url of the mtx-sidecar module.
  • Similarly, CDS_MULTITENANCY_APPUI_URL configures the URL that is shown in the SAP BTP Cockpit. Usually it points to the app providing the UI, which is the module app in this example.

The tenant application requests are separated by the tenant specific app URLs. The tenant specific URL is made up of:

https://<subaccount subdomain><CDS_MULTITENANCY_APPUI_TENANTSEPARATOR><CDS_MULTITENANCY_APPUI_URL>
https://<subaccount subdomain><CDS_MULTITENANCY_APPUI_TENANTSEPARATOR><CDS_MULTITENANCY_APPUI_URL>

You can also define the environment variable CDS_MULTITENANCY_APPUI_TENANTSEPARATOR in the extension descriptor. It’s either "." or "-" depending on whether you run the app on a live landscape or on Canary. The extension descriptor file could look like this:

yaml
_schema-version: "3.1"
extends: my-app
ID: my-app.id

modules:
  - name: srv
    properties:
      CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-"
  - name: app
    properties:
      TENANT_HOST_PATTERN: ^(.*)-${default-uri}
_schema-version: "3.1"
extends: my-app
ID: my-app.id

modules:
  - name: srv
    properties:
      CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-"
  - name: app
    properties:
      TENANT_HOST_PATTERN: ^(.*)-${default-uri}

Deployment to Kyma

Build

  1. Follow the Build Images Guide to build your CAP Java and Approuter Image.

  2. To build your sidecar image, run the following commands:

    sh
    cds build --production
    cds build --production
    sh
    pack build bookshop-sidecar --path mtx/sidecar/gen \
    --buildpack gcr.io/paketo-buildpacks/nodejs \
    --builder paketobuildpacks/builder:base \
    --env BP_NODE_RUN_SCRIPTS=""
    pack build bookshop-sidecar --path mtx/sidecar/gen \
    --buildpack gcr.io/paketo-buildpacks/nodejs \
    --builder paketobuildpacks/builder:base \
    --env BP_NODE_RUN_SCRIPTS=""
  3. Follow the Push Images Guide and push images to your registry.

Deploy

  1. To add the Helm Chart to your project, run the following command:

    sh
    cds add helm
    cds add helm
  2. Follow the steps mentioned in Deploy using CAP Helm Chart and configure your Helm Chart.

  3. To deploy your Helm Chart, run the following command:

    sh
    helm upgrade --install bookshop ./chart
    helm upgrade --install bookshop ./chart

Adding Custom Handlers

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 NameEvent ContextUse Case
DEPENDENCIESDependenciesEventContextDependencies
SUBSCRIBESubscribeEventContextAdd a tenant
UNSUBSCRIBEUnsubscribeEventContextRemove 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:

java
@Before
public void beforeSubscription(SubscribeEventContext context) {
    // Activities before tenant database container is created
}

@After
public void afterSubscribe(SubscribeEventContext context) {
    // For example, send notification, ...
}
@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 off-boarding. 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:

java
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
    // Activities before off-boarding
}

@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
    // Notify off-boarding finished
}
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
    // Activities before off-boarding
}

@After
public void afterUnsubscribe(UnsubscribeEventContext context) {
    // Notify off-boarding 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.

java
@Before
public void beforeUnsubscribe(UnsubscribeEventContext context) {
    if (keepResources(context.getTenant())) {
      context.setCompleted(); // avoid @On handler phase
    }
}
@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:

java
@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);
}
@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:

java
@Before
public void beforeSubscription(SubscribeEventContext context) {
    context.getOptions().put("provisioningParameters",
        Collections.singletonMap("database_id", "<database ID>"));
}
@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:

Kibana screenshot

Subscribe & Unsubscribe

The CAP Java SDK offers main methods for Subscribe/Unsubscribe in the classes com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe that can be called from the command line. This way, you can run the tenant subscribe/unsubscribe for the specified tenant. This would trigger also your custom handlers, which is useful for the local testing scenarios.

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 subscribe, for example, messaging subscriptions that are created.

WARNING

You can stop the CAP Java back end when you call this method, but the MTX Sidecar application must be running!

The main method optionally takes tenant ID (string) as the first input argument and tenant options (JSON string) as the second input argument. Alternatively, you can use the environment variables MTCOMMAND_TENANTS and MTCOMMAND_OPTIONS instead of arguments. The command-line arguments have higher priority, so you can use them to override the environment variables.

The method returns the following exit codes.

Exit CodeResult
0Tenant subscribed/unsubsribed successfully.
3Failed to subscribe/unsubscribe the tenant. Rerun the procedure to make sure the tenant is subscribed/unsubscribed.

To run this method locally, use the following command where <jar-file> is the one of your applications:

sh
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher <tenant> [<tenant options>]
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher <tenant> [<tenant options>]

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 adapting the start command that is generated by the buildpack and run the adapted command:

sh
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Subscribe/Unsubscribe org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash

If you use Java 8, you need to use the following command:

sh
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.Subscribe/Unsubscribe/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash
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.Subscribe/Unsubscribe/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | sed 's/^/ MTCOMMAND_TENANTS=my-tenant [MTCOMMAND_TENANTS=<tenant options>]/' | bash

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 NameEvent Context
UPGRADEUpgradeEventContext

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 CodeResult
0All tenants updated successfully.
1Failed 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:

sh
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher [<tenant 1>] ... [<tenant n>]
java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher [<tenant 1>] ... [<tenant n>]

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:

sh
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 | bash
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 | bash

If you use Java 8, you need to use the following command:

sh
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 | bash
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 | bash

For local development you can create a launch configuration in your IDE. For example in case of VS Code it looks like this:

json
{
    "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
}
{
    "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
}

MtSubscriptionService API Compatibility

The former MtSubscriptionService API described in Multitenancy (Classic) has been deprecated, but can still be used in a compatibility mode. This compatibility mode is enabled by default and can be disabled by setting application property cds.multitenancy.compatibility.enabled to false.

In this compatibility mode the event handlers as described in Multitenancy (Classic) are still triggered in addition to the event handlers of the new API. Therefore the UNSUBSCRIBE event no longer deletes the tenant database container by default. Refer to Multitenancy (Classic) > Unsubscribe Tenant for more information.

As part of the compatibility mode the HTTP-based deploy REST API is also available. However it only uses the deprecated MtSubscriptionService API. It doesn't trigger the UPGRADE event of the new API.

Once all event handlers have been migrated to the new API and the compatibility mode is no longer required it is recommended to disable the compatibility mode. The compatibility mode will be removed again in the future.

Configuration Properties

A number of multitenancy settings can be configured through application configuration properties. See section Application Configuration for more details. They can be found in the following table. The prefix for multitenancy-related settings is cds.multitenancy.

NameDescriptionDefault
servlet.pathPath of the subscription and deployment endpoints/mt/v1.0/subscriptions
servlet.enabledFlag to deactivate the endpointstrue
datasource.poolPool to use for the tenant-dependent data source. Possible values are: hikari, tomcat, atomikoshikari
datasource.<pool>.<property>Pool-specific properties. See the tools documentation for HikariCP, Apache Tomcat, and Atomikos for more details.Default depends on the pool and property
datasource.combinePools.enabledOnly one data source pool per database is created.false
servicemanager.timeoutTimeout for requests to the Service Manager in seconds3600
security.subscriptionScopeScope necessary to call the subscription endpointsmtcallback
sidecar.urlURL of the MTX sidecarNot set by default
app-ui.urlURL of the application UINot set by default
app-ui.tenantSeparatorSeparator of the tenant in the URL, for example, "." or "-"Not set by default
mtxsEnables the usage of @sap/cds-mtxs module (MTX streamlined)false
provisioning.pollingTimeoutThe maximum waiting time for the provisioning operation to finish10 minutes
mock.enabledOption to deactivate local multitenancy mode.true
mock.sqliteDirectoryRelative path to directory where SQLite files for tenants are stored.Not set by default. Only specify if the MTX sidecar stores database files in a non-default location, as CAP discovers database files in common directories of the project.
deploy.componentScanSpecifies the package to be included in component scan of Deploy Main Method with custom handlers.Not set by default