Documentation / Exploitation / Guides pratiques / Configure ports and adapters

Configure ports and adapters

This recipe binds the platform's hexagonal ports — patient, prescription, agent, notification — to concrete adapters contributed by loaded plugins, in the instance manifest's ports: block. It assumes you already have an instance manifest and know which plugin you want to source each capability from. For the full field-by-field schema see instance manifest reference; for the concept behind ports/adapters see plugins concept and the glossary entry for Port (this is the hexagonal sense — not the node-level ports wired inside a workflow, see wire ports for those).

Prerequisites

  • An instance manifest file to edit (INSTANCE_CONFIG, defaulting to instance.yaml at the repo root — see environment variables). If you don't have one yet, start with your first instance.
  • The plugin that contributes the adapter you want to select is already loaded: via PLUGINS_PATH, or declared under the manifest's plugins: and resolved from a registry — see declare and install plugins and set up a plugin registry.
  • You know the adapter's name — the identifier that plugin registered for the port, not the plugin's package name. See bundled plugins for what ships with the platform, or the plugin's own manifest/docs otherwise.

Steps

1. Open the ports: block

ports is a free-form map, keyed by port name, of { adapter, config } (PortBinding, defined in libs/shared/src/plugin/instance.ts). The key is not restricted to any fixed list: every port is defined by the plugin that owns its contract — patient and prescription by @posos/clinical, notification by @posos/notify — while the SDK types only agent and terminology. A plugin can define and expose its own port under the same mechanism — it's wired the same way.

apiVersion: meridian/instance-v1
name: my-instance
ports:
  patient: { adapter: <name>, config: { ... } }
  prescription: { adapter: <name>, config: { ... } }
  agent: { adapter: <name> }
  notification: { adapter: <name>, config: { ... } }

2. Bind patient and prescription (for clinical workflows)

No port is required at boot — an instance without a patient record starts fine. But any workflow touching the record (patient.*, order.* nodes, human validations published to the record) fails at execution with a configuration error naming the missing port, and GET /api/records/patient/:id has nothing to read (the patient adapters declare record: { contextKind: patient }, which is what wires that route and validation publication). There is no platform default: pick one adapter name per port and, if the adapter needs it, a config object.

Development, backed by the seeded in-memory store — the reference adapters of @posos/clinical (adapter name in-memory):

ports:
  patient:
    adapter: in-memory
    config:
      patients:
        - demographics: { patientId: patient-42, sex: male, ageYears: 72 }
  prescription:
    adapter: in-memory
    config:
      prescriptions:
        - id: presc-9001
          patientId: patient-42
          items: [{ coding: { code: metformine, display: "Metformine 1000 mg" } }]

Production, backed by a FHIR warehouse (@posos/fhir):

ports:
  patient: { adapter: fhir, config: { baseUrl: http://fhir.internal:8080/fhir } }
  prescription: { adapter: fhir, config: { baseUrl: http://fhir.internal:8080/fhir } }

adapter is matched by the exact (port, name) pair against every loaded plugin's contributes.adapters entries (AdapterContribSchema in libs/shared/src/plugin/manifest.ts; matched by findAdapter() in apps/api/src/plugins.ts). If nothing matches, the host logs a warning and the port stays unbound; the first behavior that needs it fails at execution with:

Port « patient » non branché — sélectionnez un adapter dans le manifeste
d'instance (ex. ports.patient = { adapter: in-memory }).

(buildPorts() in apps/api/src/instance.ts binds; the guard lives in the consuming behaviors). For a full FHIR-specific walkthrough (auth, resource mapping) see connect a FHIR warehouse.

3. Bind agent (optional — needed for agent nodes)

Only required if the workflow(s) you run use agent nodes. The bundled runner is @posos/llm:

ports:
  agent: { adapter: llm, config: { maxIters: 8 } }   # maxIters optional, defaults to 8

The runner reads the LLM provider's API key from the environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, …, per provider — see libs/llm-client/src), not from this config. Which provider/model an agent actually calls is decided per-agent, in the plugin that contributes the agent (see author an agent) — this binding only selects which runner implementation executes that call. There is no mock fallback: an agent node fails loudly at run time if its provider key is missing.

4. Bind notification (optional)

Selects the outbound diffusion channel for @posos/notify. Two bundled adapters:

# Development: prints each notification to stdout, no config needed.
ports:
  notification: { adapter: console }
# Production: POSTs each notification as JSON to a webhook.
ports:
  notification:
    adapter: webhook
    config:
      url: https://hooks.example.org/notify        # required (or NOTIFY_WEBHOOK_URL)
      headers: { Authorization: "Bearer …" }        # optional
      timeoutMs: 5000                               # optional, default 5000

The webhook adapter fails loudly on a non-2xx response or an unreachable URL — delivery is part of the workflow's contract, not best-effort (external-plugins/notify/adapters/webhook.ts). This port only governs outbound diffusion; writing a notification to the patient record itself goes through the patient port, independently of how (or whether) it's also pushed out.

5. Set adapter-specific config

config is an opaque object passed straight to the adapter's factory, (config) => PortImpl (AdapterContribSchema.module/export/config in libs/shared/src/plugin/manifest.ts) — the platform doesn't validate its shape; each adapter defines and documents its own fields. The bundled adapters follow one recurring pattern — a config field overrides an environment-variable fallback of the same purpose:

Adapter config field Env fallback
fhir (patient/prescription) baseUrl FHIR_BASE_URL
hermes (terminology) baseUrl HERMES_BASE_URL (default http://localhost:8081)
webhook (notification) url NOTIFY_WEBHOOK_URL

Because instance manifests are versioned in git, prefer the environment-variable route for anything secret (auth headers, API keys) rather than writing it into config — see security posture. terminology: bindings use the same PortBinding shape but route per code system rather than one adapter per port; see configure terminology for that recipe.

6. Apply the change

The manifest is parsed and its ports resolved once, at process start (loadInstance() in apps/api/src/instance.ts, called from apps/api/src/restate/services.ts and apps/api/src/server/api.ts) — there is no hot reload. Restart the endpoint and proxy processes to pick up the edit; see upgrade the platform for the restart order in a running deployment. An invalid manifest (bad YAML, wrong types) fails the same Zod parse just as loudly, before the process comes up.

Verify

  • On restart, the instance component logs one info line per port that resolved, with port and adapter fields, or a warning naming the adapter it couldn't find — check the endpoint/proxy logs (see enable observability for log shape and correlation).
  • If a declared port doesn't resolve, boot continues with the port unbound (a warning names the adapter); the first behavior that needs it fails with Port « … » non branché — fix the adapter name or load the missing plugin, then restart.
  • Exercise the binding end-to-end: send an event that reaches the port (e.g. run a workflow that reads the patient record, or trigger a notification) and confirm it lands where the adapter is supposed to send it — see monitor executions.
75 documents10 sectionssource : /docs · généré au build