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

# ADR 0016 vendor control plane service

# ADR-0016: Vendor control-plane service (activation, entitlement signing, renewal, fleet health)

**Status:** Accepted · 2026-07-15

## Context

ADR-0015 established the two-plane separation and implemented the **deployment
side** of the Control contract (local entitlement verification, offline
activation, allowlisted telemetry preview), explicitly leaving the vendor-hosted
Control service — the holder of the signing private key — out of scope. That gap
meant online activation/renewal against `control.findmydata.io` was documented
but not runnable: a deployment could only *paste* a pre-signed token.

This ADR covers building that vendor service (`packages/server/src/vendor/`, run
via `bun run --cwd packages/server control`) and closing the loop with a
deployment-side online client.

## Decisions

### 1. A separate deployable with the mirror-image trust properties

The vendor service is its own process with its own entrypoint
(`src/control-main.ts`), its own port (`FMD_CONTROL_PORT`, default 8720), and its
own **isolated database** (`FMD_CONTROL_DB_PATH`, schema created inline in
`vendor/store.ts` — never the customer migration set). It holds what the
deployment must not: the entitlement **signing private key** and the pseudonymous
commercial registry (accounts, plans, activation codes, deployments, the
entitlements it has signed, fleet heartbeats). The customer product holds only
the public verification key and never runs this code path.

### 2. Content boundary is preserved by construction

The only customer-adjacent data the service accepts is the strict fleet-health
heartbeat, validated against the SAME `.strict()` `FleetTelemetrySchema` the
deployment uses (`controlplane/telemetry.ts`) *before* anything is stored — an
off-allowlist field (a name, path, excerpt, raw count) is rejected, never
persisted. Activation/renewal carry only an activation code, a tenant id, an
optional deployment pseudonym, and entitlement tokens. No content, evidence,
identities, or prompts cross the boundary.

### 3. Signing keys and admin credential come only from the environment

`vendor/config.ts` resolves keys by reference (an env var *name* pointing at the
secret), never from source. Exactly one signer is configured: an Ed25519 private
key (`FMD_CONTROL_SIGNING_PRIVATE_KEY_REF`, production) or an HMAC secret
(`FMD_CONTROL_SIGNING_HMAC_REF`, dev/test). Production **refuses** HMAC signing,
an ephemeral key, both-keys-set, or a missing admin credential — the boot guard
throws rather than start in an unsafe posture. Development generates an ephemeral
Ed25519 keypair and logs the public key so a local deployment can be pointed at
it.

### 4. Activation binds a deployment to an account plan and signs an entitlement

`POST /control/v1/activate` validates the activation code (existence, not
revoked, not expired, within its activation limit, tenant binding if set),
confirms the account is active and within its deployment ceiling, allocates a
pseudonymous deployment id, and returns a freshly **signed** entitlement built
from the account plan (edition, features, capacity, term, offline policy). Plans
are edition presets (`vendor/plans.ts`) with optional per-field overrides, so a
bespoke plan needs no code change.

### 5. Renewal re-issues with an extended term and supersedes the prior token

`POST /control/v1/renew` verifies a previously-issued token (any temporal
state — renewal is usually needed *because* it expired), confirms the
deployment/account are in good standing, and issues a fresh entitlement carrying
`supersedes = <old id>`. The superseded entitlement is revoked so a replaced
token cannot be renewed again (replay resistance). Renewal reads the *current*
account plan, so plan changes take effect on renewal.

### 6. The deployment never trusts a Control response blindly

The deployment-side online client (`controlplane/control-client.ts`) fetches a
token, but `activateOnline`/`renewOnline` (`controlplane/licensing.ts`) then
verify that token **locally against the deployment's own trusted public key** and
re-bind it to the requesting tenant before storing it. A spoofed or misrouted
Control response therefore cannot inject a license (covered by the
"untrusted-key" end-to-end test). Online renewal falls back to a local re-verify
if Control is unreachable, so a transient outage does not break a still-valid
deployment inside its grace window.

### 7. Admin surface is credential-gated and minimal

Account creation, activation-code minting, and account suspension require a
Bearer admin credential (constant-time compared). When no admin key is
configured the admin routes return 503 (disabled) rather than run unauthenticated.
Activation-code plaintext is returned once and stored only as a salted SHA-256
hash.

## Consequences

* Online activation/renewal against `control.findmydata.io` is now real and
  tested end-to-end (deployment ↔ live vendor service), superseding the "gated /
  out of scope" note in ADR-0015 §2.
* The vendor service and the customer product share the entitlement + telemetry
  *contracts* (one keyring shape, one strict schema) but nothing else — proven by
  the round-trip tests in `vendor/vendor.test.ts` (30 tests).
* Out of scope here (future work): the human Account portal
  (`account.findmydata.io`), self-service checkout/billing, key rotation across
  multiple concurrent signing keys, and a hardened KMS/HSM binding for the
  production signing key (the reference contract is env/vault by design).

## Verification

`vendor/vendor.test.ts` — activation (verifiable token, single-use/metered codes,
tenant binding, suspension, capacity ceiling, pseudonym anti-hijack), renewal
(extend + supersede + revoke-old, unknown/garbage token), telemetry (strict
allowlist rejection, unknown-deployment flagging), status transitions, the full
HTTP surface (incl. admin auth 401/503), config guards (production key rules), and
two live end-to-end flows (online activate+renew, and untrusted-key rejection).
All server tests pass (237) with typecheck and lint clean; the service boots and
serves a full activation flow via `bun run --cwd packages/server control`.
