SAP CAP Blog Posts
cancel
Showing results for 
Search instead for 
Did you mean: 
DebashishDas
Active Participant
221

If you have worked with SAP BTP before, you are likely already familiar with the basics of SAP Cloud Application Programming Model (CAP) development, at least within a Trial Account. Rather than starting from the very beginning, this blog assumes a foundational understanding of CAP and focuses on a specific, practical goal: building a SaaS (Software as a Service) multitenant application using the SAP CAP framework, without any database module.

The result is a clean, lightweight SaaS application that demonstrates tenant isolation, subscription lifecycle management, and secure routing — all without the complexity of database provisioning.

What is Multitenancy?

Before we dive in, here is a quick refresher on the concept at the heart of this blog: -

Multitenancy is a software architecture in which a single instance of an application serves multiple customers, known as tenants. Each tenant is logically separated and operates as if they have their own isolated environment, even though they share the same underlying resources — such as the application, infrastructure, and (optionally) databases. 

DebashishDas_1-1777898718147.png

In the context of SAP BTP, the provider is the organization that develops and deploys the application, while the consumers are the subscriber subaccounts that use it. The SaaS Registry service manages this relationship.

Architecture Overview

Our application consists of the following components:

Component

Purpose

Approuter

Handles tenant isolation, authentication (XSUAA), and request routing

CAP Service (srv)

Exposes OData services and handles SaaS Registry subscription callbacks

XSUAA

Provides OAuth2 security with shared tenant mode for multi-tenant JWT handling

SaaS Registry

Manages tenant subscriptions — onboarding and offboarding lifecycle

Project Setup

Step 1: Initialize the Project

Start by creating a new CAP project and adding the required modules. Run the following commands in your terminal:

# Create a new CAP project
cds init cap-saas-app
cds add xsuaa approuter
cds add mta
npm install

Since this application does not use a database, remove the db/ folder from your project. It is not needed and will keep your project clean.

Configuring the Service and Approuter

Add one property to the service module - "APPROUTER_FQDN"

properties:
      APPROUTER_FQDN: "${org}-${space}-cap-saas-approuter.${default-domain}"

The Approuter is the entry point for all tenant requests. It is responsible for identifying which tenant is making the request, authenticating the user via XSUAA, and forwarding the request to the correct backend service.

TENANT_HOST_PATTERN

The most important configuration for multitenancy is  TENANT_HOST_PATTERN . This is a regular expression that the Approuter uses to extract the tenant subdomain from the incoming request hostname.

# In mta.yaml — under the approuter module properties:
TENANT_HOST_PATTERN: "^(.*)-${org}-${space}-${app-name}.${default-domain}"
SAP_JWT_TRUST_ACL:
  - clientid: "*"
    identityzone: "sap-provisioning"

The MTA placeholders resolve at deploy time. For example, in a Trial account with org  7ce04bc6trial  and space  dev , the approuter FQDN (Fully Qualified Domain Name) becomes:

7ce04bc6trial-dev-cap-saas-approuter.cfapps.us10-001.hana.ondemand.com

When a consumer with subdomain  consumer accesses the application, the full URL becomes:

https://consumer-7ce04bc6trial-dev-cap-saas-approuter.cfapps.us10-001.hana.ondemand.com

The Approuter applies  TENANT_HOST_PATTERN  to extract  consumer as the tenant subdomain, which XSUAA then uses to resolve the correct identity zone.

SaaS Registry Configuration

The SaaS Registry service is the backbone of subscription management. It is defined as a managed service resource in  mta.yaml  and is responsible for calling your application’s callback endpoints whenever a tenant subscribes or unsubscribes.

resources:
  - name: cap-saas-app-registry
    type: org.cloudfoundry.managed-service
    requires:
      - name: srv-api
    parameters:
      service: saas-registry
      service-plan: application
      config:
        xsappname: cap-saas-app-xsuaa
        appName: cap-saas
        displayName: 'CAP Multitenant Application'
        description: 'A SaaS app built with SAP CAP'
        category: "Partner Solutions"
        appUrls:
          onSubscription: ~{srv-api/srv-url}/callback/v1.0/tenants/{tenantId}
          onSubscriptionAsync: false
          onUnSubscription: ~{srv-api/srv-url}/callback/v1.0/tenants/{tenantId}
          onUnSubscriptionAsync: false
          getDependencies: ~{srv-api/srv-url}/callback/v1.0/dependencies

The appUrls reference the CAP service (srv) URL — not the Approuter. The SaaS Registry calls these endpoints directly during subscription events, bypassing the Approuter.

Security Configuration (xs-security.json)

The  xs-security.json  file configures your XSUAA instance. For a multitenant application, two settings are critical:

  • tenant-mode: shared — allows the XSUAA instance to serve multiple tenants from a single binding.
  • redirect-uris — must cover all possible Approuter URLs across CF regions
{
  "xsappname": "cap-saas-app-xsuaa",
  "tenant-mode": "shared",
  "scopes": [
    {
      "name": "$XSAPPNAME.Callback",
      "description": "Subscription via SaaS Registry",
      "grant-as-authority-to-apps": [
        "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
      ]
    }
  ],
  "oauth2-configuration": {
    "redirect-uris": [
      "https://*.cfapps.us10-001.hana.ondemand.com/login/callback",
      "https://*.cfapps.us10.hana.ondemand.com/login/callback"
    ],
    "credential-types": ["binding-secret", "x509"]
  }
}

Subscription Callbacks (server.js)

This is where the magic happens. SAP CAP allows you to extend its bootstrap process via  cds.on("bootstrap", ...) . We use this hook to register the raw Express.js routes that the SaaS Registry will call during tenant subscription and unsubscription events.

The bootstrap event is fired immediately after the Express.js app is created, before any CAP middleware or services are added. This makes it the ideal place to register raw REST callbacks.  More info: https://cap.cloud.sap/docs/node.js/cds-server#built-in-server-js

Subscribe Callback (PUT)

When a consumer subscribes, the SaaS Registry calls  PUT /callback/v1.0/tenants/:tenantId . Your handler must return the tenant-specific Approuter URL as a plain string. This URL is stored by the SaaS Registry and used to redirect the consumer.

srv/server.js

"use strict";
const cds = require("@sap/cds");
const express = require("express");
cds.on("bootstrap", (app) => {
  app.use(express.json());
  // Called by SaaS Registry when a tenant subscribes
  app.put("/callback/v1.0/tenants/:tenantId", async (req, res) => {
    const { tenantId } = req.params;
    const body = req.body || {};
    const subscribedSubdomain = body.subscribedSubdomain || tenantId;
    const approuterFQDN = (process.env.APPROUTER_FQDN || "")
      .replace(/^https?:\/\//i, "")
      .replace(/\/+$/, "");
    const tenantUrl = `https://${subscribedSubdomain}-${approuterFQDN}`;
    res.status(200).send(tenantUrl);
  });
  // Called by SaaS Registry when a tenant unsubscribes
  app.delete("/callback/v1.0/tenants/:tenantId", async (req, res) => {
    const { tenantId } = req.params;
    res.status(200).send(`Tenant ${tenantId} successfully unsubscribed.`);
  });
});
module.exports = cds.server;

Important: The subscribe callback must return the APPROUTER URL — not the srv (service) URL. Returning the srv URL will cause XSUAA authentication to fail, as the srv app has no login handler.

Defining the CAP Service

Now for the application logic. We define a simple service that returns tenant-aware information to the consumer. CAP automatically populates  req.tenant ,  req.user , and  req.locale  from the validated XSUAA JWT — no manual token parsing required.

info-service.cds — Service Definition

The CDS file name and the service implementation file name must match for CAP to auto-wire them. We name both  info-service .

// srv/info-service.cds
@requires: 'authenticated-user'
service InfoService {
    function getSubaccount() returns String;
}

info-service.js — Service Implementation

// srv/info-service.js
const cds = require('@sap/cds');
module.exports = cds.service.impl(async function () {
    this.on('getSubaccount', async (req) => {
        // CAP auto-populates these from the XSUAA JWT
        const tenant = req.tenant  || 'unknown';
        const user   = req.user?.id || 'anonymous';
        const locale = req.locale   || 'en';
        return `Tenant: ${tenant} | User: ${user} | Locale: ${locale}`;
    });
});

CAP Auto-Wiring Convention: The file srv/info-service.js is automatically linked to the service defined in srv/info-service.cds because they share the same filename. If the names do not match, CAP cannot wire them and you will get a 501 “No handler” error at runtime.

Deployment

Step 1: Build and Deploy

# Build the MTA archive
mbt build
# Deploy to Cloud Foundry
cf deploy mta_archives/cap-saas-app_1.0.0.mtar

After a successful deployment, two applications will be running in your BTP space:

  • cap-saas-srv — the CAP backend service.
  • cap-saas-approuter — the tenant-aware entry point

DebashishDas_2-1777899433092.png

DebashishDas_3-1777899450315.png

Step 2: Subscribe from a Consumer Subaccount

Navigate to your consumer subaccount in the BTP Cockpit and subscribe to CAP Multitenant Application from the Service Marketplace. The SaaS Registry will call your  PUT /callback/v1.0/tenants/:tenantId  endpoint and store the returned tenant URL.

DebashishDas_5-1777899637448.png

Step 3: Map the Tenant Route

On BTP Trial, wildcard routes are not permitted. After each subscription, you must manually map a CF route for the consumer tenant:

cf map-route cap-saas-approuter cfapps.us10-001.hana.ondemand.com \
  --hostname consumer-7ce04bc6trial-dev-cap-saas-approuter

DebashishDas_4-1777899525696.png

Replace 'consumer' with the actual subdomain of your consumer subaccount. Run 'cf routes' to verify the route was created successfully.

Testing the Application

Once the route is mapped and the consumer has subscribed, open the tenant URL in a browser:

DebashishDas_6-1777899668495.png

DebashishDas_8-1777899805588.png

Consumer 2 -

DebashishDas_9-1777899836707.png

The tenant ID corresponds to the consumer subaccount ID in BTP — confirming that full tenant isolation is working correctly.

Conclusion

In this blog, we built a fully functional SaaS multitenant application on SAP BTP using the CAP framework — with no database required. Here is a summary of what we covered:

  1. Initialized a CAP project with XSUAA and Approuter using cds add commands
  2. Configured TENANT_HOST_PATTERN to extract tenant subdomains from incoming request hostnames
  3. Set up the SaaS Registry with subscription callback URLs in mta.yaml
  4. Configured xs-security.json with tenant-mode: shared and correct redirect URIs
  5. Implemented PUT and DELETE subscription callbacks in server.js using the CAP bootstrap hook
  6. Defined a tenant-aware CAP service using the auto-wiring filename convention
  7. Deployed to Cloud Foundry and mapped tenant routes manually for BTP Trial

This architecture serves as a solid foundation. You can extend it further by adding SAP HANA persistence, role-based authorization, a Fiori Elements UI, or async subscription handling as your application grows.

I hope this blog saves you from the pitfalls I encountered along the way. Happy coding! 🚀

References

SAP CAP Documentation: https://cap.cloud.sap/docs

CAP Server Bootstrap: https://cap.cloud.sap/docs/node.js/cds-server#built-in-server-js

Top liked authors