76 · Multitenancy Basics

Configure cds-mtxs, understand per-tenant HDI containers, req.tenant routing, and the subscribe/unsubscribe lifecycle.

Episode 76 — Multitenancy Basics

What you'll learn

Why this matters

If you build an application for multiple customer organisations that need to share the same deployment but keep their data strictly isolated, you need multitenancy. CAP's @sap/cds-mtxs (MTX Sidecar) handles the hard parts: per-tenant HANA HDI containers, automatic tenant routing, and a subscription API that BTP's SaaS Registry calls during onboarding. Without this, you would have to implement tenant routing, schema provisioning, and isolation yourself.

Tenant isolation model in CAP

Aspect How CAP handles it
Data Each tenant gets its own HANA HDI container
Authentication JWT zid (zone ID) claim identifies the tenant
Routing req.tenant is set automatically from the JWT
Schema upgrades mtxs applies schema migrations to all tenant DBs
Onboarding SaaS Registry calls your /mtx/v1/provisioning/tenant endpoint

Setting up @sap/cds-mtxs

npm install @sap/cds-mtxs
// package.json — enable the plugin
{
  "cds": {
    "requires": {
      "multitenancy": true,
      "[production]": {
        "auth": { "kind": "xsuaa" },
        "db": { "kind": "hana" }
      }
    }
  }
}

@sap/cds-mtxs automatically mounts a /mtx/v1 API for provisioning, model sync, and extensibility.

XSUAA configuration for multitenancy

// xs-security.json
{
  "xsappname": "my-cap-saas",
  "tenant-mode": "shared",          // CRITICAL: must be "shared" not "dedicated"
  "scopes": [
    { "name": "$XSAPPNAME.user",        "description": "Application user" },
    { "name": "$XSAPPNAME.admin",       "description": "Admin" },
    { "name": "$XSAPPNAME.mtcallback",  "description": "SaaS Registry callback" }
  ],
  "oauth2-configuration": {
    "token-validity": 43200,
    "refresh-token-validity": 2592000
  }
}

"tenant-mode": "shared" is essential — it enables the JWT zid claim that CAP uses to identify tenants.

How req.tenant works

CAP reads the zid (zone ID) from the JWT and sets req.tenant automatically:

this.before('*', req => {
  console.log('Tenant:', req.tenant);   // e.g. 'abc-customer-guid'

  // CQL queries are automatically scoped to the tenant's HDI container
  // No WHERE clause needed — the DB connection is tenant-specific
});

You never have to add WHERE tenant = ? to your queries. The HDI container binding is resolved per tenant before the handler runs.

Tenant onboarding (subscribe)

When a customer subscribes in BTP, the SaaS Registry calls:

PUT /mtx/v1/provisioning/tenant/{tenantId}

@sap/cds-mtxs handles this endpoint. It:
1. Creates a new HANA HDI container for the tenant
2. Deploys the current CDS schema to that container
3. Returns 200 OK when done

You can hook into the onboarding event:

// srv/provisioning.js
const cds = require('@sap/cds');

cds.on('served', async () => {
  const { 'cds.xt.SaasProvisioningService': provisioning } = cds.services;

  provisioning.on('subscribe', async ({ tenant, options }) => {
    console.log(`Tenant ${tenant} subscribed`);

    // Seed initial data for the new tenant
    const db = await cds.connect.to('db', { tenant });
    await db.insert(cds.entities.Settings).entries({
      key: 'welcomeMessage', value: 'Welcome to CodeGoLive!'
    });
  });

  provisioning.on('unsubscribe', async ({ tenant }) => {
    console.log(`Tenant ${tenant} unsubscribed — cleaning up`);
    // Optionally export or archive data before the HDI container is deleted
  });
});

Testing multitenancy locally

// .cdsrc.json — mock tenant setup
{
  "requires": {
    "auth": {
      "kind": "mocked",
      "users": {
        "alice": { "tenant": "t1", "roles": ["admin"] },
        "bob":   { "tenant": "t2", "roles": ["user"] }
      }
    },
    "multitenancy": true
  }
}
cds watch
# alice (tenant t1) and bob (tenant t2) see completely isolated data

In-memory SQLite databases are used per tenant in local mock mode — no real HDI containers needed for development.

Common mistakes

Mistake Fix
"tenant-mode": "dedicated" in xs-security.json Must be "shared" for multitenancy to work
Hardcoding tenant ID in queries Never — use req.tenant and let CAP route to the correct DB
Not implementing the subscribe hook Default onboarding works, but you miss seeding initial data
Sharing a single HANA service instance Each tenant needs its own HDI container — use hana/hdi-shared
Forgetting schema migrations apply to all tenants cds-mtxs handles this, but unsafe schema changes affect all tenants simultaneously

Checkpoint ✓

You understand the CAP multitenancy model — per-tenant HDI containers, req.tenant from the JWT zid claim, and the subscribe/unsubscribe lifecycle. You can configure @sap/cds-mtxs, set "tenant-mode": "shared" in XSUAA, hook into onboarding to seed initial data, and test tenant isolation locally with mocked users.

Sign in for progress tracking & Q&A →