Domain Configuration Apps
Configuration and secrets live in the app that owns their domain (policy
domain-config-apps). There is one app for AI providers and their keys, one for
databases, one for sign-in, and so on. A person who wants to change the AI setup of a deployment opens
the AI app, not the deployment's record page, and finds every AI setting and every AI key there.
This page is the design: the inventory of what exists today, the app map, and what each app reads, writes and gates. It builds on three existing pieces and invents none of their parts:
- The Admin App:
/Adminon every instance, the home of instance-level admin settings; - Secrets: Write-Only Entry, Split Identities: every secret is entered through a write-only control and written by the governed writer identity, never by hand;
- Instance Secrets: the platform's
WriteOnlySecretSection/SecretInventorySectioncontrols and the instance's own encrypted store.
Why: the record page became a junk drawer
Every new need for a config value or a secret on a live deployment landed as another button, dialog
or panel on the control instance's Deployments/<id> page: Set Key Vault secrets… (a paste
dialog over every vault object the record names), a vault-secrets inventory, an announcement-key
panel. Each was reasonable alone. Together they made the record page the one place where everything
is typed, with no grouping by what the value is FOR. Someone rotating an OpenRouter key had to know
which vault object name the chart maps to OpenRouter__ApiKey. Someone checking a deployment's EU
residency had to read JSON.
The domain is the unit people think in. The apps group by domain, and the record page goes back to being the record: identity, status, fields, and links to the apps.
Two scopes, one set of domain names
A setting belongs to exactly one of two scopes, and the scope decides where its app lives:
| scope | what it is | changed by | reaches the portal | lives in |
|---|---|---|---|---|
| fleet — a deployment's recorded shape | the Deployments/<id> record's blocks (ai, signIn, email, gitHubApp, database fields, …) and the vault objects the record maps into the pod |
a global admin of the control instance | a governed Reconcile (record → helm values → rollout), or a governed SetSecrets (vault write, optional restart) |
the domain apps on the control instance, one page per deployment |
| instance — what an instance changes about itself, live | the instance's own mesh: Admin/Secret-* (InstanceSecrets), the Provider/* catalogs, Admin/UpdatePolicy, … |
a global admin of that instance | at once, no restart (the value is read from the mesh) | that instance's Admin app (/Admin/Settings/{tab}) |
Both scopes use the same six domain names, so a person finds the same word in both places: AI, Databases, Sign-in, Email, Payments, Integrations.
Why the fleet apps are separate apps and not Admin-app tabs
The Admin app exists on every instance and administers THAT instance. A fleet domain app administers
OTHER instances, one deployment at a time. It exists only where the Hosting package is installed (the
control instance), and every page carries a deployment. Putting it inside /Admin/Settings would mix
"this instance" and "that deployment" on one page, which is the same confusion the record page had.
So the control instance's Admin app LINKS to the six fleet apps; it does not host them.
The app map
Fleet apps — control instance, Hosting package
Each app is one node in the Hosting package: the NodeType Hosting/{X}App (sources
Hosting/{X}App/Source/) and the app node Hosting/{X} of that type. All six share one shell,
Hosting/DomainApp/Source (the picker, the record resolution, the admin gate, Apply). The
default area Deployments is a deployment picker (every Hosting/Deployment record the viewer
may read). The per-deployment page is the area Deployment with the deployment id as area id, so
every page is deep-linkable.
| app | address | reads and writes on the record | vault objects (write-only) | notes |
|---|---|---|---|---|
| AI | /Hosting/Ai → /Hosting/Ai/Deployment/{id} |
ai: providers (anthropic, azureAis, azureFoundry, openRouter, openRouterEU: endpoint, models, order, enabled, dataResidency, dataRetention), requiredDataResidency, tiers (heavy/standard/light/utility) |
the objects mapped to {Section}__ApiKey (Anthropic__ApiKey, AzureFoundry__ApiKey, OpenRouter__ApiKey, …) |
the EU/origin badges; the EU route holds no key of its own (it borrows OpenRouter's) |
| Databases | /Hosting/Databases → /Hosting/Databases/Deployment/{id} |
database, databaseServer, databaseHost, databasePort, databaseUsername, inClusterPostgres, inClusterDatabase, migrationImageRepository, backupStore |
databaseConnectionSecret (default {prefix}db-connection), the Orleans connection, storageConnectionSecret |
read-only migration status (the schema version and the last migration Job's verdict) and the backup list; a restore stays a governed Restore action |
| Sign-in | /Hosting/SignIn → /Hosting/SignIn/Deployment/{id} |
signIn: Microsoft client id + tenant, Google, LinkedIn, Apple client ids, enableDevLogin |
Authentication__{Microsoft,Google,LinkedIn}__ClientSecret, GitHub__OAuth__ClientSecret, Bootstrap__Secret |
an empty client id turns a scheme off; blank leaves it to the vault half (see SignInSpec) |
/Hosting/Email → /Hosting/Email/Deployment/{id} |
email: enabled, sender app client id, tenant, mailbox, managed identity, inbound, webhook base URL, forward address |
Email__ClientSecret |
the agent-send mode (Email:AgentSend) is instance config, not a secret |
|
| Payments | /Hosting/Payments → /Hosting/Payments/Deployment/{id} |
the commerce base URL (today an extraPortalConfig entry; owed: a payments block on the record) |
Commerce__Stripe__SecretKey, Commerce__Stripe__WebhookSecret |
live vs test is DERIVED from the key prefix (sk_live_ / sk_test_), so it is shown, never toggled; the webhook secret is generated in the portal and shown once, because Stripe must hold it too |
| Integrations | /Hosting/Integrations → /Hosting/Integrations/Deployment/{id} |
gitHubApp, opsGitHubApp (client id, installation id, owner), webhookInbox slots, registryInstanceId, announcementKeySecret, controlLaneKeySecret |
GitHub__App__PrivateKey, GitHub__Webhook__Secret, the registry instance key (issued straight into the vault, never read back), the per-deployment announcement key |
the announcement key is generated on the target instance and only registered here (below) |
Instance scope — each instance's Admin app
These are tabs of /Admin (see The Admin App), grouped under the same domain names:
| domain | tab | what it changes live |
|---|---|---|
| AI | /Admin/Settings/AiAdmin (exists) |
the instance's model credit bill and provider catalog; the catalogs themselves stay on the AI menu (/Provider/AiModels, tiers, providers) |
| Sign-in | /Admin/Settings/SignInProviders (exists) |
shows the effective schemes; the first-run hand-over |
| Integrations | /Admin/Settings/ControlLane (exists) |
the instance's announcement key: generated here, its fingerprint shown, Test connection |
| Databases | owed: /Admin/Settings/Database |
read-only: schema version, the last migration, storage backend |
owed: /Admin/Settings/Email |
read-only: whether mail is on, the mailbox, inbound state, the last send's result | |
| Payments | the Store's payments admin (the Store is its own app) | the payment-path audit |
A person's own AI keys are neither. They belong to the person, not to an admin, and stay in the
person's providers app (Providers/ProvidersApp, the ProviderSetup view: enc: in the mesh,
write-only).
The announcement key: generated on the target, registered on the control instance
Target design, not yet the procedure. Until MeshWeaver.Plugins#2435 lands, the shipped procedure is the one in Self-Update Announcement Key: the control instance ISSUES the key and the target's administrator enters it. Follow that page to operate today. The change set that moves the flow here also rewrites that page and the Instance Secrets summary.
The key that signs a deployment's self-update hand-over is generated where it is used: on the target
instance, in its Admin app's Control lane tab (InstanceSecrets Hosting:ControlInbox:Secret,
live, no restart). The control instance only has to VERIFY signatures, so it holds the same value
under the per-sender slot Hosting:PlatformWebhookSecret:{id}. The Integrations app's
Announcement key section is a WriteOnlySecretSection over that slot: an admin pastes the key
the target generated, and the page shows both fingerprints side by side, so a match is visible
without either value being shown. The deployment record keeps only the vault object's NAME
(announcementKeySecret) for the case where the key is mounted rather than entered.
What every fleet app page is made of
Every per-deployment page is the same three pieces, and none of them is new:
- The record's block, bound to the record. The form is the framework's form generator over the
block's type (
ModelProvider,ModelTiers,SignInSpec,EmailSpec, …) with a node-bound data context pointing INTO the record:LayoutAreaReference.GetMeshNodeDataContext(recordPath, bindContent: false, subPath: "content/ai/openRouter"). Every field reads from the record's node stream and writes one field back throughGetMeshNodeStream(recordPath).Update(...). There is no/datacopy and no Save button (see Data Binding). A viewer withoutUpdateon the record gets the same form read-only. - The domain's vault objects, write-only. A
SecretInventorySectionover the record's declared vault objects whose config key belongs to the domain (the AI app claims*__ApiKeyof the AI sections, the Sign-in appAuthentication__*__ClientSecret, …). Save and Generate file a governedSetSecretsaction; Disable/Enable/Delete/Recover file aSecretLifecycleaction; the status list is the operator'shosting-kv-statusreading (names, states, fingerprints; never a value). A domain claims config keys by prefix (VaultSecretDomains, next to the PluginsVaultSecretStewardthat files the actions), so a module that adds a provider adds its key to the AI app by naming it, with no change to the app. An object no domain claims is counted on the picker, never dropped silently. 🚨Ai__KeyProtection__MasterKeybelongs to NO domain, although its name starts withAi__: it encrypts everyenc:value on the instance (instance secrets, every person's provider keys), and replacing it makes all of them unreadable. It is a platform root key, changed only as break-glass, and no app lists it. - Apply. A button that files a governed
Reconcilefor this deployment. The record edit alone changes nothing on the running portal: a pod reads its configuration at start. Reconcile renders the record into helm values, runs the upgrade, and restarts when a secret class changed. It needs approval like every other non-read action, so the page shows the action it filed and links to it rather than a success message.
Everything on the page is platform controls in a real layout (Stack, DataGrid, the form
generator, WriteOnlySecretSection); no page builds its own form or markup.
Who may use which app
| surface | gate |
|---|---|
| the fleet apps (control instance) | global admin of the control instance (hub.IsGlobalAdmin()), checked live on every area; the Hosting package itself is enterprise-tier. Writing the record additionally needs Update on the record, and every governed action is authorised again by the control plane when it runs. Destructive secret operations (Delete, Purge) need a second approver. |
| the Admin app tabs (each instance) | global admin of THAT instance: an Admin grant in Admin/_Access (see The Admin App) |
| a person's own provider keys | the person |
A global admin of a customer instance is not a global admin of the control instance, so a customer can never change its own recorded shape through a fleet app. That is intentional: the record is the operator's contract.
How a change reaches the running portal
| change | path | when it is live |
|---|---|---|
| a record field (any block) | the field edit writes the record → Apply files Reconcile → approval → helm upgrade → rollout |
after the rollout completes |
| a vault object | SetSecrets (hosting-kv-set --wait) → approval → the CSI driver syncs → Restart after rolls the pods |
after the restart; without it, at the next pod start |
| an instance secret (Admin app) | InstanceSecrets write, as system after the admin check | at once, on every replica, with no restart |
The record is git-synced to Systemorph/Memex, so every fleet edit is also a commit, and the history of a deployment's AI setup is that file's history.
What the Deployment record page keeps
Identity, purpose, status and audit strips, the field table, the outbound links, and ONE link row to the six domain apps for this deployment. It carries no secret entry of its own: the Set Key Vault secrets… dialog, the vault-secrets inventory button and the announcement-key panel move to the apps above.
Inventory: every entry point today, and where it moves
| today | where | kind | moves to |
|---|---|---|---|
Set Key Vault secrets… (SecretDialog) |
control instance, Deployments/<id> page |
paste values for every declared vault object; files SetSecrets |
each domain app's vault section, by config key; the dialog is removed |
| vault-secrets inventory (MeshWeaver.Plugins#2434, draft) | same page | status, generate, lifecycle | its verbs (SecretStatus, SecretLifecycle, the compose functions) are what every domain app's vault section files; the page button is removed |
| announcement-key panel (MeshWeaver.Plugins#2435, held) | same page | issue/rotate/revoke | Integrations app, Announcement key section, register-only |
the record's ai block |
edited as raw record content | JSON | AI app, bound form |
| EU residency (MeshWeaver.Plugins#2448): the region/origin badges, the EU-only switch | chat model picker, model catalog columns | read + RequiredDataResidency |
the badges stay where models are picked; the per-deployment switch (ai.requiredDataResidency) and the per-provider dataResidency are edited in the AI app, which shows the same badges |
| OpenRouter EU route (MeshWeaver.Plugins#2454) | seeded from config (OpenRouterEU__*) |
config | AI app, the openRouterEU provider row (endpoint override, models, residency, retention); no key field, because the route borrows OpenRouter's |
| per-user provider keys | Providers/ProvidersApp (ProviderSetup) |
write-only, enc: in the mesh |
stays: it is the person's, not an admin's |
| AI Admin, Token Usage | /Admin/Settings/AiAdmin, /Admin/Settings/TokenUsage |
instance | stays (instance scope, domain AI) |
| sign-in providers | /Admin/Settings/SignInProviders |
instance, read + hand-over | stays; the fleet half is the Sign-in app |
| first-run setup wizard | /setup (pre-mesh), the provisioned wizard |
writes instance.json / the setup hand-over |
stays: it runs before any app exists. It asks for the same domains in the same order. |
| database fields, connection secrets | Fleet Console Set up a new instance dialog, the record | provision-time | provisioning stays in the Fleet Console; a LIVE deployment's database settings move to the Databases app |
| migrations | the helm migration Job, DbVersionGate (Planning a Database Migration) |
read | Databases app shows the status; running one stays a governed action |
| the record only | JSON | Email app | |
| Stripe keys | vault objects only, no UI | — | Payments app |
| GitHub App ids, private key, webhook secret | the record + vault, no UI | — | Integrations app |
| control-lane key (instance side) | /Admin/Settings/ControlLane |
instance, generated there | stays |
| registry instance key | RotateRegistryKey / RevokeRegistryKey actions |
governed | Integrations app lists the object and files those actions |
Build order
- AI app — the most-edited block, and the one the EU residency work already reshaped. First,
end to end: picker, bound
aiform with the badges, the AI vault section, Apply. - Integrations — it unblocks MeshWeaver.Plugins#2435 (the announcement key), which is held for exactly this.
- Sign-in, then Email — small bound blocks and one or two secrets each.
- Databases — the widest: fields, three secrets, migration status and backups.
- Payments — needs a
paymentsblock on the record first, so it is last.
After each app lands, the Deployment page loses the matching surface. The dialog goes when the last domain claims its last key.
Related
- The Admin App — the instance-scope home
- Secrets: Write-Only Entry, Split Identities — the vault half
- Instance Secrets — the instance's own store and the write-only controls
- Self-Update Announcement Key — the key the Integrations app registers
- Deployment on AKS — how the record renders into the chart
- Data Binding — the node-bound form every app page uses