Deployment

MeshWeaver has two distinct deploy routes. They target different infrastructure — pick the one that matches where you're deploying. Neither is deprecated.

Route Target How Doc
AKS Shared cluster <aks-cluster> — every Deployments/<name> record (memex, memex-cloud, …) A Roll Hosting/InstanceAction on the control instance (the record's image pin); the operator runs build images → kubectl set image + rollout. Direct az aks command invoke is break-glass OperatingFromThePortal.md · DeploymentAKS.md
Azure Container Apps .NET Aspire test / prod modes (ACA, Sweden Central) tools/deploy.sh prod\|test (wraps aspire deploy + migration-exit + db-version gate) DeploymentContainerApps.md

🚨 The Deployment record is the ONE input; Aspire and Helm render from it; the image receives it as configuration (Deployment:Record); Aspire emits a record, never a chart. An instance is a DeploymentContent record (Deployments/<name> on the control instance), built fluently (builder.AddMemex("memex").WithImage(…).WithPluginRepo(…)…) or by hand, and every route reads that record — nothing is configured twice. Maintainer, 2026-09-08; ConfiguringAnInstanceFromAspire.md carries the fluent surface and the parity table.

Which doc do I need?

Scenario Read
Declare an instance ONCE — the Deployment record, the fluent builder, how Aspire and Helm render from it and how the image receives it (Deployment:Record); the parity table method → field → Helm value → config key ConfiguringAnInstanceFromAspire.md
Operate an instance without cluster access — roll, restart, suspend, audit, reconcile as Hosting/InstanceActions; which reads the API does not answer yet; how to read every kubectl recipe in this tree OperatingFromThePortal.md
See every running instance — who it's for, its infra, database, and version — and how to create or delete one Instances.md
Know what each instance actually runs — platform build, commit, framework identity, update policy and every module's pinned coordinate, reported by the instance itself, hourly DeploymentInventory.md
Database backups & disaster recovery — managed PITR, geo-redundancy, restore DatabaseBackups.md
Understand the release model, merge gates, version channels, and policy-driven self-update ReleaseStrategy.md
Know what CD guarantees about a published image set — all-or-nothing publication, the promote ordering, the self-healing reconciler — and why you verify the IMAGE and never the green tick ContinuousDeliveryContract.md
Work out whether an install can actually take the newest release — the schema boundary self-update cannot cross, why the resulting stall is invisible, and the three conditions a tag must clear before it is a safe helm upgrade target SelfUpdateSchemaWall.md
Ship a code update to the memex portal on the shared AKS cluster DeploymentAKS.md
Deploy an Aspire-orchestrated test/prod Container Apps environment DeploymentContainerApps.md
Understand the private-AKS-cluster architecture & operations behind the shared portal MemexCloudDeployment.md
Add a new tenant environment on the existing shared AKS platform OnboardingNewEnvironment.md
Run a prod-like memex locally on a Mac (Colima k3s, arm64) LocalColimaMac.md
Instance-specific configuration options (portal.example.com) DeploymentOptions.md
Reclaim space — delete old ACR images / prune local Docker, safely ImageCleanup.md
Turn production errors into tickets automatically — deploy the red-log watcher, route incidents to repositories, or work out why nothing is being reported LogWatchTriage.md

The two routes provision and run on different platforms (raw AKS deployments + Helm vs. ACA via Aspire), with different update mechanics; they are not interchangeable. The sections below (local run, Azure AD, secrets, project layout) are shared across both routes.


How a release reaches the fleet

The contract (maintainer, 2026-09-03: "end of github pipeline must call memex, which must register release and publish event") is three sentences:

  1. Every publishing pipeline ENDS with one call to memex. Core's CD, after the image set is promoted, POSTs the signed platform build (event: platform-build) into the control instance's Hosting/PlatformBuilds inbox (notify-platform-update). Every node repository's node-repo-publish-bake.yml run, after its bundles are sealed for an identity, POSTs the signed publication record (event: bundle-publication — source, identity, commit, tester + portal image) into the same inbox (register-publication, its last job). Nothing runs after that call, and no pipeline sends a repository_dispatch to another repository.
  2. memex REGISTERS the release as a durable node — Hosting/PlatformBuilds/<version> for a platform build, Hosting/Publications/<identity>/<source> for a bundle publication — the source of truth for "what is published for which identity" (what the self-update availability check reads).
  3. memex PUBLISHES the event from that registration: FrameworkReleaseBroadcaster sends meshweaver-framework-released (platform) or meshweaver-upstream-published (bundle publication, client_payload.version = the identity) to the subscribed repositories — the repositories the control instance's Hosting/Deployment records name as registry sources. The subscribers' CI receives it, resolves both images from the version, builds and publishes for that identity — and ends by calling memex (1).
 pipeline (core CD | a node repo's publish-bake)        memex (control instance)              subscriber CI
 ───────────────────────────────────────────────        ────────────────────────              ─────────────
 promote / seal ✅                                       WebhookInbox Hosting/PlatformBuilds
   └─ ONE signed POST ──(platform-build |──────────────▶│ verify HMAC
      bundle-publication)… and FINISH                    ├─ REGISTER  Hosting/PlatformBuilds/<version>
                                                         │            Hosting/Publications/<identity>/<source>
                                                         ├─ subscribers = Hosting/Deployment records'
                                                         │              pluginRepos[].isRegistrySource
                                                         └─ PUBLISH   repository_dispatch ─────────────▶ on: repository_dispatch:
                                                            meshweaver-framework-released |               types: [meshweaver-framework-released,
                                                            meshweaver-upstream-published                        meshweaver-upstream-published]
                                                                                                          → bake for the version → seal → POST memex

Where the pieces are: the POST steps in main-cd.yml and node-repo-publish-bake.yml (this repo); the inbox watcher, registration and broadcast in the Hosting module's PlatformBuildInboxWatcher (MeshWeaver.Plugins, Hosting/Deployment/Source); the broadcaster in src/MeshWeaver.GitSync. PlatformReleaseNotifyGuard.CoreDispatchesToNoRepository refuses a dispatch SENDER in any workflow under .github/workflows — there is no ledger — and UpstreamBuildGateGuard.TheLaneEndsByRegisteringWithMemex_AndDispatchesToNobody pins the lane's call.

Operator view: after a promote, the control instance's log carries [PlatformBuilds] verified build …, then [PlatformBuilds] release broadcast for <version>: N subscriber(s) dispatched.; each subscribed repository shows a repository_dispatch run whose payload carries source: memex; the node repos' pin-bump PRs follow. A 2xx on the pipeline's POST proves only that memex STORED the record.

Running Locally

Aspire (local mode)

Full local development with Docker containers (PostgreSQL pgvector + Azurite, Orleans in-process):

aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost/Memex.AppHost.csproj -- --mode local

Monolith (standalone, no Docker)

Lighter setup without Orleans or external infrastructure:

dotnet run --project ../MeshWeaver.Plugins/src/Memex.Portal.Monolith
# or via the AppHost:
aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost/Memex.AppHost.csproj -- --mode monolith

Azure AD App Registration

Microsoft authentication requires an app registration in Microsoft Entra ID (Azure AD).

  1. Azure PortalApp registrations → select your app (or create one)
  2. Under AuthenticationPlatform configurationsWeb, add redirect URIs:
    • https://localhost:7122/signin-microsoft (local Monolith — HTTP fallback port 5022)
    • https://localhost:7202/signin-microsoft (local Aspire portal — HTTP fallback port 5202)
    • https://<your-deployed-domain>/signin-microsoft (deployed environments)
  3. Note the Application (client) ID and Directory (tenant) ID from the Overview page
  4. Under Certificates & secrets, create a client secret

For single-tenant apps, configure the tenant ID explicitly — the default /common endpoint is not supported.


Secrets Management

Secrets are stored in dotnet user-secrets for local development and in GitHub secrets for CI/CD. (On AKS, secrets come from Key Vault through a SecretProviderClass the chart renders from the keyVaultSecrets values block — names only, never values; see DeploymentAKS → "Key Vault secrets are DECLARED in values".)

Parameters for distributed modes (the authoritative list is the builder.AddParameter(...) calls in ../MeshWeaver.Plugins/src/Memex.AppHost/Program.cs):

Parameter Description If unset
Parameters:azure-foundry-key Azure AI Foundry API key (LLM access) Required
Parameters:azure-foundry-endpoint Azure AI Foundry /models endpoint Optional — defaulted in appsettings.json
Parameters:anthropic-endpoint Anthropic-compatible endpoint Required — blank yields Endpoint is missing for model 'X'
Parameters:anthropic-model-0/1/2 Model catalog offered in the composer's model picker Required — blank yields an empty model dropdown
Parameters:embedding-endpoint Embedding model endpoint Optional (defaults to empty)
Parameters:embedding-key Embedding model API key Optional (defaults to empty)
Parameters:embedding-model Embedding model name Optional (defaults to empty)
Parameters:key-protection-master-key Encrypts ModelProvider API keys at rest Falls back to a dev default that is not secrettest/prod MUST override
Parameters:microsoft-client-id Microsoft OAuth client ID Required
Parameters:microsoft-client-secret Microsoft OAuth client secret Required
Parameters:microsoft-tenant-id Microsoft Entra tenant ID (single-tenant apps) Optional — omitted when empty
Parameters:google-client-id Google OAuth client ID Optional (defaults to empty)
Parameters:google-client-secret Google OAuth client secret Optional — omitted when empty
Parameters:linkedin-client-secret LinkedIn publishing (client id is inlined in the AppHost) Optional
Parameters:custom-domain Custom domain for the deployed portal Optional — omitted when empty
Parameters:certificate-name TLS certificate name for the custom domain Optional — omitted when empty

🚨 Several of these deliberately carry no value: default in the AppHost. That is not an oversight: passing value: "" makes Aspire resolve the parameter to the empty string and skip the user-secrets/config lookup entirely, so the setting silently stays blank even when user-secrets has it. Don't "tidy" a default onto them.

Set a secret with:

cd ../MeshWeaver.Plugins/src/Memex.AppHost
dotnet user-secrets set "Parameters:azure-foundry-key" "<your-key>"

Project Structure

memex/aspire/
├── Memex.AppHost/                  # Aspire orchestrator — defines all resources
├── Memex.Aspire.Hosting/           # The Aspire adapter (builder.AddMemex) — derives everything from the Deployment record
├── Memex.Portal.Distributed/       # Portal with co-hosted Orleans silo
├── Memex.Portal.ServiceDefaults/   # Shared service defaults (health, telemetry)
└── Memex.Database.Migration/       # Database migration project (runs MigrationRegistry.All)

Outside aspire/, memex/ also holds Memex.Portal.Monolith (the standalone dev portal), Memex.Portal.Shared (shared portal code, including the self-update poller), Memex.Client, and Memex.LocalMesh.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.