Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

SAP Event Mesh: Multitenant Sample Scenario 1

This blog post gives an example for an eventing scenario which includes a multitenant application.
It runs on SAP Business Technology Platform, Cloud Foundry environment and uses SAP Event Mesh.
This is not an official reference application, it is just some sample code to help you in quickly setting up your project.

Quicklinks:
Quick Guide
Sample Code

Introduction

Eventing scenario:
This is – as usual – represented by sender and receiver applications.
In our example, both applications run in the cloud, i.e. in the SAP Business Technology Platform (SAP BTP).

Extension scenario:
To illustrate the technical setup, a simple example is helpful.
In this blog post we imagine an international soccer club.
It uses a SAP on-Premise enterprise system.
And there is an extension in SAP BTP, connected to the on Prem system via eventing.

Multitenancy scenario:
The interesting part is the multitenant app.
We’re assuming, the app that represents the on Prem system is a normal singletenant app.
The extension app is designed as multitenant app (SaaS – software-as-a-service).

Example scenario:
As an example, we’ve chosen an international soccer club.
To manage all the enterprise functionality like stadium, employees, customers, profits, products, etc it uses an SAP enterprise system.
Now as it has grown popular, it needs an extension to the software. It should help in managing fans and merchandising articles all over the world.
SAP BTP is ideal platform for such extension.
And using eventing is the core to keep the data in sync at real time.
To keep our example small, we just offer functionality to register new fans.
While visitors of the stadium can still register there (offline) as fans, it is now possible to use the cloud app to enter a name, and – shoot -> new fan registered.

Architecture:
We’re using 2 subaccounts in SAP BTP.
The first one is the customer account – it is the customer’s world.
Here we deploy the singletenant app, representing the onPrem system.
Let’s call it soccerclub.

The second subaccount is owned by SAP (or partner) and it is the provider account.
Here we deploy the multitenant application.
We call it fanshop – it is accessed by fans all over the world, they can purchase merchandising articles and can register as fans and subscribe to events, newsletters, all that stuff.
As it is a multitenant app, the customer subscribes to it in the customer subaccount (the first one).

The diagram shows our 2 subaccounts, the 2 deployed applications and the subscription:

Security:
To keep it really simple, we don’t support security in this first version.
We’ll cover it in next blog post.

Content

1. Create Soccerclub App
2. Create Fanshop App
3. Run the Scenario
Appendix 1: Soccerclub App
Appendix 2: Fanshop App

Prerequisites

Access to SAP BTP Cloud Foundry environment, productive environment, including 2 subaccounts.
Basic understanding of multitenancy and Cloud Foundry.
Basic Node.js skills

Preparation

Subaccounts

We need 2 subaccounts, as usual for multitenancy scenarios.
One of them will be the provider account and it needs to be entitled for creating an instance of SAP Event Mesh.

Create Project

We create a root project folder C:\club containing 2 subfolders for the 2 applications
C:\club
fanshop
soccerclub

Or this screenshot:

Each app folder contains a few files required for little node server apps:

C:\club
fanshop
config-messaging.json
config-saasreg.json
config-security.json
manifest.yml
package.json
server.js
soccerclub
manifest.yml
package.json
server.js
For your convenience please refer to below screenshot

The content of the files can be copied from the Appendix section.

1. Create Soccerclub App

We start with the application that represents the singletenant on-Premise system.
From messaging perspective, it is the message receiver.
It is as simple as possible, it just displays the fans that are stored in the system.
In addition:
It is already aware of receiving events.
It offers a webhook endpoint for incoming events.
We will  send the events in our second application.

1.0. Environment

Before we start, we make sure that we’ve chosen the proper environment, with respect to cloud account.
The “soccerclub” app represents the customer’s installation of enterprise system, as such we deploy it to the BTP-subaccount which we’ve chosen to be the customer subaccount (or consumer subaccount).

1.1. Create service instances

Our simple onPrem fake app doesn’t use any cloud services.
What we need it the Event Mesh dashboard.
Our customer wants connection based on eventing, so he needs to do some configuration for the eventing.
So we need the event mesh dashboard in the customer account.
It is subscription based, we only need the required entitlements and we can subscribe.

Subscribe to Event Mesh Dashboard

To use the dashboard, we need to subscribe to the “standard” plan.
We go you our subaccount (the one we use as customer).
The we go to Service Marketplace.
We search for Event Mesh – then choose the “Application Plan” with name “standard” and press “create”:

After the subscription is created, we can NOT press on “Go to Application”.
To access the dashboard, our user needs to have the required roles.

Roles
To assign the required roles to our user:
In our subaccount, we go to Security->Role Collections and create a new Role Collection.
We open it, press “Edit” and go to the “Roles” tab.
We open the value help of the columen “Role Name”.
To find the Event-Mesh-roles, we select “xbem-app” as filter for “Application Identifier”.
Then we can just select all roles and press “Add”.
Then we need to add our user in the “Users” section.
Note that we should use the proposal of the UI.
To add the user, we need to press the big +
Finally we press “Save” in the upper right corner.

Open
Now we can go back to the “Instances and Subscriptions” screen, choose our Event Mesh subscription and press on “Go to Application”.
We see: it is empty.
That is expected, as we haven’t created a messaging client yet.

1.2. Create app

Our simple app consists of 3 files

package.json

The app is very simple and just requires express to run a server:

{
  "dependencies": {
    "express": "^4.17.1"

manifest.yml

The manifest is short and basically defines the name of our app and its URL:

applications:
  - name: soccerclubapp
    routes:
    - route: soccerclubapp.cfapps.sap.hana.ondemand.com

server.js

The application code defines an endpoint which allows for incoming POST requests.
Such requests are sent from the SAP Event Mesh webhook subscription (we’ll register our endpoint in the Event Mesh dashboard in chapter 3).
Each event that is fired from our extension app will be forwarded to our endpoint.
As such, the implementation of the endpoint takes care of accessing the payload of the event.
For simplicity reasons, we just print it to the log.

app.post('/webhook/fanshopevents', (req, res) => {
    console.log(`===> [/webhook/fanshopevents] received message: ${JSON.stringify(req.body)}.`)
    res.status(201).send()

The complete code can be found in the Appendix section.
That’s already it.

1.3. Deploy and Test

We can test the endpoint with a postman request as follows:

URL https://soccerclubapp.cfapps.sap.hana.ondemand.com/webhook/fanshopevents
Method POST
Headers content-type:application/json
Body {“message”:”hello”}

After firing the request, we check the log of our app and see the message printed there.

Small recap

We’ve created and deployed a little app that represents an SAP enterprise system.
It provides an endpoint that will be used for webhook subscription.
It receives events coming from SAP Event Mesh.
It reads the event payload and writes it to the console.

Now that our enterprise system is up and running and ready to receive messages, we can go ahead and create our multitenant extension app to send events.

2. Create Fanshop App

In our scenario, we write an extension app that is designed as multitenant app.
For instance, an SAP partner develops the multitenant app and deploys it to his (provider) account.
The customer (soccer club) subscribes to it in his own (customer) account.
The app can then be used by fans for online registration.

2.0. Environment

Before we start, we make sure that we’ve chosen the proper environment, with respect to cloud account.
The fanshop app represents the partner development of an extension , designed as multitenant application.
As such we deploy it to the BTP-subaccount which we’ve chosen to be the provider subaccount.

2.1. Create service instances

We need instances of Event Mesh (for sending events), SaaS registry (for multitenancy) and XSUAA (required by SaaS Registry).
The creation command uses config files which we created in the preparation section and which can be found in the appendix.

Below diagram illustrates the instances and subscriptions created in each subaccount:

Create Event Mesh instance

The important setting, to make the Event Mesh tenant-aware, is the instance-type property which needs to be set to reuse:

{
  "emname": "fanshopmessagingclient",
  "namespace": "zsoccerclub/scenario/fanshop",
  "instanceType": "reuse",
  "options": {
      "management": true,
      "messagingrest": true,
      "messaging": true

Reminder:
make sure to switch to the provider subaccount before creating the service instances

The creation command:
cf cs enterprise-messaging default fanshopMsg -c config-messaging.json

Note:
The service plan “default” must be used, it is not available in Trial account

Note:
New “namespace” must be registered

Create instance of XSUAA

We aren’t applying security features to our apps, however, we need to create an instance of XSUAA, because it is required by SaaS Registry.
Our XSUAA instance needs to be multitenant aware:

{
    "xsappname": "fanshopxsappname",
    "tenant-mode": "shared"
}

cf cs xsuaa application fanshopXsuaa -c config-security.json

We need to specify the xsappname of our XSUAA instance when we configure the SaaS Registry.
Since the xsappname is generated during creation, we need to create a Service Key:
cf csk fanshopXsuaa sk
Once we have the service key, we should view the content, so we can see the generated name:
cf service-key fanshopXsuaa sk
from the service key content, we copy the value of the property xsappname, it should look like this:

Once copied the xsappname, we don’t need the service key anymore.
Poor service key….

Create instance of SaaS Registry

Before we proceed, we have to copy&paste the value of property xsappname to the config file of saas registry, as value of property appid.
In my example:

{
  "appId": "fanshopxsappname!t14860",
  "appName": "fanshopSaasregAppname",
  "appUrls": {
    "getDependencies" : https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/dependencies,
    "onSubscription" : https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/{tenantId}

Note:
The URLs need to be adapted to your landscape and don’t forget to paste your xsappname.

So now we can create the saas-registry instance:
cf cs saas-registry application fanshopSaasreg -c config-saasreg.json

2.2. Create Application

After the service instances are ready, let’s go through the process of creating the multitenant app.
Again, we need 3 files.

manifest.yml

The deployment descriptor specifies the dependencies to our 3 service instances and the URL for access.
The app will be used  by subscriber, as such we need to define a route which contains the subdomain of the subscribing subaccount as prefix.
e.g.
consumer-fanshopapp.cfapps.eu10.hana.ondemand.com
or more generic:
<subdomain>-<app>.cfapps.eu10.hana.ondemand.com

This is the usual way of developing multitenant applications:
The subdomain of the subscriber is concatenated with the app-URL.
We’ll see it below.
Once it comes to productive usage, the subdomain concatenation is replaced by custom domains.
In the meantime, our subscriber can only open the app if there is a route for it.
That’s why we specify the route in the manifest, so it will be created during deploy.
This is of course a little trick: we already know the subdomain that will be subscribing, because we’re in testing mode.

How to retrieve the subdomain of the subaccount?
Go to the subaccount used for customer, go to the overview page and copy the subdomain from there:

In my example, the manifest and its routes look as follows:

applications:
  - name: fanshopapp
    routes:
    - route: fanshopapp.cfapps.sap.hana.ondemand.com
    - route: consumer-fanshopapp.cfapps.sap.hana.ondemand.com

Note:
You need to adapt the first route to match your landscape,
and the second route, to match your subdomain.

Note:
If you aren’t sure, you can as well omit the second route in the manifest.
Alternatively, after deployment and subscription, you can still create and map a new route with the following command:
map-route myApp example.com –hostname myhost
Result:
myhost.example.com
Example:
cf map-route fanshopapp cfapps.sap.hana.ondemand.com –hostname consumer-fanshopapp

package.json

This application will send events to Event Mesh and it will be using the REST API, as offered by Event Mesh.
We’re using the node-fetch module for firing the request and we’re using the @sap/xssec module for fetching a JWT token.

{
    "dependencies": {
        "@sap/xssec": "latest",
        "express": "^4.16.2",
        "node-fetch": "2.6.2"

server.js

Our fanshop application is a server app which serves a very simple homepage which is reached when pressing “Go to Application” after subscription.

But first of all, we have to implement the 3 multitenancy callbacks, which are invoked when a subscriber presses the “Create Subscription” button.
Let’s have a quick look into the subscribe-callback.
The SaaS Registry calls us with an UPDATE operation and sends the info about the current subscriber (subdomain, tenant-id, etc) in the request body.
What we have to do in the code is to send back the app-URL for this specific subscriber.
Like that, every subscriber will have his own application-URL, dedicated for him.
In our example, during development/testing phase, what we do is:
We access the data in the request body.
We take the subdomain.
We prepend it in front of the normal app-URL
And at the end, we append the concrete endpoint of the app homepage (typically an index.html).
Unclear?
We want the final app URL should look like this:
<subdomain>-<appURL>/<endpoint>

So in my example, the resulting URL would be:
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/app
This URL can only be used by the customer owning the subaccount with subdomain “consumer”.

app.put('/mtcallback/:tenant_id', (req, res) => {
   const appHost = req.hostname  
   const subDomain = req.body.subscribedSubdomain
   res.status(200).send(`https://${subDomain}-${appHost}/app`)
})

Also important to note:
The dependencies callback.
The SaaS Registry asks us if we have dependencies that need to be notified about the subscription.
If yes, then the SaaS Registry will also invoke the callbacks of the (multitenant-aware) services.
In our case: in fact, we’re using the Event Mesh service and it is tenant-aware and it needs to be notified.
We will see below, why this is important.
In the code, we just return the value of xsappname of our Event Mesh instance (which we read from binding):

app.get('/mtcallback/dependencies', (req, res) => {
   res.status(200).json([{'xsappname': CREDENTIALS.uaa.xsappname }])
})

OK, assuming the subscription is done, what do we want to achieve with our application?
First, we need a homepage to which the end-user is taken when pressing “Go to Application”.
Furthermore, our app should offer just one capability:
The end-user should be able to “register” as “fan”.
Our app does nothing.
But under the hood, we send some data as message to Event Mesh – which is supposed to forward it to our enterprise system, the soccerclub application.

Our “homepage” is an endpoint that contains just a title – and a link which takes the user to the second endpoint, the /register endpoint:

app.get('/app', function(req, res){      
   const url = `https://${req.hostname}/register?name=JoeCool`   
   res.send(`<h1>Fanshop Homepage</h1>Click <a href="${url}">here</a> to register as Fan.`);
})

The /register endpoint supports one parameter, which is the name of the registered user.
To keep the UI design simple, we just hard-code the fan-name in our code (avoiding an input field).
The /register endpoint does nothing than sending a message to Event Mesh.
The message contains just one property, the fan name, and we take the value from the URL parameter.
The message is structured as JSON, however we need the JSON as a string, to send it in the HTTP request (see below).
One thing we have to note:
We’re a multitenant app, so we need to be tenant-aware.
In our prototype implementation, we extract the tenant info from the URL.
Then we pass the tenant-specific subdomain to our helper method.

app.get('/register', async (req, res) => {
   const hostname = req.hostname // hostname is: consumer-fanshopapp.cfapps.sap.hana.ondemand.com
   const subdomain = hostname.substring(0,hostname.lastIndexOf('-')) // subdomain is consumer

   const msg = `{"name": "${req.query.name}"}`
   const result = await _sendMessage(msg, 'fanshopQueue', CREDENTIALS, subdomain)      
   res.send(`Thank you for your registration as new Fan, '${req.query.name}'. Result: ${result}.`)      
})

Note:
We pass the name of the messaging queue, which we created above.
If you’ve used a different name, you need to adapt it here.
Here we pass only the name suffix, the full queue name (including namespace) will be generated below.

The code for sending messages can be found in some helper functions.

To send the messages, we use the REST API which is provided by Event Mesh.
The API is protected with OAuth
As such, we need to first fetch a valid JWT token.
The credentials for the fetch-token request are handed over to us in the binding of our app.
We access the binding information with the help of the library @sap/xsenv.
To fetch the JWT token, we use the library @sap/xssec.

async function _fetchJwtToken (credentials, subdomain){
   return new Promise ((resolve, reject) => {
       xssec.requests.requestClientCredentialsToken(subdomain, credentials, null, null, (error, token)=>{            
           resolve(token)

The request function of the library is tenant-aware, which means that we can pass the subdomain to get a tenant-specific token.

What does that mean?
We deploy the multitenant app to our provider subaccount.
As a consequence, the URL for fetching the token corresponds to the subdomain of the provider account.
e.g.
https://provider.authentication…com
However, the application is subscribed and used by a customer and “lives” in the subaccount of the customer.
As such, the token request needs to be sent to the URL of the customer.
e.g.
https://customer.authentication…com
That’s the reason why the xssec lib needs the subdomain info, to replace it in the URL.

To send the request to the REST API, we need the URL of the API.
The URL can be found in the binding.
However, the Event Mesh supports 3 different protocols, such that 3 different sections with 3 different URLs can be found.

We need to search for the httprest protocol to get the proper URL.
We take the shortcut (not recommended) of directly accessing the third node (which is hopefully always the httprest):

const uri = msgCredentials.messaging[2].uri

Now, this uri is not enough.
We want to send  a message to a queue.
As such, the final URL will also contain the name of the queue in the URL.
We need to concatenate it.
However one more preparation step is required:
The full queue name contains slashes which need to be encoded (sigh….)
Like this:
soccerclub/scenario/fanshop
==>
soccerclub%2Fscenario%2Ffanshop

The request is a POST request and the event payload is sent in the request body.
Our message is a small JSON object, but it has to be a String, to be passed to the HTTP request.
Below we can see it is a string:

const msg = `{"name": "${req.query.name}"}`

Finally, we can send the request.

const options = {
   method: 'POST',
   body: msg,
   headers: { 
      Authorization: 'Bearer ' + jwtToken,
      'Content-Type': 'application/json',
      'x-qos' : 0 // or 1 
   }
}

const response = await fetch(messagingRestUrl, options) 

The complete code can be found in the Appendix section.

Note:
As usual, the code is meant to be short for good overview, it needs rework obviously.

2.3. Deploy and Test

Before we deploy, we make sure that our Cloud Foundry CLI is still targetting the provider account.
After deploy we can open our application in the provider account to see that it works.
However, this is not the way how it is meant to be used.
We’ve added this route to the manifest, just to be able to test the app in the provider account.
We can open the “homepage”.
But we cannot press the link, as it would cause errors, because we’re not using tenant-specific URL.
So what we want to do is to subscribe to the multitenant app in the consumer account.

3. Run the Scenario

Subscribe
We go to our customer subaccount, open the “Service Marketplace” screen.
We search for our fanshop app, e.g. by typing “fanshop” in the filter.
Once we’ve found it, we can press “Create” in the context menu.

We again press “Create” in the dialog for creating the subscription.
Our callbacks are invoked (e.g. to notify Event Mesh about the subscription) and our fanshop app is added to the list of subscriptions.
All good – we COULD go ahead and “Go to Application” – but we don’t

Create Queue
Before we start using our subscribed app, we need to do one essential configuration step, which is to create the queue, to which we send our events.
We’ve already used the queue name in the code, but we couldn’t create the queue before the subscription of our fanshop app.
So let’s again open the Event Mesh dashboard, in the customer subaccount.
“Instances and Subscriptions” screen, choose our Event Mesh subscription and press on “Go to Application”.
Now we can see the messaging client.
After fanshop-subscription, the messaging client which we created in the provider account, has been  made available in the customer account.

Note:
Have you noticed?
Type is “Subscription”, not “Instance”.

This has been made possible by 2 settings which we did earlier:
We created the event mesh service instance with “reuse”.
And we added the event mesh service in the dependencies callback.

We click on the tile, go to the “Queues” tab and create a queue with name “fanshopQueue”
Note:
The name is just a suffix which is automatically concatenated with the namespace (which we defined in the  service descriptor while creating the service instance)

Test
Optionally, we can now go to the “Test” tab and send a message to the queue and see how the count is incremented

Send
Now we can go to our subscription of our fanshop app and press “Go to Applicaton”.
Our (simple) fanshop homepage is displayed.
Now we click the link to register a hardcoded dummy fan.
We’re taken to the /register endpoint, and under the hood an event has been fired.
In the response, which is displayed in the browser, we can see the name of the registered fan.
In the URL, we can see the hardcoded name-parameter

Check
To verify that the message has really arrived to Event Mesh, we can go to the dashboard, then go to the “Test” tab.
In the “Consume Messages” section, we can select our queue, check the number of messages, “Consume” the messages and view the message payload.

Create Webhook Subscription

Up to now, we’ve successfully verified the connection from the sender (which is a tenant of a multitenant app) to the Event Mesh.
Now we need to connect our receiving soccerclub app to the Event Mesh, to receive and view the events in the fake-enterprise-system app.
What we want to achieve:
Each message that is sent to the queue should be forwarded to the soccerclub app.
With other words, we want to subscribe to the queue (don’t confuse the “subscription” of a multitenant app with the subscription of messaging queue).

So now let’s go to the Event Mesh Dashboard and create a Webhook Subscription.
In the “Webhooks” tab, we press “Create Webhook”.
In the dialog, we configure the settings as described below:

Subscription Name:
We can enter anything of our choice, e.g. “SubscribeToFanshopEvents”.
Webhook URL:
The webhook URL is the one that we mentioned above, in chapter 1.3.
In my example:
https://soccerclubapp.cfapps.sap.hana.ondemand.com/webhook/fanshopevents
Note:
You might need to adapt the URL to match your landscape.
Authentication:
For our simple scenario, we choose “No Auth”.
You may refer to this blog post for secured webhook.

The other settings can be entered according to below screenshot:

After creation, we can open the “Actions” menu and run the “Trigger Handshake”.

Send events
We go to our customer subaccount and open our fanshop application.
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/app
We click on the link to register a dummy fan and trigger an event.
Alternatively, to circumvent the hardcoded fan-name, we can directly call the endpoint with name param in the URL.
E.g.
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/register?name=John

Receive events
As mentioned above, we don’t have any UI, so we need to verify the received event in the Cloud Foundry log.
To do so, we point our Cloud Foundry Command Line Client to the correct org and space of our customer subaccount:
cf t -o myOrg -s mySpace
then we run the command to stream the logs:
cf logs soccerclubapp
Now we can proceed sending more messages.
The result can be seen immediately in the console:

cleanup
At the end of the scenario, to remove all our artifacts, we need to first delete the subscription.
Afterwards, delete the apps and services in the respective subaccount.
1. Unsubscribe
2. Delete artifacts
For your convenience, the commands to clean up our space:

cf d soccerclubapp -f -r
cf d fanshopapp -f -r
cf dsk fanshopXsuaa sk -f
cf ds fanshopXsuaa -f
cf ds fanshopSaasreg -f
cf ds fanshopMsg -f

Troubleshooting

If things don’t work, we need to add error handling to our code.
First of all, we need to check if messages arrive in Event Mesh.
To do so, we can set the webhook subscription to “pause”.
If messages don’t arrive, we can check the response of the Event Mesh REST API in our fanshop app. The above Implementiation just returns the response status code.
To read the response body, we can add the following code to the end of the _sendMessage function:

console.log('=================>' + response.status + JSON.stringify(await response.json()) )

If there’s an error saying that “queue not found”, then the reason might be that the queue was not created in the customer subaccount.

One more hint: make sure to invoke the fanshop app in the customer subaccount, not the provider subaccount. The URL should look like this:
https://consumer-fanshopapp.cfapps.sap.hana.ondemand.com/register?name=test

Summary

In the present tutorial, we’ve created a little scenario, where 2 applications are connected via Event Mesh.
One of the applications is a multitenant app and it sends events to Event Mesh.
The receiver app is connected to Event Mesh via Webhook subscription.
The basic learning is: how to configure Event Mesh to support multitenancy.
The setting:   “instanceType”: “reuse”

Quick Guide

Using Event Mesh in a multitenant app requires the following property in the configuration:

"instanceType": "reuse"

Links

SAP Help Portal:
SAP Event Mesh landing page.
SAP Event Mesh docu about JSON params.
SAP Event Mesh documentation about REST API.
SAP Event Mesh documentation about Entitlements.

Next version of this app, including security, UI and approuter.
Blog post about Webhook Subscription

XSUAA documentation about the xs-security.json parameters.
Node.js module documentation;
https://github.com/bitinn/node-fetch
https://www.npmjs.com/package/@sap/xssec

Appendix 1: Soccerclub Application

manifest.yml

---
applications:
  - name: soccerclubapp
    memory: 64M
    routes:
    - route: soccerclubapp.cfapps.sap.hana.ondemand.com

package.json

{
  "dependencies": {
    "express": "^4.17.1"
  }
}

server.js

const express = require('express')
const app = express();
app.use(express.json())

app.post('/webhook/fanshopevents', (req, res) => {
    console.log(`===> [/webhook/fanshopevents] received message: ${JSON.stringify(req.body)}.`)
    res.status(201).send()
})

app.listen(process.env.PORT, () => {
    console.log('===> Server running.')
})

Appendix 2: Fanshop Application

config-messaging.json

{
  "emname": "fanshopmessagingclient",
  "namespace": "soccerclub/scenario/fanshop",
  "version": "1.1.0",
  "instanceType": "reuse",
  "options": {
      "management": true,
      "messagingrest": true,
      "messaging": true
  },
  "rules": {
      "queueRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      },
      "topicRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      }
  }
}

config-saasreg.json

{
  "appId": "fanshopxsappname!t14860",
  "appName": "fanshopSaasregAppname",
  "appUrls": {
    "getDependencies" : "https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/dependencies",
    "onSubscription" : "https://fanshopapp.cfapps.sap.hana.ondemand.com/mtcallback/{tenantId}"
  },
  "displayName": "Fanshop MT app (based on XSUAA, SaaSreg, Enterprise-Messaging)"
}

config-security.json

{
    "xsappname": "fanshopxsappname",
    "tenant-mode": "shared"
}

manifest.yml

---
applications:
  - name: fanshopapp
    memory: 512M
    routes:
    - route: fanshopapp.cfapps.sap.hana.ondemand.com
    - route: AsincConsumer-fanshopapp.cfapps.sap.hana.ondemand.com
    services:
      - fanshopMsg
      - fanshopXsuaa
      - fanshopSaasreg

package.json

{
    "dependencies": {
        "@sap/xsenv": "latest",
        "@sap/xssec": "latest",
        "express": "^4.16.2",
        "node-fetch": "2.6.2"
    }
}

server.js

const xsenv = require('@sap/xsenv')
const fetch = require('node-fetch')
const express = require('express')
const app = express()
const xssec = require('@sap/xssec')
app.use(express.json())

const CREDENTIALS = xsenv.getServices({ myMessaging: { tag: 'enterprise-messaging'} }).myMessaging


/* App server */
app.listen(process.env.PORT, () => {})

/* App endpoints */

app.get('/app', function(req, res){      
   const url = `https://${req.hostname}/register?name=JoeCool`   
   res.send(`<h1>Fanshop Homepage</h1>Click <a href="${url}">here</a> to register as Fan.`)
})

app.get('/register', async (req, res) => {
   const hostname = req.hostname // hostname is: consumer-fanshopapp.cfapps.sap.hana.ondemand.com
   const subdomain = hostname.substring(0,hostname.lastIndexOf('-')) // subdomain is consumer

   const msg = `{"name": "${req.query.name}"}`
   const result = await _sendMessage(msg, 'fanshopQueue', CREDENTIALS, subdomain)      
   res.send(`Thank you for your registration as new Fan, '${req.query.name}'. Result: ${result}.`)      
})

/* Multi Tenancy callbacks */

app.put('/mtcallback/:tenant_id', (req, res) => {
   const appHost = req.hostname  
   const subDomain = req.body.subscribedSubdomain
   res.status(200).send(`https://${subDomain}-${appHost}/app`)
})

app.delete('/mtcallback/:tenant_id', (req, res) => {
   res.status(200).end('unsubscribed')
})

app.get('/mtcallback/dependencies', (req, res) => {
   res.status(200).json([{'xsappname': CREDENTIALS.uaa.xsappname }])
})


/* HELPER */

async function _fetchJwtToken (credentials, subdomain){
   return new Promise ((resolve, reject) => {
       xssec.requests.requestClientCredentialsToken(subdomain, credentials, null, null, (error, token)=>{            
           resolve(token)
       })
   })  
}

function _composeMsgRestUrlForSendMsg (msgCredentials, queueNamePostfix){
   const slash = '%2F'
   const namespace = msgCredentials.namespace
   const namespaceEncoded = namespace.replace(/\//g, slash) 
   const fullQueue = namespaceEncoded + slash + queueNamePostfix
   const uri = msgCredentials.messaging[2].uri 
   return `${uri}/messagingrest/v1/queues/${fullQueue}/messages` 
}

async function _sendMessage(msg, queueNamePostfix, msgCredentials, subdomain){
   const uaa = msgCredentials.uaa   
   const jwtToken = await _fetchJwtToken(uaa, subdomain) // call subscriber account to fetch token
   const messagingRestUrl = _composeMsgRestUrlForSendMsg(msgCredentials, queueNamePostfix)
   const options = {
      method: 'POST',
      body: msg,
      headers: { 
          Authorization: 'Bearer ' + jwtToken,
          'Content-Type': 'application/json',
          'x-qos' : 0 // or 1 
      }
   }

   const response = await fetch(messagingRestUrl, options) 
   return response.status
}

Assigned Tags

      17 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Srinivas Rao
      Srinivas Rao

      Hi Carlos Roggan  - I read through the entire blog post. Multi-tenancy is an interesting topic and you have crafted the post with lot of details. Since I am still in the learning phase, I am bit confused on some points, and hence the below questions. I look forward to your reply on them.

      1. In section 1, you basically only create the subscription of the event mesh in the customer account. This means that resource consumption or metering of it has still not begun because this subaccount does not have the service instance. Is this correct understanding?
      2. From the billing perspective, only the provider account will be billed by SAP, and the provider will in turn bill the customer, isn't it ? ( I am asking this because, customer account atleast has subscription to event mesh. Isn't there any charges for it to be borne by customer directly to SAP )
      3. In webhook subscription, you mention as "trigger handshake", but the screenshot talks about "Exempt handshake". Nevertheless, could you please explain how this "handshake" is different in multi-tenancy world?
      4. When you say "In our case: in fact, we’re using the Event Mesh service and it is tenant-aware and it needs to be notified.", what actually happens and what is exactly notified to the Event Mesh of the provider subaccount?

        Does "notifying" mean some functionality to enable "message client" in the customer subaccount? If yes, then what it means to return the xsappname and to whom are we returning it, technically ?

      Thanks & Regards,

      Srinivas Rao.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Srinivas Rao

      Thanks for your patience to read it all. You don't sound like learning phase, but rather expert phase;-)
      I'm trying to answer as much as I can:

      1. See 2

      2. I’m not familiar with pricing, I assume that you’re right. Whoever creates the MT app, has to pay for the resource consumption, and if it is a partner company, then this one will have its own contracts with its customers, I’m not aware of any details. It is anyways just an example and customer can be building his own extensions in the cloud. On the other side, the customer in our example has an onPrem installation of a SAP system and wants to use Cloud for extending. The customer would need to buy license for cloud usage, which is always cheaper than installing another system, and faster than waiting for it. Again, I’m not aware of details, but I assume that using cloud-offerings on subscription basis (like EM) would be somehow charged.

      3. I proposed to manually “trigger” a handshake to see if the webhook subscription is ok. This has nothing to do with multitenancy
      On the receiving side, we have a singletenant app.
      We can see it in the URL: it doesn’t contain tenant-info
      In our example, the onPrem system doesn’t care, if the incoming message was sent by a MT or ST app.
      At the end, the information which is useful for the ST side, is contained in the message payload, and it is about the end-user, the fan.
      At the end, if developer decides to design the fanshop app as MT or ST, is not realized by the fan, nor by the ST.
      See next point.

      4. This sounds like a general MT question.
      So when we’re developing a MT app, we’re doing that way to benefit from the few advantages, which this design offers:
      Less resource consumption, because MT app is deployed only once, instead of deploying every singletenant app separately.
      This brings also the advantage of less maintenance effort, so only one app needs to be updated in case of fixes, etc
      If our MT app uses a database, then we don’t need one DB per customer, but we put all data of all customers in just 1 DB.
      Which means we need to distinguish the data by each customer.
      That’s why we need to get information about the customers who use our app.
      Each customer has one ID and we distinguish the customer data by that ID.
      (I’m of course simplifying things)
      And this is the reason why we have the callbacks.
      The multitenancy-framework, i.e. the SaaS Registry notifies us when a subscription takes place.
      In the callback we get the ID of the customer (tenant ID)
      With this ID, we open a new DB schema or whatever, in the code of the callback.
      This implies, whenever the end-user does any interaction in our app, then we need the ID, so we know how to access the data for this specific subscription (customer)
      We will get this ID in the JWT token after end-user logs in.

      OK, now coming to your question.
      Let’s imagine, our MT app is split into 2 MT apps. Then the second app needs to know about the subscription as well.
      Whenever we are notified by the SaaS registry, then the second app, on which we depend, needs to be notified as well.
      Both apps need to be multitenant-aware and registered in the SaaS Registry

      Which leads us to your question.
      The registration is done in the configuration of the service instance of SaaS Registry.
      In the configuration file, we have to specify the xsappname, this is how the SaaS Registry works.
      That’s why we need to send the xsappname (of event mesh) to the SaaS registry in the getDependencies callback.
      Like this, the SaaS registry will find the Event Mesh instance and will call the subscription callback of event mesh
      And then event mesh can display the messaging client in the dashboard
      Yes, you were right

      Cheers,
      Carlos

      Author's profile photo Srinivas Rao
      Srinivas Rao

      Thank you so much Carlos Roggan for taking out time to respond to the questions in detail. Your blog posts are real gold mines. I have been benefiting a lot from them. Thanks for contributing such rich content to the community 🙂

       

      Thanks & Regards

      Srinivas Rao.

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Woww - thank you sooo much for this encouraging comment 😉
      Will try to continue and your feedback helps in doing so !
      Cheers,
      Carlos

      Author's profile photo Gopal Anand
      Gopal Anand

      Hi Carlos,

      Thanks alot for this in-depth tutorial. I was trying to figure out the implementation of eventing in multitenancy and finally it's here.

      I have some queries,

      1. How will this work in a push scenario where the S/4HANA will be triggering event. Normally we need service keys to register event mesh in S/4HANA. Will we enable cloud foundry in the subscriber subaccount and create servise keys? Or some other approach can be used here?

      2. Does event mesh now officially supports multitenancy?

      Best regards,

      Gopal

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Gopal Anand , thanks a lot for the feedback - really good to know that there's anybody out there to whom this post is useful 😉
      Please contact me via pm to discuss your open questions.
      Kind Regards,
      Carlos

      Author's profile photo Fouad Sebbane
      Fouad Sebbane

      Hello Carlos,

      I was relieved that everything worked in the end. The simplified scenario gives a very good overall picture. I am very thankful to you for that!
      Here are a few pointers that might be of use for the next Volonters 🙂 :
      I noticed at some point that in trial-account we don't have the 'default plan'. Unfortunately, it didn't work with 'dev plan'. I luckily have access to a non-trial system. Then I realized that the 'namespace' must be registered. I misused an already registered namespace that we use for demo purposes.

      Creating or deleting a subscription sometimes fails. Troubleshooting is quite difficult. I still need to figure out how to debug the subscription.

      Many thanks again.

      Fouad

       

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hello Fouad Sebbane ,
      Thank you very much for the feedback and for the hints!
      This is a very good idea!
      I've also added a few notes in the text above.
      Kind Regards,
      Carlos

      Author's profile photo Florian Kekule
      Florian Kekule

      Hi Carlos,

       

      thanks a lot for this great blog post. I am currently trying to create the Event Mesh instance on provider account and I am getting following error with creation:

      Service broker error: Service broker enterprise-messaging-service-broker failed with: Service broker parameters are invalid: The given namespace is not reserved for the global account or tenant id

      Did you enounter similar issues or do you have an idea what could cause this issue?

      Thanks a lot and kind regards,

      Flo

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Hi Florian Kekule ,

      The reason is mentioned above in a little note.
      In productive scenarios, the namespace has to be reserved.
      To do so, you need to open a ticket on Event Mesh and ask to register your desired namespace.

      Kind Regards,
      Carlos

      Author's profile photo Florian Kekule
      Florian Kekule

      Thanks a lot for your fast and helpful response 🙂

      Author's profile photo Pintu B
      Pintu B

      Thanks Carlos Roggan for great post. It clarified some concepts like multitenancy, tenant subscription as well

      Author's profile photo Carlos Roggan
      Carlos Roggan
      Blog Post Author

      Thanks for your feedback, Pintu B and I'm glad that this writing was helpful.
      Kind Regards,

      Carlos

      Author's profile photo Milton Chandradas
      Milton Chandradas

      I am running into this error when trying to create a multi-tenant aware Event Mesh service instance...

      If I remove line 5 ("instanceType": "reuse"), then it works fine

      Service broker error: Service broker enterprise-messaging-service-broker failed with: Service broker parameters are invalid: The namespace sap/team3601/planandseat is not reserved for the global account id 724f2b9d-5057-43b4-9b47-2b4702833c2e or tenant id 760dddf4-c960-4448-ab85-205b268c5e4c
      Your blog mentions the following...  Can you elaborate on this.  Do you think this is causing the error ?
      Note:
      New “namespace” must be registered
      Event%20Mesh%20service%20instance
      Author's profile photo Gopal Anand
      Gopal Anand

      Hi @Milton Chandradas

      This plan is not available for external usage. If you are SAP Developer there's a way to use this plan.  Kindly check the Event mesh internal documentation.

       

      Best regards, Gopal

      Author's profile photo Milton Chandradas
      Milton Chandradas

      Not sure what you mean by - This plan is not available for external usage

      This is not a trial account.  This is a paid account if that's what you are referring to...

      And if I remove line 5 ("instanceType": "reuse"), then it works.  So the default plan is available...

      Author's profile photo Fiona Wang
      Fiona Wang

      Hi Gopal,

      Kindly asking where to find the Event Mesh internal documentation.

      Also where can we get to know the latest status of this feature such as availability for external use.

      Many thanks

      BR