Reading settings

c.settings() inside your function — the market → tenant → default → declared-fallback resolution order, the public settings endpoint, caching, and failing open.

Inside your function, reading a setting is one call, already resolved for the request's tenant and market:

src/main.js
app.post('/serials', async (c) => {
  const s = await c.settings();
  return c.json(await register(c.body, { warrantyMonths: s.warranty_months }));
});

c.settings() is async and returns the whole resolved set, keyed exactly as settings.json declares. You do not name the tenant and you do not name the market: both come from the request context.

Resolution order

The gateway resolves each key in order, and stops at the first that has a value:

text
1) the market override, if the request names a market and one is set
2) the tenant value
3) the `default` in settings.json
4) the offline fallback your app declared in createApp({ settings: { defaults } })

So a market-scoped key falls back to the tenant value when that market has not set one, and a key nobody has ever touched resolves to the default you shipped. Every step is the platform's work; nothing in your handler has to know which step answered.

That is why giving every setting a default matters: it is what makes step 3 exist, and therefore what makes a fresh install work before a merchant opens the form.

The declared fallback

Step 4 is yours, and it is the one people skip:

src/main.js
const app = createApp({
  name: 'serials',
  settings: { defaults: { warranty_months: 24, registry_locked: false } },
});

It is the value used when the settings service cannot be reached at all. Declare it for every key your handlers read, and keep it in step with the default in settings.json — the two disagreeing is a bug that only appears during an outage, which is the worst time to find it.

Failing open

Reads are cached per tenant, app and market, and they fail open: a settings outage falls back to your declared defaults rather than taking the app down.

Two consequences worth designing for.

Your app keeps serving during a settings outage, with your defaults. That is almost always the right trade for an admin setting — a warranty period reverting to 24 months for a few minutes beats a 502 on every request.

It is the wrong trade for a setting that gates something dangerous. A registry_locked that fails open to false unlocks a registry the merchant deliberately locked. So for anything whose safe state is "no":

  • Make the safe value the default and the declared fallback. registry_locked defaults to true if being unlocked when you cannot tell is unacceptable.
  • Or invert the key so that the fallback is the cautious one — allow_registration defaulting to false reads better than a lock defaulting to open.

Never write a setting whose fallback silently grants something.

Caching

The resolved set is cached per tenant, app and market. In practice that means:

  • Call c.settings() freely. It is not a network round trip on every invocation.
  • Call it once per handler and pass the value down, rather than awaiting it in a loop — the cache makes that cheap, not free.
  • A change a merchant makes takes effect within the cache window, not instantly. If an operator says "I changed the setting and nothing happened", that is the first thing to check, and the second is whether they set it on the market they are testing with.

Do not build your own settings cache on top of it, and do not read settings once at module scope: your function instance may serve more than one tenant over its life, and a value captured at startup belongs to whichever tenant happened to arrive first. Read inside the handler, every time.

Reading them over the API

The resolved settings are also available on the public gateway, which is what the Cockpit form and the CLI use:

Request
curl "https://api.revenexx.com/v1/settings/apps/serials?market=de" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..."
CLI
revenexx settings get-app-settings --app serials --market de

Omit market and you get the tenant-level resolution. This is the fastest way to answer "what does this tenant actually have configured?" while you are debugging, and it is the same resolution your function sees.

Practical guidance

Read settings, do not copy them. A setting mirrored into one of your own columns is a value that will go stale and a second place to change.

Guard the value you read. validation in settings.json covers the form, but a default nobody has overwritten was never validated, and the value can also arrive through the API. A number you divide by deserves a check.

Do not read a setting on a hot path you could resolve once. In a batch handler, read once at the top and use the value for every item.

Never log a sensitive setting. Execution logs are readable in App Studio. See Settings.

Next steps

  • Settings — declaring the keys you are reading.
  • Scoping — where the market in step 1 comes from.
  • The router — the rest of the request context.
  • Vocabularies — resolving a merchant-extended value's label.
Was this page helpful?