Skip to main content

Integrations & capability bindings

An integration is an app-level feature — the geocoding orchestrator, the routing orchestrator, a transit provider, a map overlay, an external data source, photos, reviews, place enrichment. Each one ships a manifest.json that declares its config schema, the secrets it needs, and the backend services it requires. The admin panel under /admin/integrations is where you turn those declarations into a working configuration: enable the features you want, fill in their settings and credentials, and — when a feature needs a backend that more than one service can supply — pick which one it uses.

If the service/integration/capability model is new to you, read How it works first. This page assumes it and focuses on the admin side. The panel and the API behind it are mapped in the Admin panel tour; installing third-party integrations is covered in Community extensions.

The integration list

/admin/integrations lists every loaded integration with its display name, the domains it serves, whether it's enabled, its health, and whether its credentials and required services are satisfied. Click into one to open its detail page, which is organized into tabs:

  • Overview — metadata, the services and other integrations it depends on, and the capability-binding picker.
  • Config — the per-integration settings form, plus a read-only table showing every resolved value and where it came from.
  • Credentials — the secret fields the integration declares, and their status.
  • Health — the current probe result, with a button to re-run it.

Two controls sit outside the tabs. An enable / disable toggle flips whether the integration loads at all, and a Reload action re-runs the integration's setup so a config or binding change takes effect. Reload is queued as a background job — watch it finish under the panel's Activity view (Monitoring).

Configuring an integration

If an integration's manifest declares a configSchema, the Config tab renders it as a form: one input per setting, typed from the schema — toggles for booleans, dropdowns for enumerations, text and number inputs otherwise. An integration with no schema (or one that exposes nothing beyond the on/off switch) shows a note instead.

Saving writes your edits to the database and reloads the integration so the new values apply right away — there's no separate restart step for integration config, unlike backend services. The form rejects unknown keys and type mismatches against the schema, and it refuses two kinds of field: secret fields (those go through the Credentials tab) and the enabled switch (use the enable / disable toggle). Every change is written to the audit log.

The config cascade

Each setting's effective value is resolved through a layered cascade. The Config tab shows a source chip next to every field naming the layer that won, and a field pinned by an environment variable is rendered disabled — you can see the value but not edit it, because the environment would override your edit anyway.

There are five layers. Highest priority wins:

PrioritySourceWhere it comes from
5envA host environment variable, INTEGRATION_<ID>_<KEY>.
4config.jsonA config.json file in the integration's own directory.
3vaultAn admin-set secret in the encrypted vault.
2databaseA value you saved in the Config form.
1defaultThe configSchema default — nothing has overridden it.

So an environment variable beats a local config.json, which beats a vaulted secret, which beats a value saved in the admin panel, which beats the schema default. Environment variables always win, which keeps a Docker deployment predictable: a value pinned in infra/docker/.env can't be silently changed from the UI, and the disabled field plus its env chip make that visible.

Why a saved value sometimes "won't stick"

If you save a field in the form and it doesn't change, look at its source chip. A higher layer — almost always an env override in infra/docker/.env — is taking precedence. Remove that override (or change it there) to let the admin value through. The same logic governs every layer of the cascade.

The environment-variable name follows a fixed pattern: INTEGRATION_, then the integration's id uppercased with hyphens turned into underscores, then _, then the config key uppercased. The key match is case-insensitive, so a camelCase schema key like apiKey is set by INTEGRATION_<ID>_APIKEY:

# infra/docker/.env — overrides the `apiKey` setting of the `ev-charging` integration
INTEGRATION_EV_CHARGING_APIKEY=your-key-here

For the bigger picture of where settings live — .env, the admin panel, and how they relate — see Configuration. The parallel cascade for backend services uses the SERVICE_<ID>_<KEY> prefix and only three layers; see Managing services.

Secrets

A config field marked "x-openmapx-secret": true in the manifest is a secret — an API key, a token, a password. Secrets are never shown in the Config form or returned in API responses; they live on the Credentials tab instead, where each declared secret is listed with its status: stored in the vault, supplied by an env override, or missing.

Set or rotate a secret from that tab. Its value is encrypted at rest in the database vault, which requires OPENMAPX_SECRETS_KEY to be set on the API host (generate one with openssl rand -hex 32). Without that key the vault is unavailable and the panel says so; an integration that declares secret fields will refuse to load at startup until the key is present, so this is a hard requirement rather than a silent degrade.

You can also supply a secret entirely through the environment, with the same INTEGRATION_<ID>_<KEY> variable used for any other setting — handy when you'd rather keep credentials in infra/docker/.env than in the database. As always, the env value wins over a vaulted one. An integration whose every required secret is still missing is flagged as unconfigured in the list and on the Overview dashboard.

Capability bindings

Integrations don't name a specific backend; they ask for a capability. The routing integration requires a routing-engine; the geocoding providers require a geocoder. Whether that capability is satisfied by Valhalla or OSRM, Photon or Nominatim, is a deployment choice — and when more than one installed service can satisfy a requirement, you choose which one, by setting a binding.

How a requirement resolves at load time:

  • A service: requirement names one specific service. It's satisfied if that service is installed and enabled — no choice to make.
  • A capability: requirement with exactly one provider is auto-selected. Nothing to do; the single matching service is used.
  • A capability: requirement with multiple providers is ambiguous. The integration won't resolve that requirement until you pick a binding — until then it logs a warning and the requirement is left unsatisfied.
  • An optional requirement with no provider falls back to the integration's public default (for example, a public routing endpoint) rather than failing.

You choose a binding on the integration's Overview tab. Each capability: requirement shows a dropdown listing every installed service that provides it; pick one to bind it, or choose (none — use fallback) to clear the binding and let auto-select or the public fallback take over. The picker only offers services that genuinely provide the capability — a service that doesn't is rejected.

A binding is a stored (integration, capability) → service mapping. The integration host loads all bindings once at startup and re-reads them on reload, so after changing a binding, reload the integration (or restart the API) for it to take effect. Like every admin action, setting a binding is audit-logged.

Bindings are per integration, not global

The same capability can be bound to different services for different integrations. Binding is also the only step you take in the panel — you still enable the backing service and render the stack separately; the binding just tells the integration which of the running providers to call.

Integration health

An integration whose manifest declares a healthCheck is probed periodically, and the result drives the status shown in the list, on the Health tab, and on the Overview dashboard. The host seeds the checks shortly after boot and refreshes them in the background; the Health tab also has a button to run the probe on demand.

A check resolves to one of three states:

  • up — the probe succeeded.
  • down — the probe failed (connection refused, a timeout, an HTTP error); the last error is shown.
  • unconfigured — a required config key or secret is unset, or a self-hosted service the check points at isn't enabled. This is treated as "not set up yet," not a failure, so a feature you haven't configured doesn't show up as broken.

An integration with no health check is simply assumed healthy. Some integrations run several sub-checks (for example, one per country or per upstream); those roll up into a single signal — any sub-check down makes the integration down, otherwise any up makes it up. Probe history is retained, so the Monitoring view can show trends rather than just the current state.

Bulk operations

Configuring features one page at a time is tedious on a fresh deployment, so the list's toolbar links to Bulk configure (/admin/integrations/bulk), a single page that consolidates the same workflow. It renders every configurable integration's settings form and credentials table inline, expanding on demand, and drives the same save and credential endpoints as the per-integration pages — so there's no separate "bulk" behavior to learn, just every form in one place.

The bulk page also publishes the complete environment-variable catalogue for your installation: for each integration, the exact INTEGRATION_<ID>_<KEY> variable name behind every setting and secret, with present / missing flags and non-secret defaults. Copy the block you need into infra/docker/.env when you'd rather pin configuration in the environment than click through forms. Secret values are never exposed here — only their variable names and whether each is currently set.

For acting on backend services in bulk (start, stop, recreate a whole preset), see Services administration and Managing services.

Where to go next

  • Admin panel — the full tour of /admin and how access is gated.
  • Services administration — enabling and operating the backends that satisfy capability requirements.
  • Community extensions — installing additional integrations from the store or a Git URL.
  • Configuration.env, secrets, and how the cascade fits the wider deployment.
  • Monitoring — health history, background jobs, and the audit log.