Tenant Options
Tenant options are runtime settings for your microservice. They are a good fit for values that differ between tenants or should be changed without rebuilding the service.
Define Options in the Manifest
Add settings to c8y.manifest.settings:
export default defineNitroConfig({
c8y: {
manifest: {
settingsCategory: 'my-service',
// ROLE_OPTION_MANAGEMENT_READ is auto-added; add ROLE_OPTION_MANAGEMENT_ADMIN yourself to write/delete.
requiredRoles: ['ROLE_OPTION_MANAGEMENT_ADMIN'],
settings: [
{ key: 'featureMode', defaultValue: 'standard', editable: true },
{ key: 'credentials.apiKey', defaultValue: 'change-me' },
],
},
},
modules: [c8y()],
})Each setting must have a non-empty defaultValue. Empty defaults are rejected while generating the manifest so bad settings fail early.
Roles: When
settingsare defined,ROLE_OPTION_MANAGEMENT_READis added to the manifest automatically so the service can read its options. Writing or deleting options requiresROLE_OPTION_MANAGEMENT_ADMIN, which you must add torequiredRolesyourself.
Which Tenant?
Tenant options live per tenant. A multi-tenant microservice must be explicit about which tenant it reads from or writes to, so the API takes a Cumulocity Client as its first argument — the client carries both the target tenant and its service-user credentials.
Get a client from one of the client helpers:
useDeployedTenantClient()— the microservice's owner/deployed tenant (its own configuration).useUserTenantClient(event)— the tenant of the current request's user (per-subscriber configuration).useSubscribedTenantClients()— every subscribed tenant.
Read and Write at Runtime
useTenantOption(client, key) returns a handle for a single option; useTenantOptions(client) returns a handle for a whole category.
import {
useTenantOption,
useTenantOptions,
useDeployedTenantClient,
useUserTenantClient,
} from 'c8y-nitro/utils'
// The microservice's own config (owner tenant):
const client = await useDeployedTenantClient()
const featureMode = await useTenantOption(client, 'featureMode').read()
const apiKey = await useTenantOption(client, 'credentials.apiKey').read()
// A subscriber's config (current request's tenant):
const tenantClient = await useUserTenantClient(event)
await useTenantOption(tenantClient, 'featureMode').set('advanced')Each handle exposes:
| Method | Description |
|---|---|
read() | Current value, or undefined if unset (404) |
set(value) | Create or update (upsert) |
getOrInsert(fallback) | Read; if unset, write fallback and return it |
delete() | Remove the option (idempotent) |
setEditable(editable, { targetTenant }) | Update the editable flag — management tenant only |
refresh() / invalidate() | Cache control for this option |
Keys defined in the manifest are generated into the C8YTenantOptionKey type, so TypeScript autocompletes them — but any other string is accepted too, so dynamic keys work (see below).
Dynamic Keys
You are not limited to manifest-declared keys. Any string works, which is useful when the key is computed at runtime (for example a per-record encrypted secret):
const client = await useDeployedTenantClient()
const secret = await useTenantOption(client, `encrypted.password.${hash}`).getOrInsert('')Whole Categories
const options = useTenantOptions(client)
const all = await options.list() // { key: value, ... }
await options.setAll({ featureMode: 'x', ttl: '30' })
await options.option('featureMode').read()Other Categories
By default the category is your microservice's own (resolved below). Pass a different category to read or write options that live elsewhere:
// Whole category:
const all = await useTenantOptions(client, 'other-service').list()
// A single option in a foreign category (via the category handle):
const value = await useTenantOptions(client, 'other-service').option('someKey').read()
// …with key autocomplete:
await useTenantOptions<'someKey'>(client, 'other-service').option('someKey').set('x')useTenantOption(client, key) is only a shortcut for the own category — it has no category parameter. Use useTenantOptions(client, category).option(key) for a single option elsewhere.
credentials.* keys are rejected at compile time for foreign categories, because encrypted options can only be decrypted within their owning microservice's category. Pass the key set as a type argument to get autocomplete for a foreign category's keys:
await useTenantOptions<'featureA' | 'featureB'>(client, 'other-service').option('featureA').read()Category Resolution
The option category is resolved in this order:
manifest.settingsCategory- generated manifest
contextPath - generated manifest
name
Use an explicit settingsCategory when you want stable option storage independent from package naming or context-path changes.
Missing Options
If Cumulocity returns 404 for an option, read() returns undefined. Other failures (e.g. a missing ROLE_OPTION_MANAGEMENT_* role) are thrown as a generic 500 — the specifics (operation, upstream status, required role) are logged via the error's internal field and never included in the HTTP response, so nothing about tenant options leaks to the caller.
Encrypted Options
Keys prefixed with credentials. are stored encrypted by Cumulocity. Use this for secrets that tenants configure through platform options. They can only be read within the microservice's own category (never a foreign category).
Cache Control
Reads are cached for 10 minutes by default, keyed per tenant (${tenant}::${category}::${key}) so different tenants never share a cached value. You can tune the default or override individual keys:
export default defineNitroConfig({
c8y: {
cache: {
defaultTenantOptionsTTL: 600,
tenantOptions: {
'featureMode': 60,
'credentials.apiKey': 300,
},
},
},
modules: [c8y()],
})Writes (set, getOrInsert, delete, setAll) invalidate the affected cache entries automatically. You can also manage caches manually at three levels:
// one option:
await useTenantOption(client, 'featureMode').invalidate()
const fresh = await useTenantOption(client, 'featureMode').refresh()
// one tenant + category:
await useTenantOptions(client).invalidateAll()
const values = await useTenantOptions(client).refreshAll()
// everything, across all tenants and categories:
await useTenantOptions.invalidateAll()
await useTenantOptions.refreshAll()invalidateAll() and refreshAll() only operate on keys that have already been accessed in the current process.
Debug Invalidation Route
Set enableTenantOptionsInvalidationRoute: true to expose:
GET /_c8y_nitro/invalidate-tenant-options?key=featureMode
GET /_c8y_nitro/invalidate-tenant-options?all=trueThis is meant for debugging and operational troubleshooting. Keep it disabled unless you have a clear reason to expose it.