Documentation / Exploitation / Guides pratiques / Declare and install plugins

Declare and install plugins

This recipe adds a plugin to a running Meridian instance: declare it in the instance manifest and let boot resolve it, either from a registry (by version range) or from a local build (path: override). It also covers PLUGINS_PATH, the filesystem-only alternative that bypasses the manifest's plugins: list entirely.

It assumes you already have an instance manifest (see First instance) and, if you're pulling from a registry, a reachable one (see Set up a plugin registry). For the full manifest schema, see Instance manifest.

Add a plugin from the registry

  1. Make sure registry: is set in the manifest, or MERIDIAN_REGISTRY is exported — the manifest key wins over the env var (apps/api/src/registry.ts, ensureInstancePlugins):

    apiVersion: meridian/instance-v1
    registry: gs://meridian-plugins        # or a local dir, or file:///srv/plugins
    
  2. Add an entry under plugins:, keyed by the plugin's package name. The shorthand form is a bare semver range; the long form is an object with a version key (libs/shared/src/plugin/instance.ts, PluginRequirement):

    plugins:
      "@posos/demo": "^0.1.0"
      "@posos/geriatrie": { version: "0.1.0" }
    

    The ranges are resolved together with the npm semver package against the versions published on the registry (resolveRequiredPlugins, libs/plugin-cli/src/registry/resolve.ts). The highest version of each range wins, unless its sdk range does not admit the host's plugin contract, or its dependsOn ranges disagree with a version the instance loads: a local plugin (PLUGINS_PATH roots, path: entries, such as the core plugins of the image), including one the host refuses, whose version still counts, or the version chosen for another required plugin. The next version in the range is then examined, and the search goes back to change the version of another required plugin when that is what rules a version out. A plugin listed in plugins: that is also found locally is not resolved on the registry: the local copy loads, and a version outside the instance range only logs a warning. A business plugin published before the image that carries the core plugin version or the plugin contract it requires is therefore not selected: the previous version stays until that image is deployed, rollback included. Any range semver accepts works (^, ~, 1.2.x, >=1.0.0 <2.0.0, an exact version, *). See Instance manifest for the full rule.

  3. Optionally confirm a matching version is actually published before you restart:

    meridian-plugin versions "@posos/demo" --registry gs://meridian-plugins
    
  4. Restart the process. Resolution happens once at boot, before plugins are loaded (apps/api/src/server/api.ts and apps/api/src/restate/server.ts both call loadPlugins(await ensureInstancePlugins(instance))):

    pnpm --filter @meridian/api run server    # catalog/API process
    pnpm --filter @meridian/api run restate    # workflow engine process
    

    Each process resolves plugins: independently. A version missing from the local cache is downloaded from the registry into MERIDIAN_PLUGIN_CACHE (default <repo>/.data/plugins, relative paths resolved from the repo root); a version already cached is reused without contacting the registry. The resulting folder becomes an extra discovery root passed to loadPlugins.

Pin an exact version instead of a range

Use an exact version string ("1.4.2") instead of a range when you need reproducible boots regardless of what gets published later — an exact version is a range with one match. There's no separate "pinning" syntax: it's the same version field. When the pinned version's dependsOn rejects a version the instance loads, there is no other candidate and the boot fails.

Override with a local path (skip the registry)

Use this for local development against an unpublished plugin, or a hotfix you don't want to push through the registry yet.

  1. Build the plugin so it has a dist/ (meridian-plugin build, esbuild — see Build and bundle). The registry-based flow above downloads this same shape of folder; path: just points at one directly instead.

  2. Replace the version with a path: for that plugin's entry:

    plugins:
      "@acme/vitals": { path: ../vitals/dist }
    

    A requirement declares version or path, never both — the manifest schema rejects an entry that sets neither or sets both (libs/shared/src/plugin/instance.ts, PluginRequirement.refine).

  3. Relative paths are resolved from the repository root, not from the manifest file's own location — the same rule INSTANCE_CONFIG itself uses (apps/api/src/registry.ts, fromRepoRoot).

  4. Restart the process. No registry is contacted for this entry at all — the path is used as-is, so nothing is downloaded and nothing is cached (apps/api/src/registry.ts, ensureInstancePlugins: the req.path branch returns before a registry store is even opened).

Load a plugin without declaring it (PLUGINS_PATH)

PLUGINS_PATH is independent of the manifest's plugins: list — it's a discovery root the plugin host scans on every boot regardless of what the instance declares (apps/api/src/plugins.ts, pluginRoots). Use it to mount externally delivered plugins onto the host filesystem (on-prem, air-gapped, container image layering) without touching the instance manifest at all. There is no version resolution here: whatever is on disk at that path loads, as-is.

  1. Point it at one or more compiled plugin directories — each one either a dist/ folder itself (contains plugin.yaml directly) or a parent folder whose immediate subfolders each contain one. Separate multiple entries with a comma or the OS path-list delimiter (: on Linux/macOS, ; on Windows):

    export PLUGINS_PATH=/opt/acme/plugins/vitals
    # or a parent of several plugin folders:
    export PLUGINS_PATH=/opt/acme/plugins
    
  2. Restart the process:

    PLUGINS_PATH=/opt/acme/plugins pnpm --filter @meridian/api run restate
    

    PLUGINS_PATH folders are added alongside whatever the instance's plugins: resolved to; both kinds of roots are loaded the same way — there is no implicit in-repo root. See Plugin host loading for how discovery and load order work across these roots.

Verify

  1. Watch the boot log: each resolved plugins: entry logs its plugin name, resolved version, whether it was downloaded or served from cache (registry entries) or its local directory (path: entries), and a final line listing every plugin actually loaded (apps/api/src/registry.ts and apps/api/src/plugins.ts, loadPlugins). « plugin requis : versions plus hautes écartées » lists the higher versions of the range that were set aside, each with its reason; « (requis) » in a reason marks a version the resolver chose for another required plugin.

  2. A plugin that is listed in plugins: but cannot be resolved (invalid range, no registry configured, range matches no published version, no combination of versions compatible with the host and the local plugins) fails the boot loudly. Treat that the same as an invalid manifest: it's a configuration error, not a runtime warning. The versions set aside are logged first, then the message names the plugin left without a version and what rules its versions out, three versions at most, for example [registry] @posos/demo@« ^0.2.0 » : aucune version publiée compatible avec l'hôte et les plugins locaux (0.2.3 : « @posos/clinical » 0.2.4 hors de « ^0.3.0 »). Publier une version compatible ou modifier la plage de l'instance. Publish a compatible version, deploy the image that carries the required version, or change the instance range. A path: that doesn't exist does not fail the boot: the host only warns « racine de plugins introuvable — ignorée » and loads without it.

  3. Open Plugins in the console: each loaded plugin is listed with its version, the directory it was loaded from, its events, its contributions and its manifest — see Inspect loaded plugins.

  4. Check the plugins log for load warnings. Each plugin is checked at load: a problem logs one of « contrat plugin incompatible — plugin ignoré », « dépendance signalée », « événement signalé », « exemple d'événement retiré », « déclencheur signalé », « nœud signalé », « binding hors contrat », « plage sdk insuffisante » or « module non chargé ». See Plugin load warnings.

    • « module non chargé » names a module of the plugin that does not import, most often a file missing from the plugin's dist/. The contribution that uses it is ignored and the rest of the plugin loads. Rebuild the plugin with meridian-plugin build, or ask its author for a fixed version.
    • « dépendance signalée » names a dependsOn entry of the plugin. When the version of the dependency found is outside the declared range, the range is invalid, or the plugin cites itself, the plugin is not loaded: load a version of the dependency in the range, or install a version of the plugin whose range accepts the one found. When the dependency is not loaded, or was itself refused with a version in the range, the plugin loads without the contributions that cite it: install the dependency with the plugin, or fix its load.
    • « plugin en double — copie ignorée » names a plugin found in two directories: only the first root loads (dir is the copy ignored, kept the copy loaded). Remove one of the copies.
    • In the registry log, « « @s/x » : copie locale 1.0.0 chargée, hors de la plage de l'instance « ^2.0.0 » » names a plugin listed in plugins: that is also found locally: the local copy loads instead of the registry one. Remove the local copy, or change the instance range.
    • A declared event and an event trigger resolve their context kinds, types and events in the plugin's dependsOn closure. When a plugin listed in dependsOn is not loaded, the events and event triggers that cite it are dropped, and the Plugins page marks them ignored at load. Install the dependencies with the plugin.
    • A plugin that declares sdk: "^0.1.1" (declared events) needs a host on plugin contract 0.1.1 or later; an older host skips it with contrat plugin incompatible — plugin ignoré.
97 documents6 sectionssource : /docs · généré au build