Settings

The settings.json reference — what a merchant can configure, tenant versus market scope, choice values from enum_values or a vocabulary, sensitive values, grouping and validation.

settings.json declares what a merchant can configure about your app. The Cockpit renders the form, the platform stores the values and resolves each one per market, and your function reads the resolved value with one call. You do not build a settings screen and you do not store the values yourself.

The authoritative contract is at https://schemas.revenexx.com/settings.schema.json. Point your editor at it with $schema — it validates as you type, and it is the always-current list of what each type accepts.

settings.json
{
  "$schema": "https://schemas.revenexx.com/settings.schema.json",
  "settings": {
    "warranty_months": {
      "title": { "en": "Warranty period (months)", "de": "Garantiezeit (Monate)" },
      "description": { "en": "How long a registered serial stays under warranty." },
      "type": "number",
      "scope": "market",
      "default": 24,
      "group": "warranty",
      "validation": { "min": 0, "max": 120 }
    },
    "registry_locked": {
      "title": { "en": "Lock the serial registry" },
      "description": { "en": "When locked, no new serials can be registered." },
      "type": "boolean",
      "scope": "tenant",
      "default": false,
      "group": "general"
    }
  }
}

The top-level key is settings, a map of key to definition. Keys are snake_case, and they are what your function reads by name — so treat a key as a published identifier, not a label you can tidy up later.

The definition

FieldRequiredDescription
titleYesThe form label. String or locale map.
typeYesWhich input the Cockpit renders and what the stored value is. One of five — see below.
descriptionNoHelp text under the field. Written for a merchant, not for you.
scopeNotenant (default) or market. See Scope.
defaultNoThe value used when nothing has been set.
groupNoGroups related settings into one section of the form.
sensitiveNoMarks the value as a secret. See Sensitive values.
enum_valuesNoA fixed list of allowed values for a choice-typed setting.
vocabularyNoA merchant-extensible list of allowed values, instead of a fixed one.
validationNoBounds and patterns the Cockpit enforces before saving.

type

type decides the input the Cockpit renders and the shape of the stored value. There are five, covering a text value, a number, a boolean, a single choice and a set of choices — "number" and "boolean" appear in the example above.

The exact spelling of each is in settings.schema.json, and that is the copy to trust: point your editor at the schema and it will both complete and validate the value. It is a small enough file that reading it once is faster than looking a name up twice.

Pick the narrowest type that expresses the setting. A number with validation bounds is better than free text a merchant can put "24 months" into; a choice is better than a number whose only valid values are three of them.

Scope: tenant or market

ScopeBehaviour
tenantOne value for the whole tenant.
marketConfigurable per market, falling back to the tenant value when unset.
settings.json
"warranty_months": { "title": { "en": "Warranty period (months)" }, "type": "number", "scope": "market", "default": 24 }

A market-scoped setting needs the markets app as a dependency:

manifest.json
"dependencies": { "revenexx/markets": "^0.1" }

Choose market when the business answer legitimately differs by segment — a warranty period, a rounding rule, a legal threshold. Choose tenant for anything operational or global: a feature switch, an external endpoint, a lock.

Do not reach for market "just in case". A per-market setting is a per-market decision a merchant now has to make, multiplied by the number of markets they run, and a tenant-scoped setting can be widened to market scope later without breaking anything. See Scoping.

enum_values versus vocabulary

Both constrain a choice-typed setting to a list. The difference is who owns the list.

enum_values is your list. A fixed set, defined in the file, changeable only by shipping a new version of your app.

settings.json
"rounding": {
  "title": { "en": "Price rounding" },
  "type": "select",
  "scope": "market",
  "enum_values": ["up", "down", "nearest"],
  "default": "nearest"
}

vocabulary is the merchant's list. It names a vocabulary your app publishes, which the merchant can extend at runtime. A value they add appears in the form without you doing anything.

settings.json
"default_condition": {
  "title": { "en": "Default device condition" },
  "type": "select",
  "scope": "tenant",
  "vocabulary": "device_conditions"
}

The choice between them is a design decision, not a style one:

UseWhen
enum_valuesYour code branches on the value. Three rounding modes are three code paths, and a fourth one a merchant invents cannot work.
vocabularyThe value is data your code passes through — a label, a category, a classification. A merchant knowing their business better than you do is the normal case.

The test is: would a value you have never seen break your handler? If yes, enum_values. If it would just flow through, vocabulary. See Vocabularies.

Sensitive values

settings.json
"erp_api_key": {
  "title": { "en": "ERP API key" },
  "description": { "en": "Issued in your ERP's integration settings." },
  "type": "string",
  "scope": "tenant",
  "sensitive": true
}

sensitive: true marks the value as a secret: the Cockpit masks it in the form, and it is not something to display back, log, or put in an event payload.

This is the practical way to hold a credential for an outbound call today, because the secret grant is not yet proven. Two rules that go with it:

  • Never log a sensitive value, and never include one in a response, a trace attribute or an error message. c.log() output is readable in App Studio.
  • Never put a credential in the app directory instead. The whole directory is uploaded on deploy and the manifest is published; a settings key that the merchant fills in is per tenant and stays out of your repository.

group

settings.json
"group": "warranty"

Groups related settings into one section of the rendered form. Settings sharing a group cluster together; ungrouped settings land in the default section.

Use it as soon as you have more than about five settings. A form of fifteen ungrouped switches is a form nobody reads, and grouping is the cheapest documentation you can give a merchant — a section called "Warranty" tells them which three fields belong to one decision.

validation

settings.json
"validation": { "min": 0, "max": 120 }

Bounds and patterns the Cockpit enforces before it saves, so a bad value is a form error rather than a runtime surprise. Numeric bounds (min, max) and a string pattern are the ones you will reach for; the schema has the full set.

Validate here and guard in your handler. The form is not the only writer — the value can also arrive through the settings API — and a default that has never been overwritten is not a validated value either.

Practical guidance

A setting is a question you are asking a merchant. Every one has a cost: somebody has to decide, and somebody has to maintain the decision. Prefer a sensible default and no setting to a setting with a sensible default.

Never make a setting the answer to something you can derive. If your code can work it out from the data, work it out.

Give every setting a default, so a fresh install works before anyone opens the form. That is also what makes seeding defaults and the offline fallback coherent.

Write title and description in the merchant's language, in both senses. "How long a registered serial stays under warranty" is a sentence about their business; "warranty_months (int)" is a sentence about your schema.

Do not rename a key. Your function reads it by name, and so does anything reading settings through the API. Add the new key, migrate, and retire the old one — the same discipline as a column rename.

Next steps

Was this page helpful?