This guide explains how CAP supports multi-tenant SaaS applications and what you as an implementor and provider of such should know.


    How It Works

    SAP BTP exposes a method for implementing and provisioning SaaS applications on Cloud Foundry. To effectively operate and run SaaS applications and to make them economically successful (TCO), such applications can be deployed once by the application provider, and then be shared by many customers. Such sharing of applications and related computational resources between many tenants is addressed by the multitenancy capabilities of SAP BTP. One important aspect is to guarantee separation between different tenants, so that a tenant isn’t allowed to access any data of other tenants. Apart from data separation, Identity and Access Management must be isolated between tenants.

    CAP supports application developers in implementing the APIs required by SAP BTP. If the application uses a SAP HANA database, multitenancy is available out-of-the-box. CAP features provided:

    • Expose endpoints for un-/subscribing tenants, for returning subscription dependencies and for tenant database updates
    • Out of the box default implementation for un-/subscribing using SAP HANA HDI containers
    • Out of the box default implementation for tenant database updates
    • Application can implement handlers for its own un-/subscribing logic
    • Tenant-specific routing of service requests → use tenant-specific metadata and tenant-specific database connection

    The cds-mtx Module

    CAP provides the npm module for Node.js published as @sap/cds-mtx on

    Java SaaS application deployment

    It provides a number of APIs for implementing SaaS applications on SAP BTP. All APIs are based on CDS. They can be exposed through plain REST, and/or consumed by other Node.js modules when running on the same server as cds-mtx.

    • provisioning: implements the subscription callback API as required by SAP BTP. If a tenant subscribes to the SaaS application, the onboarding request is handled. cds-mtx contacts the SAP HANA Service Manager service to create a new HDI container for the tenant. Then, database artifacts get deployed into this HDI container. In addition, the unsubscribe operation and the “get dependencies” operations are supported.

    • metadata: can be used to get CSN- and EDMX-models, get a list of available services and languages.

    • model: is used to extend existing CDS models and to perform tenant model upgrades after having pushed a new version of the SaaS application.


    These setup instructions build upon the previously introduced bookshop sample project.


    To get started, you need a space in the SAP BTP and a HANA Cloud or HANA-as-a-service database connected to it. Further information can be found here.


    1. Add required services to your .cdsrc.json or package.json:
      "cds": {
        "requires": {
       "db": {
         "kind": "hana",
         "model": ["db", "srv"],
         "vcap": {"label": "service-manager"}
       "uaa": {"kind": "xsuaa"},
       "multitenancy": true
    2. Add the following entries to the dependencies in your package.json:

      "@sap/cds-mtx": "^2",
      "@sap/hana-client": "^4",
      "passport": "^0.4.0",
      "@sap/xssec": "^3"
    3. Add one of the following alternative annotations to the AdminService (admin-service.cds) and the CatalogService (cat-service.cds) of the bookshop app:

      • If you intend to use the Postman collection for testing (see Testing with Postman), enable access by technical users:
          @requires: 'system-user'
      • Otherwise, enable access by authenticated natural persons:
         @requires: 'authenticated-user'


    Deployment Structure

    CAP-based SaaS applications must contain the cds-mtx library, which runs in a Node.js server environment. If the application’s business logic has been implemented in JavaScript, then cds-mtx runs on the same Node.js server as the business logic. In this case, the server exposes the business APIs as well as the APIs of cds-mtx. How to enable multitenancy for a CAP Java application is described in section Multitenancy for the Java runtime.


    Each SaaS application must have bindings to at least three SAP BTP service instances:

    1. User Account and Authentication Service (service xsuaa): Binding information contains the OAuth client ID and client credentials. The XSUAA security library can be used to validate JWT token from requests and to retrieve the tenant context from the JWT.
    2. Service Manager for SAP HANA (service service-manager): CAP uses this service for creating a new SAP HANA Deployment Infrastructure (HDI) container for each tenant and for retrieving the tenant-specific database connection.
    3. SaaS Provisioning Service(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.

    Deploy Using MTA

    The following mta.yaml is a minimal example creating all the services required for a multitenant bookshop application. It can be used to build and deploy the project as described in Deploy Using MTA.


    _schema-version: '3.1'
    ID: bookshop
    version: 1.0.0
    description: "A simple multitenant bookshop application"
      enable-parallel-deployments: true
       - builder: custom
          - npm install --production
          - npx -p @sap/cds-dk cds build --production
     - name: bookshop-srv
       type: nodejs
       path: gen/srv
         EXIT: 1 # required by deploy.js task to terminate
         SAP_JWT_TRUST_ACL: [{"clientid":"*","identityzone":"sap-provisioning"}] # Trust between server and SaaS Manager
        - name: bookshop-db
        - name: bookshop-uaa
        - name: bookshop-registry
        - name: srv-api # required by consumers of CAP services (e.g. approuter)
            srv-url: ${default-url}
        - name: mtx-api # potentially required by approuter
            mtx-url: ${default-url}
     - name: bookshop-db
       type: org.cloudfoundry.managed-service
         service: service-manager
         service-plan: container
         hdi-service-name: ${service-name} # required for Java apps
     - name: bookshop-uaa
       type: org.cloudfoundry.managed-service
         service: xsuaa
         service-plan: application
         path: ./xs-security.json
     - name: bookshop-registry
       type: org.cloudfoundry.managed-service
           - name: bookshop-uaa
           - name: mtx-api
               prop: ~{mtx-url}
               param: ~{mtx-url}
           service: saas-registry
           service-plan: application
             xsappname: bookshop-${space}
             appName: bookshop-${space}
             displayName: bookshop
             description: A simple CAP project.
             #category: "Category"
                 onSubscription: ~{mtx-api/mtx-url}/mtx/v1/provisioning/tenant/{tenantId}
                 onSubscriptionAsync: false
                 onUnSubscriptionAsync: false
                 callbackTimeoutMillis: 300000

    Additionally, an xs-security.json file to specify the XSUAA service settings has to be created, with roles for subscription and deployment.


        "xsappname": "bookshop-${space}",
        "tenant-mode": "shared",
        "scopes": [
          { "name": "$XSAPPNAME.mtcallback", "description": "Subscribe to applications" },
          { "name": "$XSAPPNAME.mtdeployment", "description": "Deploy applications" }
        "role-templates": [
            "name": "MultitenancyAdministrator",
            "description": "Multitenancy Administrator",
            "scope-references": [



    Import this Collection into Postman. It contains a set of variables, which you have to set.

    • From XSUAA binding information, extract clientid, clientsecret, paasTenantId (= tenantid), and paasSubdomain (= identityzone).
    • appurl is the URL of the bookshop-srv application.

    Send requests:

    1. Fetch a JWT token with request “Authenticate → Get Token w/ Client Credentials”. Please check whether the test results are ok, indicating that the JWT token has been stored.
    2. Subscribe the PaaS tenant with request “mtx API → Onboard PaaS tenant
    3. Try all other requests …

    Testing with Server Running Locally

    You can run the bookshop server (bookshop-srv) locally and have it connected to the remote database. To achieve this, you need to provide database connection details, which you can obtain as follows:

    1. Deploy the MTA as described previously.
    2. Run cf env bookshop-srv to obtain output similar to the following:
        "VCAP_SERVICES": {
         "service-manager": [
           "binding_name": null,
           "credentials": {
         "xsuaa": [
    3. Copy the VCAP_SERVICES definition, that is, the part between and including the outermost curly braces.
    4. Create a file default-env.json in the db subfolder of your app.
    5. Paste the copied data.

    To start the server, run npm run start in the root directory of bookshop-srv. By default, the server listens on localhost:4004. (The port can be changed through the PORT environment variable.)

    This address can be used as server URL in the remainder of this documentation. You can also run the Postman requests against it by simply adjusting the appurl accordingly.

    Event Handlers

    If you’re using a CAP Java server, it re-exposes the APIs required by SAP BTP’s SaaS Manager (the Provisioning API). We recommended leveraging the corresponding Java-based mechanisms to add handlers to these APIs. Handlers for the Model-API of cds-mtx must always be implemented on the Node.js server, because this API isn’t re-exposed by the Java stack.

    cds-mtx serves its APIs by using CDS technology. Therefore, any additional application logic can be implemented as CDS event handlers. Event handlers can be implemented in JavaScript files, which are named according to the service name. Those files can be placed into the folder containing your CDS service definition files (for example, the srv/ folder, but they can be placed anywhere where models are loaded from). In case of the cds-mtx APIs, use the files provisioning.js, model.js, and metadata.js. See the following use cases for examples.

    Use Case: Implement a Tenant Provisioning Handler

    You can set an application entry point for the subscription in the SAP BTP Cockpit (usually a UI).

    • Create the file provisioning.js in folder srv:
      module.exports = (service) => {
      service.on('UPDATE', 'tenant', async (req, next) => {
        const res = await next(); // first call default implementation which performs the HDI container creation
        return '<bookshop-srv-url>/admin';

      In the provided code sample, you have to replace <bookshop-srv-url> with the URL of your bookshop-srv application on Cloud Foundry. The /admin endpoint is returned then. It’s important that this endpoint isn’t protected (doesn’t require a JWT token to be called).

    • Rebuild (mbt build -t ./) and redeploy (cf deploy bookshop_1.0.0.mtar) your application.

    Use Case: Application-Specific Scope Checks

    Another common task is to implement application-specific scope checks for the subscription requests. This scope check can be implemented by the “before - UPDATE -tenant” event handler within provisioning.js:

    module.exports = (service) => {
      // event handler for doing some application specific scope checks
      service.before ('UPDATE', 'tenant', async (req) => {
        console.log('[INFO ][BEFORE_UPDATE_TENANT] ' +;
        if (!'Callback')) {     // check for the scope "Callback"
          console.log('[ERROR][BEFORE_UPDATE_TENANT]: JWT does not contain required scope.');
          // Reject request
          const e = new Error('Forbidden');
          e.code = 403;
          return req.reject(e);
        } else {
          console.log('[INFO ][BEFORE_UPDATE_TENANT]: JWT contains relevant scope');

    Use Case: Conditional Offboarding

    Applications may want to prevent the deletion of a tenant database container, if certain conditions are met (for instance, there are still subscribers accessing this database). This behavior can be implemented by replacing the default “DELETE tenant” implementation:

    module.exports = (service) => {
      // Event handler checking a condition before deleting the tenant DB container as a response to an offboarding request.
      // This handler overwrites the default handler!
      service.on ('DELETE', 'tenant', async (req, next) => {
        console.log('[INFO ][ON_DELETE_TENANT] ' +;
        if (1 === 2) {    // put your condition here
          console.log('[INFO ][ON_DELETE_TENANT]: Offboarding will not be executed');
        } else {
           await next();  // call default implementation which is doing regular offboarding


    Job Queue Size

    The job queue size can be modified to allow for more concurrently running operations. For instance, its size could be increased to 10 by adding

    "mtx": {
      "jobqueue": {
        "size": 10

    to your package.json or cdsrc.json, or by setting the environment variable CDS_MTX_JOBQUEUE_SIZE=10.

    API Reference

    All APIs receive and respond with JSON payloads. Application-specific logic (for example, scope checks) can be added using event handlers.

    Provisioning API

    • Subscribe tenant

      PUT /mtx/v1/provisioning/tenant/<tenantId>

      Minimal request body:

      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE"

      Only if eventType is set to CREATE, the subscription is performed . See Develop Multitenant Applications for more details.
      An application can mix in application-specific parameters into this payload, which it can interpret within application handlers. Use the _application_ object to specify those parameters. There’s one predefined sap object, which is interpreted by cds-mtx default handlers. With that object, it’s possible to set service creation parameters to be used by the service manager service when creating HDI container service instances. A typical use case is to provide the database_id to distinguish between multiple SAP HANA DBs mapped to one Cloud Foundry space.

      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE",
      "_application_": {
        "sap": {
          "service-manager": {
            "provisioning_parameters": { "database_id" : "<HANA DB GUID>" },
            "binding_parameters": {"<key>" : "<value>"}

      Having more than one SAP HANA databases mapped to one space, subscription doesn’t work out of the box, unless you’ve specified a default database.
      This can be achieved by specifying a default when mapping a database to a space (not available for HANA Cloud). See Share an Instance with another Cloud Foundry Space for more details.

    • Unsubscribe tenant

      DELETE /mtx/v1/provisioning/tenant/<tenantId>
    • Subscription dependencies

      GET /mtx/v1/provisioning/dependencies

      Response body: Array of String. The default implementation returns an empty array.

    • GET subscribed tenants

      GET /mtx/v1/provisioning/tenant/

      Returns the list of subscribed tenants. For each tenant, the request body is returned which have been used for subscribing the tenant.

    Model API

    • Get cds model content

      GET mtx/v1/model/content/<tenantId>

      Returns the two objects base and extension in response body:

      "base": "<base model cds files>",
      "extension": "<extension cds files>"
    • Activate extensions

      POST mtx/v1/model/activate

      Request body (example):

      "tenant": "tenant1",
      "extension": "<cds extension files>",
      "undeployExtension": false

      The extension element must be a JSON array of arrays. Each first-level array element corresponds to a CDS file containing CDS extensions. Each second-level array element must be a two entry array. The first entry specifies the file name. The second entry specifies the file content. Extension files for data models must be placed into a db-folder. Extensions for services must be placed into srv-folder. Entities of the base model (the nonextended model) are imported by using ... from '_base/...'.

      If the undeployExtension flag is set, all extensions are undeployed from the database that are no longer part of the extensions in the current activation call.

      ❗ Warning
      undeployExtension has to be used with care as it potentially removes tables and their content from the database.

      Request body detailed sample:

      "tenant": "6c07c584-f463-46e5-9340-51958c51dd0a",
      "extension": [
      "using my.bookshop from '_base/db/data-model'; \n extend entity bookshop.Books with { \n ISBN: String; \n rating: Integer \n  }"
      "namespace com.acme.ext; \n entity Categories { \n key ID: String; \n description: String; \n }"
      "using CatalogService from '_base/srv/cat-service'; \n using com.acme.ext from '../db/new-entities'; \n extend service CatalogService with { \n  @insertonly entity Categories as projection on ext.Categories; \n }"
      "undeployExtension": false
    • Upgrade base model from filesystem (asynchronous)

      POST mtx/v1/model/asyncUpgrade

      Request body:

      "tenants": ["tenantId1", "tenantId2", ...],
      "autoUndeploy": <boolean>

      Upgrade all tenants with request body { "tenants": ["all"] }.

      If autoUndeploy is set to true, the auto-undeploy mode of the HDI deployer is used. See HDI Delta Deployment and Undeploy Allowlist for more details.

      Response (example):

      { "jobID": "iy5u935lgaq" }

      The jobID can be used to query the status of the upgrade process:

      GET /mtx/v1/model/status/<jobID>

      During processing, the response can look like:

      "error": null,
      "status": "RUNNING",
      "result": null

      Once a job is finished, the collective status is reported like this:

      "error": null,
      "status": "FINISHED",
      "result": {
          "tenants": {
              "<tenantId1>": {
                  "status": "SUCCESS",
                  "message": "",
                  "buildLogs": "<build logs>"
              "<tenantId2>": {
                  "status": "FAILURE",
                  "message": "<some error log output>",
                  "buildLogs": "<build logs>"

    The status of a job can be QUEUED (not started yet), RUNNING, FINISHED, and FAILED.

    The result status of the upgrade operation per tenant can be RUNNING, SUCCESS, or FAILURE.

    Logs are persisted for a period of 30 minutes before they get deleted automatically. If you request the job status after that, you’ll get a 404 Not Found response.

    Metadata API

    All metadata APIs support eTags. By setting the corresponding header, you can check for model updates.

    • GET edmx

      GET /mtx/v1/metadata/edmx/<tenantId>

      Returns the edmx metadata of the (extended) model of the application.

      Optional URL parameters

    • GET csn

      GET /mtx/v1/metadata/csn/<tenantId>

      Returns the compiled (extended) model of the application.

    • GET languages

      GET /mtx/v1/metadata/languages/<tenantId>

      Returns the supported languages of the (extended) model of the application.

    • GET services

      GET /mtx/v1/metadata/services/<tenantId>

      Returns the services of the (extended) model of the application.


    Extend Tenant Data and Service Models

    cds-mtx provides an API to extend data and service models in a tenant-specific way. The CDS Software Development Kit, @sap/cds-dk, contains a command line client with which an extension developer can maintain and activate cds model extensions files. See section Extending SaaS Applications for more details.

    Perform the following steps to enable and perform extensions of the Getting Started Bookshop SaaS application:

    1. Add required scopes to XSUAA configuration.
    2. Authorize Extension Developer on SAP BTP.
    3. Set up an Extension Project.
    4. Create extension cds files and activate.

    Add Required Scopes

    Tenant-specific CDS model extensions are performed by an extension developer, usually a customer. Therefore, those activities are protected by XSUAA scopes. You can add the following configuration to your xs-security.json:

      "scopes": [
        { "name": "$XSAPPNAME.ExtendCDS", "description": "Create extensions" },
        { "name": "$XSAPPNAME.ExtendCDSdelete", "description": "Undeploy extensions" }
      "role-templates": [
          "name": "ExtensionDeveloper",
          "description": "CDS Extension Developer",
          "scope-references": [

    Rebuild the application while applying the extension descriptor with:

    mbt build -t ./ -e scopes.mtaext

    Then redeploy while just updating the bookshop-uaa service instance:

    cf deploy bookshop_1.0.0.mtar -r bookshop-uaa

    You need to have the User & Role Administrator role to be able to define role templates. If the previous command gives you a related error message, please assign this role to you.

    Authorize Extension Developer

    • Open the overview screen of a Subaccount in SAP BTP Cockpit. This can be any Subaccount, which is already subscribed to your application. For first testing, you can choose your Paas/Provider Subaccount into which you’ve deployed your application. Note down the value for “Subdomain”. In the following, we refer to it as <subdomain>. It represents the tenant.

    • Choose Trust Configuration and click on the active Default identity provider name. Trust Configuration

    • Enter your E-Mail address and choose Show Assignments.
    • Choose Assign Role Collection.
    • From the popup, assign CAP-Getting-Started.

    You’re now authorized to use the extension API to extend cds models for your PaaS/Provider tenant.

    Set Up an Extension Project

    Initialize a new extension project for tenant <subdomain>:

    • Execute cds extend <bookshop-srv-url> -s <subdomain>
    • The system will respond with the message “Invalid passcode”.
    • Fetch a one-time passcode used for authentication by following the link given in the system message. Log on at the browser logon screen. Note down the shown authentication code <passcode>.
    • Execute cds extend <bookshop-srv-url> -s <subdomain> -p <passcode>. A project structure will be generated in a new folder with name <subdomain>.

    Create and Activate an Extension

    • In the db/ folder of this project, create the new file ext.cds:
      using sap.capire.bookshop from '_base/db/schema.cds';
      extend entity bookshop.Books with {
      ISBN : String;
    • Execute cds activate <subdomain> (from the folder one above the folder <subdomain>). For the tenant <subdomain>, the new field ISBN gets activated on the database.
    • Verify that the new field is exposed for Books. Use Postman to fetch a JWT for the extended tenant and send a GET request to <bookshop-srv-url>/browse/$metadata.

    Extensibility Configuration

    Extensibility settings can be added to the mtx settings of your .cdsrc.json file:

    "mtx": {
      "element-prefix": ["Z_", "ZZ_"],
      "namespace-blocklist": ["", "sap."],
      "extension-allowlist": [
            "for": ["my.bookshop.Authors", "my.bookshop.Books"],
            "new-fields": 2
        }, {
            "for": ["CatalogService"],
            "new-entities": 2

    Element Prefixes and Namespace Blocklist

    Field extensions can only be performed with prefixes listed in the `element-prefix’ prefix list.

    Similarly, new entities and services can’t be created within the blocked CDS namespaces listed in namespace-blocklist.
    Namespace restrictions are “inherited”, that is, protecting “” also protects “*”

    Entity Allowlist

    The mtx section also lists the entities and services that can be extended; their fully qualified names are listed in the entity-allowlist.
    The entity-allowlist can contain restrictions to limit the number of extensions that can be created. With new-fields and new-entities, the maximum number of fields for entities and the number of new entities for services can be set.
    It is also possible to specify the entity and service names as namespaces. The restrictions then apply to all entities services in that namespace.

    "extension-allowlist": [
        "for": ["my.bookshop"],
        "new-fields": 2
        "for": ["*"],
        "kind": "service"

    The example allows two new fields for all entities in namespace my.bookshop and an unlimited number of new entities for all services.

    If this list isn’t provided, all entities or services can be extended.

    If entries of the lists can’t be resolved, cds build fails with an error.

    Show/Hide Beta Features