mesh-sdk
The stable spine a Novox Mesh module's own code builds against — the tool it uses to plug into the mesh, and nothing that belongs to a specific module or to another tier.
It earns its place by rarely changing (novox/hq ADR 0039). The test for anything here: if editing it recompiles unrelated modules and it changes often, it does not belong. Per-module code (a service's API client, its tool implementations, its create-a-resource adapter) lives in the module, never here — that coupling is exactly what turned the old SDK into constant maintenance and made every edit rebuild every module.
What is in it
Five small areas, each a thing a module's tools/ or provisioner/ imports:
contracts — the runtime shapes module code touches
Not the manifest schema (the control plane owns and parses that, in Go). The shapes a module's
running code receives and returns: a provisioning grant and its credentials, the
mesh interface definitions (what an analytics grant contains, what an oidc-client grant
contains — the contract both a provider and a consumer conform to), and the tool and event types.
These change rarely and deliberately; when one does, a rebuild of everything is correct.
provisioner — the reconcile harness every provider shares
The loop is identical for postgres, redis, minio and umami: watch the grants directory, for each
requested grant call the provider's adapter, write the sealed credential, handle withdrawal, and
keep converging (watch). That loop lives here. A module provides only the adapter — "create
a database", "create a umami site" — which is the volatile, per-service half and belongs in the
module. This is the ADR 0039 line drawn through provisioning: stable loop here, per-service create
in the module.
tools — the tool-serving harness
registerModuleTools, the ToolDefinition type, and the registry/worker that loads a module's
tools and exposes them through the mesh's command surface. How a tool is declared and served is
settled; it does not change when an individual tool does. The tools themselves, and the API client
they call, live in the module.
messaging — the broker and event framework
The broker client (connect, request/reply, publish/subscribe), the event-consumer a module uses to react to mesh events, and the envelope/routing-key conventions. Transport primitives.
primitives — sealing, semver, resolved-env
Seal/unseal for secrets, semantic version comparison, and the accessor a module uses to read its own resolved environment.
What is NOT in it, and where it goes
- A module's API client and its tool implementations → in the module. (The Plex client, the
Umami client,
tools/plex.ts— all module-local. ADR 0039.) - Delivery machinery — build executor, bundler, dependency resolver, artifact manager, feature handlers → mesh-controller. It co-evolves with the pipeline.
- Host synchronisers — config-sync, ufw config, vhost generation, systemd, health checks → mesh-host's apply engine. The host applies these; they are not module code.
- Domain logic — tasks, workflows, agents, provider integrations → tier-2 contexts.
That is why this stays small: everything volatile has somewhere else to be.
Using it
A module's tools:
import { registerModuleTools, ToolDefinition } from "@novox/mesh-sdk/tools";
import { UmamiClient } from "../client.js"; // the client lives in the module, not here
registerModuleTools("umami", (env) => getUmamiTools(new UmamiClient(env.UMAMI_URL)));
A provider's provisioner:
import { runProvisioner, Grant, Credential } from "@novox/mesh-sdk/provisioner";
runProvisioner("analytics", {
async create(grant: Grant): Promise<Credential> { /* create a umami site, return the grant */ },
async remove(grant: Grant): Promise<void> { /* delete the site */ },
});
The adapter is the only thing the module writes; the watching, sealing and grant-file handling are the harness's.
Where the reasoning lives
Design and decisions are in novox/hq:
02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md— the stability rule this repository is02-DECISIONS/0040-what-a-module-is.md— a module, and why its per-module code lives in the module