Skip to content

Configuration & GitOps

The configuration of a Phrame system is a git repository. Everything downstream is distribution.

Orchestration — desired state, actual state, and the loop between DESIRED — what should be running Architect /Operatorauthor intent GitLabphrame-configversioned config-syncvalidate →retained MQTT MQTT brokerdesired topics(retained) commit webhook retained orchestration-reconcilerchooses a node · commits the placement writes node.jsononly to a branch,never to a tag ACTUAL — what is really running node-controlleron every host Dockerinput-proc · mixersTAMS · egress actual stateheartbeat ·host stats mirrors →web-ui converge publish aggregate desired state, per node Git is the source of truth for desired state — a commit is a deployment, and the running system is reported back, not assumed

  • Source of truth: the phrame-config repository in GitLab
  • Bridge: services/config-sync (Node / TypeScript daemon)
  • Distribution: retained MQTT topics, qos: 1

GitLab is the source of truth; MQTT is the distribution channel; config-sync is the bridge between them.

  • On startup it authenticates to the broker, loads the JSON schemas from the config repository, then performs a full sync — reading every file under configs/ and publishing each to its retained topic.
  • On every git push a webhook fires, and it re-syncs only the added or modified files, publishing an audit event describing the commit.
  • Every file is validated against its JSON schema before publishing. A file that fails validation is logged and skipped — it is never published. A bad commit therefore cannot take the system down; it simply fails to take effect, leaving the last good configuration retained on the broker.

Schemas live in configs/schemas/*.json and are registered under the filename — node-config.json becomes schema id node-config. If schema loading fails at startup, validation is disabled with a warning and syncing continues, on the argument that distributing unvalidated configuration beats distributing none.

Only paths matching a known pattern are published. Schemas, Lua scripts and registry.json are deliberately ignored rather than pushed to the broker.

The git ref is not fixed at deploy time. The retained topic {prefix}/system/gitlab/ref, with payload {"ref":"<branch|tag|sha>"}, is the runtime source of truth:

Terminal window
mosquitto_pub -t phrame/system/gitlab/ref -r -m '{"ref":"v1.4.0"}'
  • At startup, config-sync adopts the retained value if the topic is set, deliberately ignoring the GITLAB_REF environment variable as potentially stale. If the topic is empty, it seeds it from GITLAB_REF. So the environment variable is only ever a cold-start seed.
  • At runtime, publishing a new ref re-evaluates lock state and runs a full re-sync from the new ref. Changes are serialised, and an unchanged value is a no-op.

Branch versus tag is a real behavioural difference. A branch ref keeps the git-push webhook live, so commits republish changed files. A tag or commit SHA locks it: pushes are ignored, and the reconciler refuses to write. The only way to move a pinned deployment is to publish a new ref here.

That is what lets “this facility is running v1.4.0” be a statement about the running system rather than about its inputs.

Operator — choosing the live configuration

Section titled “Operator — choosing the live configuration”

Selecting that ref is not a job for mosquitto_pub in production. The Operator SPA (services/operator) lets an operator choose the active platform configuration — a GitLab tag or branch of phrame-config — and create new branches from the UI.

It is deliberately thin. The REST and GitLab gateway lives in config-sync (GET /gitlab/tags, /branches, /ref; POST /gitlab/ref, /gitlab/branches), and setting a ref reuses the existing retained-topic control path rather than introducing a second way to do the same thing. Authentication is a Keycloak JWT verified against JWKS, requiring the phrame-operator role (phrame-admin is also accepted).

phrame-config (the service) is the versioned config editor for changes not made through Architect: acquire a Redis lock (30-minute TTL), edit in Monaco, save — which verifies the lock still holds — commit and push to GitLab, then release. Locks are broadcast over Socket.io so a second editor sees the file is held rather than discovering it at save time. State is SQLite for users, Redis for sessions and distributed file locks.

@phrame/orchestration-contracts is published to the GitLab npm registry on git tag, with the tag required to equal the package.json version so the two cannot drift. It exports the naming rules for every UUID-derived identifier, the orchestration event payload and its validator, the container-spec type, and the instance-config types.

Consumers authenticate to the registry with CI_JOB_TOKEN via a repo-root .npmrc, which means the contracts project’s job-token allowlist must permit each consumer — a step that is easy to miss and fails the consumer’s build rather than the contract’s.