> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quivr.thevibecompany.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Switch plugins without restarting

> Register a new plugin version at its address, let Quivr check it, and make it serve without restarting the api or the worker.

You run the new plugin version next to the one in service, register it with Quivr, wait for Quivr to check it, then activate it. The api and the worker follow within seconds, with no restart. This page switches a deployment's ingestion to the plugin from [Write an ingestion plugin](/plugins/write-an-ingestion-plugin).

## Prerequisites

* A key with the `plugins:admin` action in `QUIVR_OPERATOR_KEY`. Organization keys never get it; with the local stack, `eval "$(make -s env)"` sets one.
* The plugin certified by `quivr plugin test`, and reachable from the api and the worker processes.

## Steps

<Steps>
  <Step title="Run the new version">
    Run it at its own address, next to the version in service. Here, from the folder that holds `my-embedder`:

    ```bash theme={null}
    quivr plugin dev --port 9960 my-embedder
    ```
  </Step>

  <Step title="Register it">
    Send the exact text of the `quivr-plugin.yaml` the plugin was built from, its address, and the settings you would give it in a [pin](/plugins/pin): `configuration`, `routes`, `kinds` or `spaces`.

    ```bash theme={null}
    jq -n --rawfile manifest my-embedder/quivr-plugin.yaml '{
      idempotency_key: "acme-embedder-0.1.0", endpoint: "http://127.0.0.1:9960",
      manifest: $manifest, spaces: {"acme.embedder.base": "served"}
    }' | curl -s -X POST "$QUIVR_API_URL/v0/admin/plugins" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      -H "Content-Type: application/json" -d @- | jq '{registration_id, plugin_id, version, state}'
    ```

    ```json theme={null}
    {"registration_id": "plugin_registration_…", "plugin_id": "acme.embedder", "version": "0.1.0", "state": "registered"}
    ```

    ```bash theme={null}
    export REGISTRATION_ID=<the registration_id above>
    ```

    Quivr refuses at once, with `422 invalid_plugin`, anything it would refuse at startup, such as a configuration that fails the plugin's schema. The same `idempotency_key` returns the same registration.
  </Step>

  <Step title="Wait for the check">
    Quivr runs the Contract Runner, the checks of `quivr plugin test`, against the plugin's address. Read the registration until its `state` is `validated`:

    ```bash theme={null}
    curl -s "$QUIVR_API_URL/v0/admin/plugins/$REGISTRATION_ID" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      | jq '{state, certified: .check.certified}'
    ```

    ```json theme={null}
    {"state": "validated", "certified": true}
    ```

    A `rejected` registration lists every check and its issues in `check.checks`. The plugin must report the digest of the manifest you sent, so one built from another manifest is rejected. To check a fixed build, register it again with a new `idempotency_key`.
  </Step>

  <Step title="Activate it">
    ```bash theme={null}
    curl -s -X POST "$QUIVR_API_URL/v0/admin/plugins/$REGISTRATION_ID/activate" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      | jq '{plan_id, roles: [.roles[] | {role, plugin_id, version}]}'
    ```

    The answer is the new Pipeline Plan, which maps every role of the deployment to the plugin that serves it. The registration now serves every role it declares. The previous version of the same plugin leaves the plan, and so does a plugin whose roles it takes over entirely, here `core.ingest`.
  </Step>

  <Step title="Stop the old version once it is inactive">
    Work that started before the switch finishes on the old version (see [below](#work-finishes-on-the-version-it-started-with)). Until that work is done, the old registration reads `draining` and its `pinned_work` counts what is left:

    ```bash theme={null}
    curl -s "$QUIVR_API_URL/v0/admin/plugins" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      | jq '[.items[] | {plugin_id, version, state, pinned_work}]'
    ```

    Stop the old version once its `state` is `inactive`. Earlier plans stay readable at `GET /v0/admin/plugins/plans/{plan_id}`.
  </Step>
</Steps>

## Check it worked

```bash theme={null}
curl -s "$QUIVR_API_URL/v0/admin/plugins/plan" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
  | jq '[.roles[] | select(.plugin_id == "acme.embedder") | .role]'
```

The active plan names your plugin for its roles. Every api and worker process checks the active plan every 2 seconds (`plugin_plan_poll` in the [configuration](/reference/configuration)) and swaps in a new one whole. New work and every new search use the new plan.

## Roll back

If the new version misbehaves, one call makes the previous plan's plugins serve again:

```bash theme={null}
curl -s -X POST "$QUIVR_API_URL/v0/admin/plugins/plan/rollback" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
  -H "Content-Type: application/json" -d '{"idempotency_key": "rollback-acme-embedder-0.1.0", "pinned_work": "stop"}' \
  | jq '{source, previous_plan_id, roles: [.roles[] | {role, plugin_id, version}]}'
```

```json theme={null}
{"source": "rollback", "previous_plan_id": "plan_…", "roles": [{"role": "ingestion", "plugin_id": "core.ingest", "version": "…"}, "…"]}
```

The answer is a new plan whose roles are the previous plan's, and `previous_plan_id` names the plan you left. To return to an earlier plan, add its `plan_id` to the request. The api and the worker follow a rollback as they follow an activation. Nothing already produced is rewritten: articles the version you left processed keep their text and stay searchable.

`pinned_work` decides what happens to work pinned to the version you left:

| Value | What happens |
| - | - |
| `drain` (default) | The work finishes on that version, which drains as after an activation. |
| `stop` | Once each process follows the new plan, within `plugin_plan_poll`, the work's next call to that version fails instead. The processing of an article stops with the diagnostic `pinned_plan_stopped`, with the same outcome as `pinned_plugin_unavailable` (see [below](#work-finishes-on-the-version-it-started-with)). A rebuild fails with that code. A connector run fails as when its plugin is unavailable, and its next run uses the new plan. |

Quivr first checks that every plugin the rollback brings back still answers at its address. If one does not, or a different build answers, the call is refused with `409 plugin_unreachable`, naming the plugin and the cause, and the plan stays as it was. Start the plugin again, then retry. A rollback that would break one of the rules under [Limits](#limits) is refused like an activation.

`GET /v0/admin/plugins/plans` lists the latest plans, newest first. Each plan shows what recorded it (`source`: `configuration`, `activation` or `rollback`) and the plan it replaced.

## Work finishes on the version it started with

The processing of a receipt, a connector run and a rebuild each record the plan that was active when they started. Their retries, their later steps and a worker restart keep calling that plan's plugins, so an article is never processed half by one version and half by the other. Keep the old version running until it is `inactive`.

If the old version cannot be reached while it drains, its pinned work retries with backoff, up to `pinned_plugin_attempts` attempts (10 by default). The work then stops with the diagnostic `pinned_plugin_unavailable`, which names the plan and the plugin: a Version still waiting for its text is quarantined, a searchable Version keeps its text and its enrichment stops, and a rebuild fails. The work is never moved to the new version. A plugin of the active plan, in contrast, may be unreachable for any time: its work keeps retrying. Do not terminate pinned work by hand in Temporal, since its count would never be released.

## The configuration and the plan

The plugins pinned in `QUIVR_CONFIG` are registered at every start. The configuration then applies, role by role, only what changed in it since it last applied. A role whose pinned plugin you changed follows the configuration; a role you did not touch keeps what the plan says, so an activation survives restarts.

## Limits

* The check runs the normative fixtures only, since Quivr has just the manifest. A plugin that needs its own fixtures to be certified, such as a connector or a normalizer for a media type other than `text/markdown`, is rejected; pin it in the configuration instead.
* Alert rules switch through the configuration only: each Subscription names its rule's version, so its evaluations never change version.
* An activation cannot add or remove the retrieval role; pin or unpin a retrieval plugin in the configuration.
* An activation that would break a startup rule is refused with `409 plugin_conflict`: one normalizer per media type, one provider per connector kind, one ingestion and one retrieval plugin, extension namespaces, and vector spaces whose model changes without a new space version.
