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.
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 installSince 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.comWhen a consumer with subdomain consumer accesses the application, the full URL becomes:
https://consumer-7ce04bc6trial-dev-cap-saas-approuter.cfapps.us10-001.hana.ondemand.comThe 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/dependenciesThe 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.mtarAfter 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
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.
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-approuterReplace '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:
Consumer 2 -
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:
- Initialized a CAP project with XSUAA and Approuter using cds add commands
- Configured TENANT_HOST_PATTERN to extract tenant subdomains from incoming request hostnames
- Set up the SaaS Registry with subscription callback URLs in mta.yaml
- Configured xs-security.json with tenant-mode: shared and correct redirect URIs
- Implemented PUT and DELETE subscription callbacks in server.js using the CAP bootstrap hook
- Defined a tenant-aware CAP service using the auto-wiring filename convention
- 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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.