Hosting

Administer the MeshWeaver deployments you operate — as mesh nodes, not as a spreadsheet and a folder of scripts.

Ships What
Hosting/Console The app (entryPoint, app: true): every instance at a glance — recorded state, the version actually running, sampled health with the sample's age, per-instance Grafana links — plus the record catalog and the new-instance request form. An OPERATOR surface: it reports the fleet, it does not command it. Type: Hosting/FleetConsole.
Hosting/Deployment A node type: one operated installation — host, namespace, database, image, update policy. The git-backed intent. Areas: Content (the record + its latest status sample), Overview (the fleet table), Logs (that deployment's log slice).
Hosting/DeploymentStatus Observation: replicas, restarts, the image actually running, derived health — one node per deployment, overwritten per sample. Area: Board (the fleet health board, each row compared against the latest available image).
Hosting/Issue A durable, tracked deployment incident with an open→resolved lifecycle, so a problem that happened and healed is still visible. Its Source/FleetWatch is the scheduled sweep that NOTICES: every 5 minutes it observes each recorded deployment, writes the status node, reconciles the incidents, and raises a platform notification for every Critical condition — a healthy fleet raises nothing, and one it could not READ raises Unobserved rather than silence. See FleetWatch.md.
Hosting/LogEntry A window of Loki landed as nodes, searchable by meaning. Area: Recent.
Script/refresh-status Samples kube-state-metrics via Prometheus into DeploymentStatus nodes. Roster comes from the Deployment records. Run it from its page.
Script/ingest-logs Pulls a log window from Loki for every recorded deployment. Run it from its page.
Hosting/InstanceAction The request node for a lifecycle run — provision, suspend, reactivate, back up, restore, tear down, reconcile/roll, restart, audit — and for the two OBSERVATIONS an operator used to need a cluster credential for: Sample (what every replica runs and says about itself → DeploymentStatus) and Logs (a Loki window → LogEntry nodes, with the LogQL, the count and the cut recorded). Phased, fail-fast, audited on the node. Areas: Content (one run), Runs (the fleet's run log). See Guide.md → "Roll, restart, observe".
Hosting/Backup One database dump that EXISTS: where it is, how big, and whether it was read back. Areas: Content, Backups.
Hosting/ConfigAudit What a namespace runs that NO repository renders — the Audit instance action's typed report (env patches, live-edited ConfigMap keys, unmanaged objects, secrets nothing renders; names only), verdict clean/drift per instance. Areas: Content (one report), Fleet (every instance's verdict). See Audit.md.
Hosting/BackupStore Where dumps land — provider, container, prefix, retention. Records, not secrets. Areas: Content, Stores.
/deployment Record and inspect deployments; report record-vs-live honestly.
/deployment-activity The pattern for provisioning, levelling and teardown inside the mesh, as activities — including the identity boundary that makes it safe.
/instance Run the lifecycle: create an instance, suspend it, give its owner their data, tear it down.
Hosting/ModuleInventory Which modules an instance carries, with the coordinate that says which BUILD. Areas: Content (one instance), Fleet (every reporting instance compared).
Script/report-modules Folds THIS instance's own coordinates and reports them — locally on the control instance, signed over the webhook from a tenant.
Hosting/RepoHealth The repository/CI audit no repository can run on itself — required contexts read from BOTH protection shapes (a ruleset-protected repo answers 404 on the classic endpoint), pull requests that are green and nobody armed, an UNMERGEABLE queue entry poisoning the stack behind it, an open UPSTREAM pull request that deletes a project this repo builds against, unpinned upstream checkouts, and a default branch that is RED right now (read from the newest DECISIVE run — a cancelled or skipped run decided nothing). Areas: Content, Fleet, Scan (measure now). See Repositories.md.

Install

Installing this package

Hosting is preInstalled and requires Store, so on a platform-baseline instance it arrives on its own and its content is open. To bring it onto a mesh that does not have it: /Store → the Hosting card → Provision (global admin), or create a Store/Provision node over MCP — never a hand-created Space (see AGENTS.md: the creator would silently own the partition). Verify with /Hosting/Console rendering the fleet console and search path:Hosting scope:descendants nodeType:NodeType -compilationStatus:Ok returning nothing.

Setting up a NEW instance — the setup dialog

The full configuration a brand-new instance boots with is entered in ONE place: /Hosting/Console → Request → "Set up a new instance…". Six steps:

  1. Identity — name (becomes namespace, database prefix, helm release and host), subdomain, the TEMPLATE record to clone, idle policy, reason.
  2. Main database — server, database name, the Key Vault secret NAME for the connection string, and optionally the connection string itself.
  3. Main storage — the storage account backing the instance's shares/content, its secret name, its connection string.
  4. Boot modules — the module assemblies the instance must come up with (the database driver MeshWeaver.Hosting.PostgreSql, the AI engine MeshWeaver.AI, the MCP endpoint MeshWeaver.Mcp, the view packs). A listed module that cannot load STALLS the rollout instead of booting green without the feature. 🚨 A non-empty list is the COMPLETE required set — the configuration overrides the image's list BY INDEX, never appends.
  5. Packages & repos — the plugin repositories to mount (one per line, Name https://url [ref] [source]; source = this instance SERVES the repo) and the packages a fresh boot seeds itself with.
  6. Review — the composed record, in the same field-table shape the fleet's config visualizer shows for the live instances, plus the vault secret names and the derived boot configuration (PluginCatalog:* + Modules:Required:*). The preview goes through the SAME composition the approval runs, so it is byte-for-byte what gets written.

Every field may stay blank — blank means "the template's value", which is why the template is required: the control plane clones infrastructure, it never guesses it.

Where the connection strings go. The Deployment record syncs to git, so it carries secret NAMES only ({name}-db-connection / {name}-storage-connection by convention). A typed value is encrypted with the platform key protector and stored on the request node — on a mesh without Ai:KeyProtection:MasterKey the filing is REFUSED rather than stored in cleartext. Left blank, the provision's hosting-kv-ensure step mints the vault secrets, or an operator sets them out of band (az keyvault secret set --vault-name … --name …).

Filing is not provisioning. The dialog writes an UNARMED Hosting/InstanceRequest; a global administrator approves it on the node (requestedAction: Approve), and only then does the control plane compose the record, open the config-repo PR and raise the provision — which itself refuses on a mesh where the operator is not enabled (see OperatorEnablement.md).

Playwright coverage: e2e/setup-dialog.spec.ts drives the dialog against the disposable harness mesh (see e2e/README.md for booting one) — the step walk, the review's composed preview, the plaintext-never-stored invariant, and the approval composing the record with stated values winning over the template.

Which modules are deployed

Hosting/ModuleInventory answers it, and the Fleet area answers the question worth asking: where do the instances disagree.

An instance reports itself. The control instance cannot read a tenant installation's nodes, and adding a way for it to would be a far larger permission decision than a fleet inventory deserves. So each instance folds its own coordinates and posts them to the control instance's signed webhook inbox (the PlatformBuilds shape) — nothing inbound is opened on a tenant, and the control instance gains no new reach. An instance that has never reported shows as not reported, never as "no modules": the two look identical in a row count, and one of them is a confident wrong answer about a healthy portal.

🚨 Both shapes, always. A module's coordinate lives either in {Space}/_GitSync or in a Plugins/{id} install record. Reading only the install records reports almost nothing and looks healthy — measured on memex 2026-08-10: 42 sync configs, ZERO install records. The fold is in ModuleInventoryContent.Fold, and when a module appears in both, the sync's commit wins as the build identity while the installer's version is carried alongside.

Silence is not a match. A module reported with no comparable build counts as differing, not as agreeing — two instances that cannot say what they run are not evidence that they agree. Stale reports are labelled with their age rather than rendered as current state.

Browsing logs

Two surfaces, deliberately:

A layout area does not query Loki on render: that would make every page load depend on the log stack being up. Ingest is a script, i.e. an activity, which is the only reason HTTP belongs in it.

The lifecycle

Planned ──provision──▶ Live ──suspend──▶ Suspended ──teardown──▶ Decommissioned
                        ▲                    │
                        └────reactivate──────┘

An instance is suspended before it is destroyed. Suspending stops the portal, takes a VERIFIED dump, and re-points the host at a paywall page served here — nothing is deleted, and reactivate undoes it. The suspension is the users' notice, their explanation and their window to export their data; teardown refuses on anything still Live, and waits out a grace period measured from the suspension.

The redirect is an Ingress patch, not a DNS change: the fleet shares one ingress controller and one public IP, the host's A-record already points at it, and the namespace's TLS secret keeps serving the certificate. A DNS-level redirect would take the cert with it and leave the customer at a TLS error instead of a page explaining what happened.

🚨 The paywall target must be anonymously readable, and it is not by default. The page is the Suspended area on the Deployment node, and the fleet space that holds those records is admin-only — so a suspended customer, who is anonymous by construction (they typed their own URL), would be bounced to sign-in instead of shown the page. Grant Anonymous + Public Viewer on the Deployment node you point PAYWALL_URL at, or point it at a public page of your own. Nothing enforces this: the redirect lands either way, and the failure looks like a sign-in prompt rather than a misconfiguration. The area itself is already written for that audience — it renders the instance's name and its retention window and NOTHING operational (no database, no namespace, no vault, no suspension reason), which is asserted in Test/.

An instance is defined by its record

Hosting/Deployment carries the whole instance spec — including pluginRepos (which plugin repositories it mounts) and preInstall (what a fresh boot installs). Those render into the portal's own PluginCatalog:* configuration, which the new instance applies itself on first boot; the last provisioning phase then VERIFIES the catalog, because an instance with green pods and an empty Store is the failure worth catching.

So creating an instance is: write the record, run the action. Nothing lives in a folder on somebody's laptop, which is what makes this usable by any platform developer.

Two questions, no defaults

A disruptive action (suspend, teardown, restore) will not run until both are ANSWERED: whether to back the database up, and the deployment's own id as confirmation. An unanswered backup question is a refusal — a default either destroys data nobody agreed to lose or hides that nobody decided.

A backup is a backup once it has been read back

Hosting/Backup.state reaching Verified — the archive was downloaded, parsed and found to contain tables — is what the destructive phases are gated on. Stored means bytes were uploaded and nobody looked; a truncated upload exits zero. IsRestorable refuses everything else.

Off by default

Instance actions need Hosting:Operator:Enabled=true plus an operator image, namespace and token, and belong on the control instance only. Without them every action refuses and says what is missing — the identity these runs use can delete namespaces and drop databases.

Enablement is a Helm release ROLL, not a merge: the values block reaches a container only when the release deploys, so "merged and green everywhere" can still refuse every action. Prove it with an action probe, never with the checklist — OperatorEnablement.md carries the executed runbook and the probe. And when the proper lane genuinely cannot run and someone breaks glass with kubectl, that is half an operation with a same-session reconcile attached — the Break glass section in /instance names the three debts (record repo-first, action node as audit + retry, status sample).

The model

A deployment is a node: the intended state, version-controlled through GitSync, reviewable as a diff. What a deployment is doing is a separate node type, sampled from the monitoring stack. Changing a deployment is an activity: one run with progress, cancellation, a log and an identity it ran as. The overview shows records; the board shows samples; the gap between them is the work list.

On the Systemorph fleet this plugin is the admin surface on memex.systemorph.com: the Deployments Space holds the records, two-way GitSynced to Systemorph/Memex (mesh/Deployments) — edit a record in the UI and the next commit carries it back to the repository; edit the repository and the next update brings it to the UI.

Records, not secrets

Deployment nodes are designed to sync to a git repository. They carry identifiers and topology only. The keyVault / keyVaultSecretPrefix fields name where secrets live — they never hold values, and neither do the other fields. A connection string in a record is a leak the moment it commits.

Which secrets a pod reads is declared by NAME in the record's keyVaultSecrets section, and the chart renders the SecretProviderClass, the CSI mount and the envFrom from it — so the record is the only place a secret's name lives, and the cluster holds nothing a repository cannot see. The naming rule (<prefix><Section>-<Key>Section__Key), the full path from a record field to the pod's env, and the rule that nothing may live only on the cluster are in Configuration.md.

Where the images come from

The fleet has ONE image source it publishes to — ACR — and one registry it pulls from: cr.meshweaver.cloud, a blob-backed OCI registry the public instance hosts beside its portal (MeshWeaver#3353). The rule every record follows:

Roll and Reconcile ensure the pull Secret too, first thing, so an instance provisioned before its record named one is not left rolling onto an image it cannot pull.

Intent is not observation

pinnedImageTag records what someone asked for. What a deployment is running is sampled from kube-state-metrics into DeploymentStatus nodes — deliberately a separate, unsynced, disposable type, so an observed number can never be committed back to git as if it were intent. The board compares the two and says which is which.

Updates themselves are a pull each installation performs on its own: the portal's self-updater polls the registry and patches its own Kubernetes Deployment (see deploy/aks/SELF-UPDATE.md in the MeshWeaver repo). Nothing in this plugin pushes an image anywhere.

Installing

From the Store. It needs Store@^1.0.0 and nothing else — no cluster credentials, no cloud identity. The scripts reach Prometheus/Loki over the cluster network, so they only return data when the mesh runs next to the monitoring stack. Reading live state and applying changes are separate, separately-permissioned steps; see /deployment-activity before granting anything.


How a deployment actually works — with examples

The one rule

Administer an instance THROUGH MEMEX — the MCP or the GUI. Never by hand-editing the config repository on GitHub, and never by patching the cluster with kubectl / az aks command invoke.

The mesh is the control plane. It holds the records, and it is what writes the repo and the cluster; it writes by opening a PULL REQUEST on a FEATURE BRANCH of the config repository, never a direct commit and never a live kubectl apply. So a change is reviewable before it exists, the repo stays a 1:1 description of what is deployed, and the roll is the merge rather than something someone did by hand at 2am.

Two failure modes make this a rule rather than a preference, and both look fine at the time:

🚨 The names are CROSSED — never infer an instance from a name

Call it Host Namespace Helm release Key Vault Secret prefix Config dir
meshweaverthe plugin registry memex.meshweaver.cloud memex-cloud memexcloud Systemorph memexcloud- deployments/aks/memex-cloud
systemorph memex.systemorph.com memex memex meshweaverkeyvault (none) deployments/aks/memex
atioz atioz.meshweaver.cloud atioz atioz deployments/aks/atioz

The namespace called memex is systemorph.com; the vault called Systemorph serves meshweaver.cloud. Both read backwards. Confirm against the RECORD — get @Deployments/{name} carries host, namespace, cluster, keyVault, keyVaultSecretPrefix, configPath, repository, helmRelease — or, in a cluster, against the ingress host. Never against the name. Getting this wrong restarts the wrong production portal; that is not hypothetical (2026-08-19).

⚠️ MeshWeaver/deploy/aks/** is the CHART DEFAULT plus an EXAMPLE env — not what is deployed. Reading its keyvaultName as live config is exactly how the vaults get crossed.

Example — record a deployment

A deployment is a node. Nothing is provisioned by writing it; it is the git-backed intent.

{ "id": "acme", "namespace": "Deployments", "name": "acme",
  "nodeType": "Hosting/Deployment",
  "content": { "$type": "DeploymentContent",
    "host": "acme.meshweaver.cloud", "namespace": "acme", "cluster": "memexaks-cluster",
    "database": "acme", "imageRepository": "meshweaver.azurecr.io/memex-portal-ai",
    "keyVault": "Systemorph", "keyVaultSecretPrefix": "acme-",
    "configPath": "deployments/aks/acme", "repository": "https://github.com/Systemorph/Memex",
    "owner": "ACME AG", "purpose": "Customer instance." } }

Example — run a lifecycle action

Changing a deployment is an InstanceAction node that a control plane watches. requestedAction is the only field you write; state / phase / message are watcher-owned.

{ "id": "acme-provision", "namespace": "Deployments", "name": "acme-provision",
  "nodeType": "Hosting/InstanceAction",
  "content": { "$type": "InstanceActionContent",
    "deployment": "acme", "requestedAction": "Provision",
    "reason": "onboarding ACME", "dryRun": true } }

Watch the same node: state goes Requested → Running → Done (or Failed / Refused).

🚨 dryRun: true renders the plan and runs NOTHING — the honest first step for anything destructive, and the only mode safe to leave on a node someone else might re-trigger. Drop it only when you have read the plan.

🚨 A destructive action needs confirmation repeating the deployment id EXACTLY. Not a checkbox: a checkbox is clicked on whichever row happens to be open, and this cannot be.

{ "content": { "$type": "InstanceActionContent",
    "deployment": "acme", "requestedAction": "Teardown",
    "confirmation": "acme", "backup": "Backup",
    "reason": "contract ended 2026-09-30", "dryRun": true } }

A secret can exist and still be invisible

Three independent things must line up, and nothing checks them together:

  1. the VALUE exists in the deployment's vault, under keyVaultSecretPrefix + its name;
  2. the SecretProviderClass lists that objectName and syncs it to the env key;
  3. a pod has RESTARTED since — the CSI driver reads the vault only at pod start.

Miss (2) and the feature behaves as though it were never built: MapPublish only mounts POST /api/plugins/bundles/{plugin} when Plugins:Registry:PublishToken is configured, so the route 404s and looks like a stale image. 🚨 And an SPC naming a secret the vault does not hold blocks pod startup (ContainerCreating forever) — the old pod keeps serving, so it stalls the rollout rather than paging anyone.

⚠️ Naming is not a pure transform. meshweaver prefixes every secret with memexcloud-, but Commerce:Stripe:SecretKeymemexcloud-Stripe-SecretKey (the Commerce segment is dropped) while Ai:KeyProtection:MasterKeymemexcloud-Ai-KeyProtection-MasterKey. Exceptions exist, so the mapping must be read, never derived.

🚨 Never kubectl exec … printenv to debug this — it prints secret VALUES into your terminal and into CI logs. Read the SPC's objectNamekey mapping instead.

CI — what runs, and what a client must provide

Gate What it proves
validate every node JSON is a well-formed MeshNode; manifest.lock matches the tree
compile-check every NodeType's Source/ compiles against the framework assemblies
test-repos the types reach CompilationStatus.Ok, render, and each type's Tests area runs GREEN
chart-drift (config repo) the rendered chart matches the LIVE cluster — this is what a hand-patch breaks

A client operating their own instance needs:

The e2e suite

e2e/instance-lifecycle.spec.ts drives exactly this flow — record → verify → change → refuse an unconfirmed teardown → dry-run teardown → delete. It runs only signed in against a non-shared mesh, uses a unique per-run id so it can never name a real deployment, and sets dryRun on every action.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.