Files
sencho/docs/features/blueprint-model.mdx
T
Anso b1decbb32a docs: v1 docs refresh (batch 7) (#1627)
* docs(introduction): refresh screenshots and correct stale nav/tab coverage

Replace all 5 screenshots with fresh 1920x1080 production captures.
Document the shipped Take down stack action, the Compose Labels stack
tab, and the Docker Labels Fleet tab, none of which were mentioned.
Correct the Console nav item to note it is a limited-availability
surface rather than a plain role/tier-gated view.

* docs(quickstart): refresh screenshots and correct preflight, dashboard, and menu drift

Replace all three quickstart screenshots with fresh captures and correct
several claims that drifted from the current UI:

- Document the environment preflight's 7th check (Sencho compose
  location), previously missing entirely from both the text and the
  screenshot alt text.
- Add the Stack health table's Source and Port columns, and note that
  columns are sortable and default to load order.
- Note the masthead's running-container count, live CPU/memory readout,
  and alert count.
- Fix the profile menu list: replace the nonexistent "Feedback" entry
  with "Open New Issue", drop "Appearance" (it lives in Settings, not
  the profile menu), and add the conditional "Billing" entry.
- Note that the sidebar groups stacks by Docker Compose label once
  labels are assigned, with pinned stacks first.

* docs(configuration): document missing env vars and fix JWT secret wording

Add SENCHO_UPLOAD_DIR, TRIVY_CACHE_DIR, SENCHO_MESH_RECONCILE_INTERVAL_MS,
SENCHO_MESH_PROXY_TUNNEL_IDLE_MS, SENCHO_COMPOSE_COMMAND_TIMEOUT_MS, and
SSO_OIDC_CUSTOM_ENABLED, all real environment variables that were missing
from the reference tables. Clarify that the JWT signing secret has no
environment variable at all, generated and stored in the database only,
rather than implying an unused JWT_SECRET var exists.

* docs(stack-management): refresh against current live app and source

Rewrites the Stack Management page against the production node and
frontend source: adds the Compose Labels anatomy tab (new since the
last refresh), the Mute action on the stack header and sidebar
context menu, corrects the update-available banner wording and the
sidebar context menu's lifecycle ordering, notes the Doctor tab's
severity dot and the scan-status banner, and replaces all 14
screenshots with fresh production captures.

* docs(editor): refresh against current live app and source

Replace all 8 screenshots with fresh 1920x1080 production captures (mobile
shot at phone viewport). Document the self-stack protection dialog that
blocks deploy/delete actions on the stack running the current Sencho
instance, the multi-container summary strip and Compact/Detailed density
toggle, the mutually exclusive Expand containers / Expand logs controls,
the Files tab full-screen toggle, and the post-deploy scan-status banner
on the Anatomy panel, none of which were previously covered.

* docs(editor): recapture mobile compose screenshot without redaction

The previous mobile screenshot used the plex stack, whose compose volume
mount includes a real host path that had to be blacked out and overlaid
with placeholder text, leaving a visible seam. Recapture against the
dozzle stack instead, whose only volume is the Docker socket, so the
screenshot needs no editing.

* docs(stack-file-explorer): refresh page against current app and code

Renames "Files & Volumes tab" references to the current "Files" tab
label, documents the full-screen toggle and word-wrap control, expands
the non-browsable-volume reasons to match the current containment
logic (single-file binds, the full protected host-path list, and the
Docker-unreachable named-volume case), documents the 100-item bulk
selection cap and the 5000-file/1 GiB archive limits, corrects the
non-existent DISK_FULL error code, notes that override compose files
are unprotected, and scopes the atomic-write claim to fs-backed roots.
Replaces all 9 screenshots with fresh production captures taken in the
Files tab's full-screen mode, so no other stack UI (health metrics,
logs) appears in the background.

* docs(resources): refresh against current live app and source

* docs(dashboard): refresh against current live app and source

* docs(app-store): refresh against current live app and source

Replaces all 6 screenshots with fresh production captures and corrects
the Environment Variables and About-panel metadata claims to match what
the bundled LinuxServer.io registry actually returns today.

* fix(docs): recapture app-store advanced-tab screenshot without scroll cut-off

The prior capture was taken mid-scroll to reveal the Security checkbox,
leaving the template logo and About text cropped awkwardly at the top.

* docs(global-search): refresh against current live app and source

* docs(deep-links): refresh against current live app and source

- Add App Store, Logs, Update, Console, and Audit to the URL table (previously undocumented)
- Document hub-only URL redirect behavior and cross-link to Multi-Node Fleet
- Note Fleet tab URL segments do not always match their on-screen label (Status/configuration, Map/dependencies)
- Document the no-env-files edge case (Env tab absent, falls back to compose)
- Clarify Settings section-list state is phone-only; desktop always normalizes to /settings/appearance
- Note which top-level views share identical URLs between desktop and phone
- Remove greenfield-violating temporal phrasing ("now has", "as today")
- Replace em dashes in touched bullet list

* docs(appearance): refresh against current live app and source

* docs(stack-activity): refresh against current live app and source

Add the missing "Stack taken down" event category, cross-link the
Suppressed badge to notification mute rules, list the new Compose
Labels tab in the Anatomy panel strip, and replace both screenshots
with current production captures.

* docs(dossier): fix generated-facts wording, add rollback readiness section

Corrects three Dossier tab Generated Facts rows against current frontend
behavior: the Ports count is not filtered to host-published mappings, the
Network row's "bridge" label is fixed text rather than a derived driver,
and missing env var names appear only in the Markdown export, not the
live tab. Adds a Rollback Readiness subsection under Connected Features
(previously only an orphaned Limitations bullet), links to Compose
Networking and Storage Portability, and clarifies that a missing export
section can mean either an older node build or a failed on-demand fetch,
not only a version gap. Recaptures all three screenshots against
production.

* docs(stack-drift): refresh against current live app and source

Fixes the stack detail tab bar description (Compose Labels tab was
missing, Files/Edit compose were wrongly listed as tabs), documents
that re-check only requires read access to the stack (so viewer and
auditor roles can trigger a ledger write), and cross-links the network
findings to the Networking tab's runtime drift section and the Fleet
Overview's Drift filter. Replaces all four screenshots with fresh
1920x1080 production captures.

* docs(compose-doctor): refresh against current live app and source

Corrects the rule count from 31 to 32 and documents the previously
undocumented self-managed-stack guard rule and enforcement, the
per-browser dismiss control for the summary card and Doctor tab dot,
the acknowledged summary state, the full Health-Gated Updates verdict
mapping, and the temporary exposure intent option. Replaces all four
screenshots with production captures.

* docs(compose-networking): refresh against current live app and source

Adds cross-links to the new node-wide Networking operator page, documents
the "view node networking" link and the unset/inherit exposure-intent pill
labels, corrects the stale Resources Hub network-tab reference, and
replaces all screenshots against the production node.

* fix(docs): unbreak stack-activity page render

The screenshot alt text used backslash-escaped quotes, which is
invalid MDX/HTML attribute syntax. Mintlify failed to parse the file
and silently dropped it from routing, so the page 404'd despite being
listed in docs.json navigation.

* docs(environment-guardrails): refresh against current live app and source

Fix the ENV FILES section description: it lists env_file: entries and
project files with an existence problem, not every declared env file.
A cleanly-resolving project file is already named in the Project
Environment File panel above it. Replace all three screenshots with
current production captures.

* docs(docker-label-audit): refresh against current live app and source

Add production screenshots (page had none), a Capability gating
section, a Limitations section, and a Troubleshooting accordion.
Tighten the secret-redaction heuristic description and cross-link
Stack Labels, Compose Doctor, Environment Guardrails, and Node
Compatibility.

* docs(compose-storage): refresh against current live app and source

Captured fresh production screenshots (both prior images were stale)
and fixed a leftover pre-rename card title pointing at the Files tab.

* docs(stack-labels): refresh against current live app and source

Removed the stale trailing-dot sidebar claim (that rendering was
removed from the sidebar in a prior UI pass), documented the new
label-scoped notification muting available from the sidebar, stack
context menu, and Settings panel, corrected the Fleet Actions card
copy to match the redesigned cards, added the previously undocumented
bulk-assign size cap and the node-scoped label bulk-action API, and
replaced all 8 screenshots with fresh production captures.

* docs(sidebar): refresh against current live app and source

Removes the stale per-row label dot claim (removed from StackRow), corrects the update indicator and Updates chip color from orange to fuchsia, and documents Mute/Take down in the context menu, the label group mute kebab, Ctrl+A/Esc bulk-mode shortcuts, pin-eviction toast, and the offline-node skip behavior in cross-node search. Replaces all 8 screenshots with fresh production captures.

* docs(deploy-progress): refresh against current live app and source

Corrected the Scan entry point (node-wide Scan this node from Security
Overview, not a per-stack config scan), added the missing Take down
verb and entry point, broadened recovery actions to cover restart and
rollback failures, fixed the modal status text and raw-output color
claims to match the live UI, and replaced all screenshots with fresh
captures from a throwaway demo stack on the production node.

* docs(atomic-deployments): refresh against current live app and source

* docs(health-gated-updates): refresh against current live app and source

* docs(deploy-enforcement): refresh against current live app and source

Correct the tier note to every-tier (verified against the current
policy gate, which no longer has a paid-only blocking switch), add the
Security Overview deploy-enforcement summary card, tighten the honor-
suppressions default wording, cross-link the separate pre-deploy scan
advisory dialog to avoid confusion with the block dialog, and replace
all three screenshots against the current production UI.

* docs(blueprint-model): refresh against current live app and source

Replaced all 11 screenshots with fresh production captures. Corrected
the volume-destroying-drift Enforce-downgrade claim (appeared three
times): that code path has no call sites in the runtime reconciler, so
any compose edit on a stateful or state-unknown blueprint always
re-enters Awaiting confirmation regardless of drift mode. Corrected
the Create workflow (creation does not trigger an immediate
reconciliation tick) and the Delete workflow (now requires typing the
blueprint name to confirm).

* docs(scheduled-operations): refresh against current live app and source

* docs(blueprint-model): drop tier framing, treat as Community-native

Blueprints carry no tier gate, so "available on every tier" implied a
comparison that doesn't exist. Removed the tier framing from the intro
note, the Prerequisites table, and the Security section; the role
requirement (admin to write, every role to read) already says
everything that matters.

* docs(auto-update-policies): refresh against current live app and source

Documents that every update trigger on this page, Apply now and
scheduled Auto-update tasks alike, runs through the same atomic backup,
build-aware rebuild, and post-update health gate as a manual update
from the stack editor, and is subject to the same deploy enforcement
policy gate. Corrects the readiness-computation description to scope it
to registry-image services and cross-links to Health-Gated Updates,
Atomic Deployments, Deploy Enforcement, and Stack Management. Replaces
the readiness board screenshot with a fresh production capture and
removes three orphaned screenshots left over from the page's prior
CRUD-policy layout.

* docs(auto-heal-policies): refresh against current live app and source

Correct the admin-only prerequisite: viewing policies and history is
open to every signed-in role, only creating/toggling/deleting requires
admin. Rewrite the matching troubleshooting entry to match, note the
overlap behavior between an All-services and a named-service policy on
the same container, and replace all three screenshots with a real
production policy.

* docs(webhooks): refresh against current live app and source

Replaced all three screenshots with fresh production captures (single
webhook create/reveal flow, two-card configured list), verified every
claim against WebhookService/StackOpLockService/registry.ts, and added
the previously undocumented per-stack lock skip behavior for
non-Git-source-sync actions with a matching troubleshooting entry.

* docs(global-observability): refresh against current live app and source

* docs(audit-log): refresh against current live app and source

- Fix nav terminology: Audit is a top navigation tab, not a sidebar tab.
- Correct retention claim: Admiral shows full retained history (default
  90 days), not a fixed 14-day window; the 14-day clamp only applies to
  the Community API, which has no navigation entry point at all.
- Fix settings path: Settings - Operations - Data Retention (previously
  pointed at the now-split Developer Diagnostics section).
- Restore current Recovery Vault naming (was stale Sencho Cloud Backup).
- Expand the tracked-actions list: stack take-down, label management,
  MFA reset, and notification suppression rules were missing.
- Note that assigning the Auditor role itself requires Admiral.
- Replace all four screenshots with fresh production captures (Stream,
  Table, expanded row, Data Retention) reflecting the current five-row
  retention card.

* fix(audit-log): reframe data-retention screenshot to card content only

The prior crop included the Settings sub-navigation panel, which isn't
part of what the surrounding text describes. Retake tightly cropped to
match the page's other screenshots (card content only, no chrome).

* docs(multi-node): refresh against current live app and source

Rewrites the Pilot Agent enrollment flow (now a generated compose.yaml
plus docker compose up -d, not a single docker run command) and the
Settings scope table (current registry: Stacks, Container Alerts,
Docker & Storage, Data Retention, Mute Rules, Recovery, and the
Admiral Account / Recovery Vault renames). Adds a Sencho Mesh
cross-link matching the live add-node form copy. Replaces all eight
screenshots with sanitized production captures.

* docs(pilot-agent): refresh against current live app and source

* docs(readme): refresh GitHub README against current live app

Replace all 9 screenshots (stale top nav showing a removed Console
item and missing the new Networking page); document Take down,
Drift Detection, Environment and Secrets Guardrails, Storage
Portability, Docker Label Audit, Remote Updates, and the node-wide
Networking dashboard; fix a broken non-root-user anchor link; correct
the notification channels list (no email channel exists yet) and the
global search scope (pages, nodes, and stacks, not containers or
services); rename "scan policy packs" to the current "scan policies"
terminology; broaden the App Store bullet to cover custom registries.

* docs(readme): reposition intro copy and refresh badge row

Lead with DevOps/platform/sysadmin framing instead of homelab-first,
drop pre-1.0 "production" language, state plainly that Sencho
provides a UI instead of promotional "does the work you do" phrasing,
and call out that multi-node was part of the architecture from the
start. Swap the Discussions badge for CodeQL, last-commit, open
issues, and live website/docs status badges, and add a blog link.

* docs: retake Dashboard and Global Search screenshots for accuracy

Configuration Status now shows Recovery Vault instead of the stale
Cloud Backup label, and the search palette capture includes the
Networking page entry added after the prior screenshot batch.

* docs: refresh Fleet View page against current app

Fleet View has drifted since its last rewrite: the Docker Labels tab,
Export Dossier action, Networking filter/badge, node label pills and
latency in topology, the Policy sync status row, and the node card
Mute submenu were all shipped but undocumented. The Check Updates and
Add Node actions also moved into the Overview toolbar (renamed Node
Update / Manage Nodes) and no longer sit in the shared action row.

Replaces all five screenshots with fresh production captures and
corrects the masthead's motion description (a calm shimmer on Healthy,
a steady glow on Degraded/Critical, not a pulsing dot). Also fixes a
Fleet Sync limitation that claimed no first-party sync-status panel
exists, now that the Status tab surfaces one.

* docs: refresh Fleet Dossier page against current app

Adds the page's first screenshot, documents the network exposure summary now included per stack, documents the storage-summary omission versus the Stack Dossier export, and adds two troubleshooting entries.

* docs(fleet-federation): deep rewrite for the rollout-approval gate

Federation's cordon and pin controls no longer take effect by
themselves: the reconciler requires a confirmed rollout preview
(Apply now -> Confirm Apply) before it mutates the fleet, and pinning
or unpinning always clears a blueprint's approval. Rewrote the mental
model, step-by-step, lifecycle table, security section, limitations,
practical workflows, troubleshooting, and cross-links to reflect that
gate. Also corrected the audit-log claims: cordon/uncordon/pin are not
filterable by a node.cordon/blueprint.pin action taxonomy, only by the
free-text search box against the method, path, and summary.

Replaced 4 screenshots and added 3 new ones (production fleet plus an
isolated instance to capture the rollout preview dialog in both a
safe and a blocked state), and fixed one stale pin-workflow sentence
in blueprint-model.mdx that the same audit surfaced.

* docs(fleet-federation): drop tier-availability framing

The feature isn't tier-gated, so calling out "every tier" reads as an
unnecessary advertisement. Removed the tier note and the Community/
Admiral prerequisite row, and reworded two role-visibility sentences
that had conflated tier with role.

* docs(fleet-actions): deep rewrite for the v2 action-card redesign

Rewrites the page against the shipped Fleet Action Card redesign: a
single cyan rail with per-card action-class chips instead of the old
per-card rose/purple/amber rails, plus the live blast-radius preview,
dry-run, and confirmed-target binding mechanics that replaced the old
static warning banners. Documents two previously-undocumented
endpoints (match-preview, prune/estimate), the preview-confirm-execute
model shared by all three cards, per-node locking, timeouts, and the
version-gated confirmed-target contract for mixed-version fleets. All
five screenshots recaptured against the live production UI.

* docs(fleet-actions): fix nested quote in screenshot alt text

* docs(fleet-sync): refresh against current app and document proxy gap

Retake all screenshots against the live fleet (control authoring view,
a genuine replica showing the read-only banner, and replicated
suppression rows) and document a previously-unwritten behavior: scan
policies, CVE suppressions, and misconfig acknowledgements are fetched
localOnly and are not proxied through the node switcher like most
other Security page tabs, so viewing a remote's Fleet Sync state
requires signing into that instance directly.

* docs(fleet-backups): refresh against current app and note new integration points

Replaces all 6 screenshots with fresh 1920x1080 production captures and
corrects several drifted details: the Cloud Snapshots panel's pagination
and per-item delete action, the exact Recovery Vault storage-mode label,
and the real per-file size-cap behavior (an oversized compose.yaml skips
the whole stack; an oversized .env is dropped but the stack still
captures). Documents two entry points that were missing from the page:
the pre-update snapshot checkbox in Health-Gated Updates and the snapshot
coverage nudge on the Storage Portability tab. Expands the single warning
banner description in the detail view to cover all four banner types,
notes the Snapshots tab and Recovery Vault are hub-only, and adds a
Where Fleet Backups fits table cross-linking the six adjacent features.

* docs(remote-updates): refresh against current app and add hardened-channel notes

Replace all screenshots with fresh 1920x1080 production captures and fix the
Node Updates trigger label (Check Updates -> Node Update). Add the Changelog
tab, correct the reconnect overlay's stale 'Update timed out'/Try Reloading
copy to the current Taking longer than expected/Reload to check text, and
document the Admiral Hardened Build channel's carve-outs (pinned-but-not-blocked,
entitlement-gated local update path). Disambiguate this page from the
unrelated top-level Update (Health-Gated Updates) nav tab.

* docs(node-compatibility): refresh against current app and expand capability list

Replace all screenshots with fresh production captures (node switcher,
capability lock card, connection test panel) and swap the lock-card example
from a now-hidden Host Console to Audit Log, which is directly reachable.
Bring the capability table from 26 to 35 entries to match the current
registry, add a Mute Rules row, and split out fine-grained fallback
capabilities (update-guard, service-scoped-update, cross-node-rbac,
stack-down-remove-volumes) with their non-lock-card fallback behavior.
Fix the compose-networking row, which incorrectly claimed to also gate the
node-level Networking overview. Note that Pilot Agent nodes never advertise
host-console regardless of version.

* docs(security): refresh against current app and document scan-node launcher

Replaces all screenshots with fresh production captures, documents the
Scan this node launcher and the third Scanner setup toggle (pre-deploy
scan advisory), tightens the Compose risks example list and the Policies
block-condition description to match the live app, and adds an On a
phone section.

* docs(vulnerability-scanning): refresh for exploit intel and risk-based policies

Documents exploit intelligence (KEV/EPSS evidence tags), the redesigned
risk-based scan policies (severity/known-exploited/fixable block
conditions replacing the single max-severity field), and the new
Findings badge state. Replaces all screenshots with fresh production
captures and adds a Secrets tab example.

* docs: refresh CVE suppressions page

Retook all screenshots against production (prior ones predated the
triage-status/OpenVEX UI and the edit capability). Documented the
suppression edit flow, which the page previously described as
delete-and-recreate only. Corrected the remote-node banner copy on the
Suppressions tab and added a screenshot of it. Cross-linked the
Misconfig acknowledgements panel that now sits on the same tab.

* docs(two-factor-authentication): refresh screenshots and document rate-limit layers

Replace all 11 non-count-variant screenshots with fresh production captures.
Document the throttled sign-in state precisely (kicker/hero/caption change,
not just the input), that a replayed TOTP counts toward the lockout counter,
and the separate per-network-address sign-in rate limit that sits in front
of the per-account MFA lockout.

* docs(api-tokens): refresh screenshots and document service-scoped deploy actions

Deploy Only now authorizes eight lifecycle POSTs (was six): the per-service
update and restore endpoints were missing from the scope table. Universal
restrictions gained a row for image channel management, which was already
rejecting API tokens in code but undocumented. Replaced all five screenshots
with fresh captures showing the current three-scope create flow and a
populated list with one token of each scope.

* docs(upgrade): refresh against current app and remove legacy-version framing

Fixes migration-list inaccuracies (registry credentials were never
plaintext, unlike node API tokens), rephrases the SSH/TLS and JSON-config
migration bullets to drop version-numbered legacy framing, adds a GHCR
mirror note matching the quickstart pattern, and cross-links the Hardened
Build entitlement-gated update path so digest-pinned installs aren't sent
down the manual docker pull steps.

* docs: refresh Backup & Restore against current backend

Broadens the encryption.key warning to cover everything CryptoService
actually encrypts (node tokens, Git source and Fleet Secrets, SSO/MFA,
Recovery Vault, fleet snapshot contents), not just registry credentials.
Adds the built-in backupData CLI as a no-host-tooling alternative to the
sqlite3 .backup command, cross-linked to Emergency command-line recovery.
Corrects node-token migration guidance: tokens are signed with a secret
stored in sencho.db and remain valid after a host move, so no
regeneration is required.

* docs: refresh Recovery guide against current Settings page and CLI

Documents the Settings · Recovery page's full System health / Environment /
Safe actions / Command-line hub instead of only its environment-preflight
section, adds the missing "Sencho compose location" preflight check, adds the
SSO/OIDC/LDAP lockout scenario now that disableSso.js exists, and corrects the
rollback description (exact UI label, backup-required and image-layer caveats).

* docs(emergency-cli): sync in-app page description, add CLI operational details

Aligns the "in-app Recovery page" summary with the freshly refreshed Recovery
guide (System health / Environment / Safe actions / Command-line recovery, SSO
providers, one-click command download). Adds constraints verified against the
CLI source: password/username minimums, the valid SSO provider identifiers with
an example, and a note that diagnostics.js always reports Docker as unreachable
(it runs without a live Docker connection, unlike the in-app page).

* docs(troubleshooting): refresh against current backend and nav

Corrects several claims that had drifted from the implementation:
network name validation (underscore is not a valid leading character),
the remote-update delayed-failure window (3 minutes, not 90 seconds),
and an unsubstantiated version-compatibility check on the Admiral 403
entry. Removes two entries describing legacy pre-v0.39 node behavior
that no longer applies to any currently shipped build. Replaces stale
"Profile > Settings > X" navigation with the current Settings hub
paths, and "wifi icon" with the current Test Connection label. Adds
Pilot Agent awareness to the remote-offline entry with a cross-link to
its dedicated troubleshooting section, and trims the docker-run
converter entry to point at the fuller, already-current version on the
Stack Management page instead of duplicating it with a broken anchor.

* docs(verifying-images): document the :dev GHCR integration tag

Verified the existing cosign/SBOM/VEX/tag-policy content against
docker-publish.yml and docker-preview.yml; all accurate. Added the
previously undocumented :dev/:dev-<sha> integration tag published on
every push to main via docker-dev.yml.

* docs(trivy-setup): sync install guide with node-scoped scanner and current Trivy upstream docs

Documents that Trivy installs independently per node, adds the TRIVY_BIN
override and non-PATH mounting option, fixes the deprecated apt-key install
flow and RHEL repo gpgkey placement, notes the Exploit intelligence toggle
for air-gapped hosts, and replaces the stale Scanner setup screenshot.

* docs(two-factor-admin): document lockout recovery and self-reset gotcha

Verified the reset flow, CLI recovery, and SSO toggle claims against
current source and replaced the stale admin-reset modal screenshot.
Added the failed-attempt lockout behavior (undocumented despite being
promised in the page description) and a warning about resetting your
own 2FA from the Users list, which signs you out immediately unlike
the self-service disable flow.

* fix(docs): make Settings and Self-hosting discoverable in the sidebar

The Reference and Operations groups used root pages that were only
reachable by clicking the ambiguous group label, and that click also
triggered an unwanted double action (expand plus navigate). List both
pages as explicit sidebar entries and disable global drilldown so group
headers only expand or collapse.

* docs(settings): sync reference page with current Settings Hub

Renamed License to Admiral Account throughout (Hardened Build channel
switch, image channel display, DURATION pill), corrected the password
policy, documented the new Appearance navigation and log-chip-color
controls plus font options, noted Mesh data plane is not on every
installation, fixed the label cap (50, not 100), and replaced every
screenshot with a fresh capture from the production node.

* fix(docs): apply the sidebar toggle-only fix to Start here and Product guide

These two groups had the same click-to-navigate-and-expand pattern as
Reference and Operations. Flattening them is safe here too: the
Product guide root page has no directory listing depending on it, and
the Start here root page's own hand-authored Next steps CardGroup
already covers the same links the auto-generated listing duplicated.

* docs(licensing): refresh licensing page for Hardened Build and sales-led Admiral pricing

Admiral pricing moved from self-serve checkout to a contact-sales model, and
a new Hardened Build image-channel switcher shipped in the Admiral Account
settings page; neither was reflected in the docs. Also documents the
lifetime-license edge case (no Manage subscription button, no Billing row)
and refreshes all four screenshots against the current UI.

* docs(security): sync reference page with current API-token and encryption scope

Verified every claim against the live implementation: the API-token universal
restrictions list was missing MFA management, Recovery Vault, and image
channel management; the encryption-at-rest field list was missing Recovery
Vault credentials; added a note on the password strength indicator's
recommended-vs-enforced distinction. Refreshed the SSO settings, API tokens,
and audit log screenshots against the production node.

* docs(settings): fix password policy and session-invalidation claims

Cross-checked against auth.ts while verifying the same claims on the
security reference page: the enforced minimum is 8 characters (the
12+/mixed-case/number text is a frontend strength hint, not a validated
rule), and a password change invalidates every other session for the
account rather than leaving them valid.

* docs(contact): sync contact channels with sales-led pricing and current support gating

Replace the retired contact@sencho.io with hello@sencho.io (the address actually
used in the website footer and the pricing page's Get in touch CTA), narrow the
licensing@sencho.io scope to existing-license activation/billing/refunds now that
new Admiral conversations route through hello@sencho.io, drop the stale LICENSE-file
and in-app upgrade-prompt claims (the AGPLv3 relicense removed both), correct the
Support settings path and add the published response-time targets, and remove the
unsubstantiated bug bounty mention.
2026-07-21 09:13:12 -04:00

403 lines
41 KiB
Plaintext

---
title: "Blueprints"
description: "Fleet-wide compose templates that Sencho keeps in sync across the nodes you choose, with stateless or stateful classification, drift detection, and three response modes."
---
A **Blueprint** bundles a `docker-compose.yml` with a node selector and a drift policy. Every minute Sencho compares the live state on each targeted node against the blueprint's desired revision, and either reports the drift, dispatches a notification, or auto-corrects, depending on the mode you picked. You declare a stack once; Sencho handles the distribution and the reconciliation loop.
Blueprints live under **Fleet · Deployments**.
<Note>
Blueprints are Sencho's Compose-first orchestration model. Creating, editing, withdrawing, and applying blueprints requires an admin role; operators and viewers can read the catalog and the detail sheet. Pinning a blueprint to a specific node lives under **Fleet · Federation** (admin role required).
</Note>
<Frame caption="Fleet · Deployments catalog with blueprint tiles, the All / Drifted / Observe / Suggest / Enforce filter chips, and the New Blueprint action in the top-right.">
<img src="/images/blueprint-model/catalog.png" alt="Blueprint catalog in Fleet Deployments" />
</Frame>
## Mental model
Three moving parts cooperate per blueprint.
1. **The declared spec.** A blueprint is a row in Sencho's database. It carries the compose YAML, the selector (labels or explicit node IDs), the drift policy, and a monotonic revision number. The revision auto-increments every time the compose changes.
2. **The reconciler.** A background loop on the controlling Sencho ticks every 60 seconds (with a 5-second initial delay after startup) and on demand after **Confirm Apply**. Each tick only mutates the fleet when the blueprint's current operational intent is approved. The tick resolves the selector, compares every desired-vs-live node, and runs only the place or remove outcomes the operator already confirmed.
3. **The executor.** Per-node deploy and withdraw run against the local Docker socket on local nodes, and through the standard authenticated proxy to `/api/stacks` on remote nodes. Every blueprint deployment writes a `.blueprint.json` marker into the stack directory; the marker carries the blueprint ID, the revision, and the last-applied timestamp.
The marker is the trust root. If a directory by the blueprint's name already exists on a node and does not carry a matching marker, the reconciler refuses to touch it and surfaces a **Name conflict** on the deployment row. A Blueprint named `nginx` will never overwrite an existing user-authored `nginx` stack on any node.
Statelessness vs statefulness is decided at author time by parsing the compose file. Stateless blueprints (no persistent volumes, or only `tmpfs` mounts) deploy and evict freely. Stateful blueprints (named volumes or bind mounts) get explicit operator-confirmation prompts on the first deploy to a fresh node and on eviction from any node. Blueprints with `external: true` volumes are classified as **unknown** and treated as stateful for safety.
Drift detection runs on every tick for every Active deployment regardless of policy. The policy only governs what Sencho does next: surface the drift silently, notify, or auto-redeploy.
## Key capabilities
**One declaration covers many nodes.** Pick nodes by label or by node ID. The selector set is re-resolved on every reconciliation tick. When a new node matches after a label change, the catalog shows **reapproval required** until you confirm the updated blast radius. Removing a label, removing a node, or shrinking the selector likewise needs confirmation before withdraw outcomes run (subject to the stateful-eviction safety rail).
**Drift detection always on.** Each tick compares the marker's revision against the live containers and the blueprint's current revision, then checks that all containers labeled with the compose project name are running. Drift is recorded on the deployment row whatever the policy is. **Observe** records it silently, **Suggest** also dispatches a notification, **Enforce** also redeploys.
**Stateful safety rails.** Stateful and state-unknown blueprints enter **Awaiting confirmation** on every fresh node before the first deploy, and **Evict blocked** when a node falls out of the selector while still hosting a stateful deployment. The reconciler never deploys empty volumes or destroys named volumes without a human acknowledging the action.
**Pin override and cordon respect.** Admins can pin a blueprint to a single node from **Fleet · Federation**. A pin replaces the selector entirely, deploys only to the pinned node, and overrides the cordon flag on that node. Cordoning a node otherwise prevents the reconciler from picking it for new placements; existing deployments on a cordoned node keep running and stay drift-checked.
**Vulnerability-policy participation.** Local blueprint deploys evaluate against the same pre-deploy policy gate that the per-stack deploy lane uses. If an enabled policy blocks one of the blueprint's image references, the deployment row moves to **Failed** and the stack is never written to disk. Remote blueprint deploys are routed through the remote node's stack deploy endpoint, so policy enforcement runs on the remote instance with that node's credentials and scanner state.
**Per-node, per-revision marker ownership.** Every blueprint deployment writes a `.blueprint.json` marker carrying the blueprint ID, revision, and last-applied timestamp. The reconciler refuses to touch any directory whose marker does not match its blueprint ID, so user-authored stacks and other blueprints can coexist on the same node without collision.
## Prerequisites
| Requirement | Detail |
|---|---|
| User role | **Admin** to create, edit, withdraw, accept, and apply. Operators and viewers can read the catalog and the detail sheet. Pinning requires admin. |
| Nodes | At least one node that the selector resolves to. Remote nodes need a healthy proxy connection; see [Multi-node management](/features/multi-node) and [Pilot Agent](/features/pilot-agent) for enrollment. |
| Compose YAML | Valid `docker-compose.yml`, 96 KiB or fewer. |
| Blueprint name | 1 to 64 characters matching `^[a-z0-9][a-z0-9_-]*$`. The name doubles as the stack directory on every targeted node. Rename is blocked while any non-withdrawn deployment or guard exists. |
| Selector | A `labels` or `nodes` selector with up to 200 entries per side. An empty resolved set is allowed but produces no deployments. |
| Compose directory | Per-node compose directory must be writable. Remote nodes must accept the controlling instance's bearer token; this is the same channel the rest of the fleet management already uses. |
## The catalog
The Deployments tab opens on a catalog of every blueprint configured on this instance. Each tile shows the name, an optional description, the classification chip (stateless, stateful, or unknown), the active-vs-targeted node count, and the current drift mode. Filter chips at the top break the catalog down by drift mode and let you isolate any tile that has drift.
<Frame caption="Catalog tiles. Each tile carries the classification chip, the active-over-targeted count, the selector summary, and the drift mode. The filter chips above the grid pivot the view.">
<img src="/images/blueprint-model/catalog.png" alt="Deployments tab catalog with classification chips and filter chips" />
</Frame>
The first time you visit the tab, the catalog is empty and a three-step explainer walks you through Author, Target, and Reconcile, with a **Create your first Blueprint** button at the bottom.
<Frame caption="Empty Deployments tab. Three numbered cards (01 Author, 02 Target, 03 Reconcile) explain the model before you create the first blueprint.">
<img src="/images/blueprint-model/empty-state.png" alt="Empty Deployments tab with three-step explainer" />
</Frame>
## Anatomy of a Blueprint
| Field | Purpose |
|---|---|
| **Name** | Used as the stack directory on every targeted node (`<COMPOSE_DIR>/<blueprint-name>/`). Lowercase letters, digits, hyphens, and underscores only. Rename is allowed only when every deployment is withdrawn (or there are none); live placements and guards block rename. |
| **Description** | Short prose for the catalog tile and the detail header. |
| **Compose** | Standard `docker-compose.yml`. The same file ships to every targeted node. Sencho parses it on save and classifies the blueprint as stateless, stateful, or unknown. The YAML must parse successfully and stay under 96 KiB. |
| **Selector** | Either label expressions (any-of plus all-of) or a list of node IDs picked by hand. |
| **Drift policy** | Observe, Suggest, or Enforce. Drift detection always runs; only the response differs. |
| **Reconciler enabled** | Toggle the reconciliation loop without deleting the blueprint. The **Disable** button on the detail sheet flips this toggle off; **Enable** turns it back on. Useful when you want to pause auto-deploys without losing the configuration. |
Sencho writes a `.blueprint.json` marker into each targeted node's stack directory. The marker carries the blueprint ID, revision, and the timestamp of the last apply. The reconciler refuses to touch any directory that does not carry a matching marker, so a Blueprint named `nginx` will never overwrite an existing user-authored `nginx` stack on any node. When the directory exists without a matching marker, the deployment row enters the **Name conflict** status until you rename one of the two.
Before a local Blueprint deploy starts, Sencho applies the same pre-deploy [vulnerability scan](/features/vulnerability-scanning) policy gate used by standard stack deploys. If an enabled policy blocks one of the Blueprint's image references, the deployment row moves to **Failed** and the stack is not written to disk. Successful local Blueprint deploys also trigger the normal post-deploy scan. Remote Blueprint deploys are routed through the remote node's stack deploy endpoint, so policy enforcement runs on the remote instance with that node's credentials and scanner state.
## The editor
**New Blueprint** opens a single editor dialog with everything inline: name and description fields, a Monaco YAML editor with the classification banner sitting above it, the selector picker (Labels or Specific nodes), the drift policy cards, and the Reconciler enabled toggle.
<Frame caption="The Blueprint editor. Name and description fields, the classification banner pinned above the compose editor, and the Specific nodes selector below. Drift policy cards and the Reconciler enabled toggle sit further down in the same scrollable dialog.">
<img src="/images/blueprint-model/editor-dialog.png" alt="Blueprint editor dialog with name, description, classification banner, compose editor, and node selector" />
</Frame>
### Classification banner
The banner above the YAML editor shows what Sencho found when it parsed your compose file:
- **Stateless · portable** when no persistent volumes were detected, or only `tmpfs` mounts. Sencho can deploy and evict the blueprint freely on any node.
- **Stateful · pins to data** when named volumes or bind mounts were detected. Each node holds its own data; Sencho does not replicate volumes between nodes.
- **State unknown** when `external: true` volumes were detected. Sencho cannot prove portability and treats the blueprint as stateful for safety.
Click the signals toggle on the right of the banner (`1 SIGNAL`, `2 SIGNALS`, etc.) to expand the list of reasons Sencho classified the way it did, line by line.
<Frame caption="The classification banner expanded. The signals list shows the exact rule that fired for each detected mount, volume, or external reference.">
<img src="/images/blueprint-model/classification-banner.png" alt="Classification banner in the Stateful · pins to data variant with two signals expanded" />
</Frame>
### Selector
A **Labels** selector matches any node whose labels satisfy the expression. The picker is split into two pill rows: **Match nodes with ANY of these labels** (the OR set), and **AND ALSO require ALL of these** (the AND set). Either side may be empty. The dialog renders a one-line summary just below the pills, so you can see exactly which nodes the expression resolves to before you save.
A **Specific nodes** selector picks nodes by ID with a checkbox list. Use it when you want a one-off blueprint that runs only on a known node, or when no labels exist yet.
Add labels to nodes from **Settings · Infrastructure · Nodes**. Each node row carries a Labels column with a `+` button.
<Frame caption="Settings · Infrastructure · Nodes. The Labels column carries pills per node; the `+` button opens an inline add-label popover.">
<img src="/images/blueprint-model/node-labels.png" alt="Settings Infrastructure Nodes page with the Labels column and add-label popover" />
</Frame>
### Drift policy
The three policy cards in the editor map exactly to the three modes the reconciler runs against every tick:
| Card label | Reconciler behavior |
|---|---|
| **Observe** · Detect & display, no notifications | Drift surfaces in the deployment table; no notification, no auto-fix. |
| **Suggest** · Detect & notify, operator decides | Sencho dispatches a `blueprint_drift_detected` notification through your notification routes, if any. |
| **Enforce** · Detect & auto-fix, silent on success | Sencho re-deploys the blueprint silently when drift is detected. A `blueprint_drift_correction_failed` notification fires only when the auto-fix attempt fails. |
Even **Observe** keeps Sencho honest about what it found: the deployment row shows "drifted 3h ago: service caddy exited code 1". Silence would forfeit Sencho's authority over your fleet.
Enforce's auto-fix only ever redeploys the blueprint's *current* revision to correct container-level drift (a stopped or crashed container, for example). A compose edit never reaches that auto-fix path on a stateful or state-unknown blueprint: any revision change re-enters **Awaiting confirmation** on every targeted node first, regardless of drift mode, so a human always signs off before a new revision touches existing data. See [Lifecycle: status transitions](#lifecycle-status-transitions).
## The detail sheet
Click any tile in the catalog to open the blueprint detail sheet on the right. The sheet header shows the name, the meta line (`<selector> · <drift mode> · rev <n>`), the action bar (Apply now, Edit, Disable, Delete), the description, the deployment table, and a collapsible compose source.
<Frame caption="The detail sheet. Header carries the selector and revision; the action bar offers Apply now, Edit, Disable, and Delete; the deployment table lists one row per resolved node.">
<img src="/images/blueprint-model/detail-sheet.png" alt="Blueprint detail sheet showing header, action bar, and deployment table" />
</Frame>
The deployment table lists every node currently in the blueprint's resolved selector set. Each row carries the node name, the deployment status, the last-activity timestamp, a notes column (drift summary or the last error message), and the action available for that row.
Possible status values:
| Status | Meaning |
|---|---|
| **Pending** | The reconciler has acquired the deployment lock and is about to start. |
| **Awaiting confirmation** | A stateful blueprint reached a new node. The reconciler refuses to deploy until you click **Confirm deploy** in the action column. |
| **Deploying** | The compose action is in flight on this node. |
| **Active** | The container set matches the blueprint's revision. |
| **Drifted** | The reconciler detected a difference between the live state and the desired one. |
| **Correcting** | An Enforce-mode auto-fix is in flight. |
| **Failed** | The deployment or withdrawal errored. The notes column carries the message. |
| **Withdrawing** | A withdraw is in flight. |
| **Withdrawn** | The deployment was successfully removed; the row is about to be cleared. |
| **Evict blocked** | A node left the selector while a stateful deployment was active. The reconciler refuses to evict until you choose an explicit mode. |
| **Name conflict** | A directory by the blueprint's name already exists on the node and does not carry our marker file. The reconciler will not touch it. |
<Frame caption="A deployment row in Awaiting confirmation. The Confirm deploy action sits at the right; the notes column explains why the reconciler is waiting on the operator.">
<img src="/images/blueprint-model/detail-state-review.png" alt="Blueprint detail sheet with a deployment row in Awaiting confirmation state" />
</Frame>
## Lifecycle: status transitions
The common paths through the status enum.
- **Stateless first deploy.** No row → **Pending** → **Deploying** → **Active**. Drift detected later on Enforce flips to **Drifted** → **Correcting** → **Active**.
- **Stateful first deploy.** No row → **Awaiting confirmation**. Operator clicks **Confirm deploy** and picks **Deploy fresh** → **Deploying** → **Active**.
- **Compose change on a stateless deployment.** **Active** → **Deploying** on the next tick → **Active**.
- **Compose change on a stateful or unknown deployment.** **Active** → **Awaiting confirmation** on every targeted node so the operator can decide whether the new revision is safe for that node's local data. Operator confirms → **Deploying** → **Active**.
- **Node leaves a stateless selector.** **Active** → **Withdrawing** → **Withdrawn** and the row clears.
- **Node leaves a stateful selector.** **Active** → **Evict blocked**. Operator chooses **Snapshot, then evict** or **Evict and destroy data** → **Withdrawing** → **Withdrawn**.
- **Pre-existing stack collision.** New row → **Name conflict**. No further action without operator rename of the user-authored stack or of the blueprint itself.
- **Deploy or withdraw error.** Any in-flight status → **Failed** with the message in the notes column. The row stays put until the next tick or until the operator clicks **Apply now** to retry.
## Working with Blueprints
### Create
1. Go to **Fleet · Deployments**.
2. Click **New Blueprint**.
3. Fill in the name, description, compose YAML, selector, and drift policy.
4. Watch the classification banner update as you type. It tells you whether the blueprint is portable or pinned to data.
5. Click **Create blueprint**. The blueprint starts with approval **pending**. No fleet mutation runs until you confirm a rollout preview.
If the YAML is malformed or larger than 96 KiB, Sencho rejects the save before creating a Blueprint row.
### Apply on demand
The reconciler runs every minute, but only for blueprints whose current operational intent is **approved**. To authorize or re-authorize a rollout, click **Apply now** on the detail sheet (or **Retry** on a failed row). Sencho opens a rollout preview that lists place and remove outcomes, requirements, and health notes. **Confirm Apply** stores that approval and runs the confirmed executor actions. If some nodes fail (for example a remote host is unreachable) or an unmanaged same-name stack blocks placement, Confirm still records approval, but the response and toast report those per-node outcomes instead of a clean success. Changing the compose, selector, name, pin, drift mode, or enabled flag clears approval back to pending. Catalog tiles show **pending** or **reapproval required** when confirmation is needed again (for example after a label change adds a new target node).
Raw `POST /api/blueprints/:id/apply` clients must send `{ planFingerprint, actions }` from a fresh `GET .../preview`. A body without that confirmation is rejected.
### Edit
Click **Edit** on the detail sheet. Editing the compose bumps the revision. Stateless blueprints redeploy on the next reconciliation tick. Stateful and state-unknown blueprints re-enter **Awaiting confirmation** on every targeted node so the operator can decide whether the new revision is safe for each node's local data, whether or not the change actually touches a volume.
### Confirm a stateful first deploy
The first time a stateful blueprint reaches a node, the deployment row enters **Awaiting confirmation**. Click **Confirm deploy** in the action column to open the state review dialog. **Deploy fresh** creates empty named volumes on the target node and starts the stack with the default state from each container image. **Restore from snapshot** is reserved for the future Volume Migration feature and is currently disabled.
<Frame caption="State review dialog. Deploy fresh creates empty named volumes on the target node; Restore from snapshot is reserved for the future Volume Migration feature.">
<img src="/images/blueprint-model/state-review-dialog.png" alt="State review dialog with Deploy fresh enabled and Restore from snapshot disabled" />
</Frame>
### Withdraw a single deployment
In the deployment table, click **Withdraw** on the node's row. For stateless blueprints, Sencho confirms once and runs `docker compose down` plus a directory removal. For stateful blueprints, the eviction dialog asks how to handle the data on this node:
- **Snapshot, then evict (recommended)** captures the blueprint's compose definition into [Fleet · Snapshots](/features/fleet-backups) before running `docker compose down`. The volume bytes still leave the node when compose tears down the named volumes; the snapshot only preserves the YAML so you can redeploy it elsewhere.
- **Evict and destroy data** runs the eviction without a snapshot. Type the blueprint name to confirm.
- Manual **Evict and destroy** / **Snapshot, then evict** require a current approved remove outcome for that node, and the node must no longer be desired. Confirm a remove rollout after the node leaves the selector first; destructive Evict while the node is still desired, or without remove approval, returns a stale-guard error. Plain stateless **Withdraw** does not require remove approval.
<Frame caption="Stateful eviction dialog. Snapshot, then evict captures the compose YAML to Fleet · Snapshots; Evict and destroy data requires typing the blueprint name to confirm.">
<img src="/images/blueprint-model/eviction-dialog.png" alt="Stateful eviction dialog with Snapshot then evict and Evict and destroy data options" />
</Frame>
<Frame caption="Stateless eviction dialog. A single Withdraw deployment action; no data prompt because nothing persistent was detected.">
<img src="/images/blueprint-model/eviction-stateless.png" alt="Stateless eviction dialog with a single Withdraw deployment action" />
</Frame>
<Note>
**Snapshot, then evict** captures the compose definition only. Volume bytes are not shipped. The named volumes managed by this stack on the target node are removed by `docker compose down` just as with **Evict and destroy data**. To preserve data, capture volumes manually before withdrawing (see *Migrating stateful data between nodes* below).
</Note>
### Delete the blueprint
Click **Delete** on the detail sheet, then type the blueprint's name to confirm. Stateless deployments, and any deployment that never made it past **Awaiting confirmation**, are withdrawn for you as part of the delete. A stateful or state-unknown blueprint with a deployment that is live on a node refuses to delete; withdraw that deployment from the deployment table first, so you choose the snapshot-or-destroy path for its data, then delete the blueprint.
## Federation: pin a blueprint to a single node
Admins can pin a blueprint to a specific node from **Fleet · Federation**. A pinned blueprint deploys only to its pinned node, regardless of the configured selector, and overrides the cordon flag on that node. The Blueprint detail sheet shows a read-only `Pin` section when a pin is in place; pin management itself lives in the Federation tab.
<Frame caption="Fleet · Federation, Blueprints subsection. Each row shows the blueprint, its configured selector, the Pinned to dropdown, and the effective placement that the reconciler will use.">
<img src="/images/blueprint-model/federation-pin.png" alt="Federation tab pin policy table with one blueprint pinned and one unpinned" />
</Frame>
See [Fleet Federation](/features/fleet-federation) for the cordon and pin model.
## Cordoned nodes
Cordoning a node prevents the reconciler from selecting it for **new** placements and skips state-review provisioning on it. Existing deployments on a cordoned node keep running, and drift checks continue normally. Pin policy overrides cordon, so a blueprint pinned to a cordoned node still deploys.
## Notifications
Two notification events fire from the reconciler:
- `blueprint_drift_detected` (warning severity), in Observe and Suggest modes when drift is detected. In Observe the row updates silently in the table; in Suggest the notification is also dispatched through your notification routes.
- `blueprint_drift_correction_failed` (error severity), in Enforce mode when an auto-fix attempt fails.
Both events route through the standard alert pipeline. Configure delivery channels under [Notification routing](/features/alerts-notifications#notification-routing).
## Security and trust boundaries
**Who can do what.** The user role determines the available actions. Reading the catalog, the detail sheet, and the deployment status is open to every role. Creating, editing, withdrawing, accepting a stateful deploy, and applying on demand require the admin role. Pinning a blueprint requires the admin role.
**The marker file is the trust root.** The reconciler will only deploy into, modify, or withdraw a directory that carries a `.blueprint.json` marker whose blueprint ID matches. A pre-existing directory with no marker, or a marker referencing a different blueprint, surfaces as **Name conflict** and is never modified.
**Local vs remote policy enforcement.** Local blueprint deploys evaluate the pre-deploy vulnerability policy gate against the local scanner state and credentials before the compose file is written to disk. Remote blueprint deploys are routed through the remote node's standard `/api/stacks` and `/api/stacks/<name>/deploy` endpoints over the proxy, so the remote node enforces its own policy with its own scanner state. The controlling instance does not bypass remote policy.
**Remote-node call path.** Sencho's controlling instance reaches remote nodes through the same authenticated proxy used by the rest of the fleet management surfaces, carrying the remote node's bearer token. Remote nodes do not need to accept any inbound connection beyond the one they already accept for fleet operations.
**Audit.** Blueprint create, edit, delete, pin, withdraw, and accept are administrative actions and are recorded through the standard [Audit log](/features/audit-log) pipeline along with the rest of the admin surface.
## Limitations and non-goals
By design, Blueprints do not include:
- A distributed storage layer (no CSI, no Longhorn-style replication).
- Automatic volume migration between nodes.
- Per-node parameter overrides or templating (one compose, all nodes).
- Staged or canary rollouts.
- Versioning history with one-click rollback. To revert, paste the prior compose into the editor and save.
Concrete operational constraints:
- **Reconciler cadence.** The tick interval is 60 seconds (5-second initial delay after startup). Approved blueprints reconcile on that cadence. Use **Apply now** and **Confirm Apply** after a change that clears approval.
- **Compose size.** YAML must be 96 KiB or fewer. Split very large compose files into smaller blueprints, or move generated content out of the compose body.
- **Selector size.** A selector accepts up to 200 entries per side (200 `nodes.ids`, or up to 200 each in `labels.any` and `labels.all`).
- **Name.** 1 to 64 characters matching `^[a-z0-9][a-z0-9_-]*$`. Rename is allowed only when every deployment is withdrawn (or there are none). Live placements and guards must be resolved first; rename clears approval back to pending.
- **Snapshot semantics.** **Snapshot, then evict** captures the compose YAML only. Volume bytes are removed by `docker compose down` just as with **Evict and destroy data**.
- **Restore from snapshot.** Reserved for the future Volume Migration feature and currently disabled in the state review dialog.
These omissions keep Blueprints honest: a compose-native fleet primitive that distributes the file you already have to the nodes you choose.
## Migrating stateful data between nodes (manual)
Sencho's compose-native lane does not include automatic volume shipping. **Snapshot, then evict** is a compose-only safety net: it preserves the YAML so you can redeploy elsewhere, but it does not move data. To relocate a stateful Blueprint's data from node A to node B, do it by hand before withdrawing:
1. Stop the Blueprint deployment on node A from the deployment table. Use **Snapshot, then evict** so the compose YAML is parked in [Fleet · Snapshots](/features/fleet-backups) while you handle volumes.
2. Use your host tooling (`docker run --rm -v <volume>:/data busybox tar -czf - /data > snapshot.tar.gz`, or app-aware tooling such as `pg_basebackup`, `mysqldump`, or `mongodump`) to capture the volume on node A.
3. Transfer the artifact to node B and restore it into the named volume there.
4. Update the Blueprint's selector to include node B; click **Apply now**, review the preview, and **Confirm Apply**.
A future Volume Migration feature will automate this with app-aware backup tooling.
## Practical workflows
**Multi-node identical reverse proxy.** A stateless `caddy-edge` blueprint with a labels selector matching `any=[edge]` and drift mode **Enforce**. Adding a new edge node deploys the proxy automatically within one tick. Editing the compose to bump the Caddy image redeploys every edge node on the next tick without operator intervention. Drift caused by a manual `docker compose down` on one edge node is corrected silently within a minute.
**Single-node managed Postgres.** A stateful `pg-fleet` blueprint with a `nodes` selector pointing at one database node and drift mode **Suggest**. The first deploy enters **Awaiting confirmation** so the operator chooses **Deploy fresh**. Subsequent compose changes (image bump, config change) re-enter **Awaiting confirmation** on the same node so the operator can decide whether the new revision is safe for the existing volume. Drift on the running container fires a `blueprint_drift_detected` notification but never auto-redeploys.
**Pin a blueprint to a specific node despite the selector.** Open **Fleet · Federation**, find the blueprint in the pin policy table, and pick the target node from the **Pinned to** dropdown. The pin saves immediately but also clears approval, so nothing moves until you open the blueprint here in Deployments, click **Apply now**, and **Confirm Apply**. Once confirmed, the pin overrides the selector for that blueprint, deploys only to the pinned node, and also overrides cordon on that node. Useful for relocating a stateful service to a specific host without rewriting the selector. Clear the pin (and confirm again) to restore selector-driven placement.
**Observe-only audit blueprint.** A stateless monitoring stack (Vector, Promtail, a Prometheus exporter) with drift mode **Observe**. Drift is recorded silently in the deployment table; no notification fires and no auto-fix runs. Useful when you want Sencho to track placement and detect divergence on a low-signal stack without paging anyone.
## Troubleshooting
<AccordionGroup>
<Accordion title="A deployment row shows 'Name conflict' and refuses to deploy">
A directory by the blueprint's name already exists on that node and does not carry the `.blueprint.json` marker. The most likely cause is a manually created stack with the same name. Resolution: rename either the existing stack or the blueprint, then click **Apply now** on the detail sheet to retry.
</Accordion>
<Accordion title="A stateful deployment is stuck in 'Awaiting confirmation'">
The reconciler will not auto-deploy a stateful blueprint to a node it has never run on. Click **Confirm deploy** on the row, then choose **Deploy fresh** in the dialog. Sencho will create empty named volumes and start the stack.
</Accordion>
<Accordion title="A deploy was blocked by vulnerability policy">
Open the **Security** page and review the active scan policies for the target node on the **Policies** tab. The Blueprint deployment row records the blocking policy and affected image count. Either fix the image, relax the policy, or deploy through an explicit admin-approved bypass on the stack surface. Blueprints do not silently bypass deploy enforcement.
</Accordion>
<Accordion title="Compose content was rejected on save">
Sencho accepts valid YAML up to 96 KiB. Split very large compose files into smaller blueprints or move generated content out of the Blueprint. Do not paste secrets into the compose body; use environment files or [Fleet Secrets](/features/fleet-secrets) where appropriate.
</Accordion>
<Accordion title="A remote node is offline or disconnected during apply">
The row moves to **Failed** with the remote error. Reconnect the node (see [Pilot Agent](/features/pilot-agent) and [Multi-node management](/features/multi-node)), verify whether the stack directory contains `docker-compose.yml` and `.blueprint.json`, then click **Apply now**. If the directory exists without a matching marker, Sencho treats it as a name conflict until you rename or remove the remote stack manually.
</Accordion>
<Accordion title="A Docker daemon or registry failure surfaced during apply">
The deployment row moves to **Failed** and records the Docker or registry error. Resolve the daemon, socket, registry credentials, rate limit, disk, or volume-permission issue on the affected node, then click **Apply now**. Watch that node's stack activity and security scan status after retry.
</Accordion>
<Accordion title="Drift is reported but never gets corrected">
Confirm the drift policy is **Enforce** and that **Reconciler enabled** is on. Open the detail sheet to check the row's status and the most recent drift summary. If the row is sitting in **Awaiting confirmation** instead of **Drifted**, that is expected: a compose edit on a stateful or state-unknown blueprint never auto-corrects under any drift mode, including Enforce. It always waits for an explicit **Confirm deploy** so a human signs off before the new revision touches that node's data. Enforce's auto-fix only applies to container-level drift (a stopped or crashed container) on a deployment that is already **Active**.
</Accordion>
<Accordion title="'Apply now' is greyed out">
**Apply now** requires the blueprint to be enabled. Open the detail sheet, click **Enable** to flip the reconciler back on, then click **Apply now**. The button is also disabled while the detail sheet is in edit mode; save or cancel the edit first.
</Accordion>
<Accordion title="Disabling a blueprint is rejected">
Blueprints with active or drifted deployments refuse to disable, because doing so would orphan the running containers without a follow-up plan. Withdraw each affected deployment first, then disable the blueprint.
</Accordion>
<Accordion title="The data is gone after 'Snapshot, then evict'">
Both eviction modes run `docker compose down`, which removes the named volumes managed by the stack on the target node. The snapshot stored in **Fleet · Snapshots** captures the compose definition only; the volume bytes are not included. To preserve data, capture the volume by hand before withdrawing (see *Migrating stateful data between nodes*). Bind mounts on the host filesystem are left in place by both eviction modes.
</Accordion>
<Accordion title="Eviction was aborted with 'Failed to capture compose snapshot before eviction'">
Sencho aborted the eviction because the pre-eviction compose snapshot could not be written to the database. The deployment is still in place; nothing was destroyed. Check that the database is reachable, then retry the eviction. If you accept data loss and want to evict regardless, use **Evict and destroy data** instead.
</Accordion>
<Accordion title="A pinned blueprint did not deploy to nodes in its selector">
By design. A pin replaces the selector entirely with the single pinned node, even if the selector resolves to other nodes. Either clear the pin in the Federation tab to restore selector-driven placement, or move the pin to a different node.
</Accordion>
<Accordion title="The reconciler took a minute to react to a change I just made">
The reconciler tick interval is one minute for approved blueprints. After editing the selector, the compose, or the drift policy, click **Apply now**, review the rollout preview, and **Confirm Apply**. The same path recovers a row in **Failed** status.
</Accordion>
</AccordionGroup>
## Common questions
<AccordionGroup>
<Accordion title="Do Blueprints replace my existing stacks?">
No. Stacks created in the per-stack lane (the **Stacks** sidebar) and Blueprints (the **Deployments** tab) coexist on every node. A Blueprint is identified by its `.blueprint.json` marker; the reconciler refuses to touch any directory that does not carry one with a matching blueprint ID, so user-authored stacks are safe from blueprint actions.
</Accordion>
<Accordion title="Is a Blueprint the same as a Stack?">
A Blueprint is a fleet-wide declaration: one compose YAML targeting a set of nodes. Each node where the blueprint resolves materializes as a Stack on that node, in the same compose directory layout the rest of the per-stack lane uses, with a `.blueprint.json` marker added. You can browse the materialized stack in the per-node Stacks view; the Blueprint is the source of truth that drives it.
</Accordion>
<Accordion title="How fast does drift get noticed?">
After you confirm the updated rollout, the next reconciler tick (or the Confirm Apply execution itself) places or removes as approved. The reconciler ticks every 60 seconds with a 5-second initial delay after startup. Within Enforce mode, drift correction for an already-approved place outcome begins on the same tick that detects the drift.
</Accordion>
<Accordion title="Can I roll back to a previous revision?">
Not through a one-click history. Blueprints intentionally do not keep a versioned revision history. To revert, paste the prior compose into the editor and save; the reconciler treats the change as a new revision and redeploys.
</Accordion>
<Accordion title="Does the snapshot in 'Snapshot, then evict' contain my database?">
No. The snapshot captures the compose YAML only, so you can redeploy the same definition elsewhere. Volume bytes (the database files, the cached state, the uploads directory) are removed by `docker compose down` along with the named volumes. To preserve data across an eviction, capture the volume by hand before withdrawing.
</Accordion>
<Accordion title="Why doesn't Sencho move volumes between nodes for me?">
Volume migration is app-aware. A Postgres datadir is not the same kind of artifact as a Redis RDB file or a MinIO bucket, and a generic `tar` of the volume is rarely safe while the container is running. A future Volume Migration feature will integrate app-aware backup tooling. Until then, the manual workflow in *Migrating stateful data between nodes* is the supported path.
</Accordion>
<Accordion title="If I cordon a node, does that uninstall the Blueprints on it?">
No. Cordoning only blocks new placements and skips state-review provisioning. Existing deployments on the cordoned node keep running and continue drift-checking. To uninstall a deployment from a cordoned node, withdraw it explicitly from the deployment table.
</Accordion>
<Accordion title="Can a Pilot-attached node host a Blueprint deployment?">
Yes. The reconciler dispatches deploys to remote nodes through the standard proxy regardless of whether the remote runs in Pilot Agent or Distributed API Proxy mode. The remote node enforces its own vulnerability policy and writes its own marker file, the same way a local deploy does.
</Accordion>
</AccordionGroup>
## Where Blueprints fits
<CardGroup cols={2}>
<Card title="Fleet Federation" icon="network-wired" href="/features/fleet-federation">
Cordon nodes to block new placements and pin blueprints to a specific node to override the selector.
</Card>
<Card title="Fleet Snapshots" icon="camera" href="/features/fleet-backups">
Where Snapshot-then-evict parks the compose YAML and where you redeploy it from.
</Card>
<Card title="Fleet Actions" icon="bolt" href="/features/fleet-actions">
Bulk operations on the running fleet. Blueprints sit on the desired-state lane; Fleet Actions sit on the imperative lane.
</Card>
<Card title="Atomic Deployments" icon="shield-check" href="/features/atomic-deployments">
Deploy safety on the per-stack lane. The same vulnerability policy gate runs before each Blueprint deploy.
</Card>
<Card title="Pilot Agent" icon="link" href="/features/pilot-agent">
Outbound-only remote mode. A Pilot-attached node is eligible as a Blueprint target the same way a Distributed API Proxy node is.
</Card>
<Card title="Alerts & Notifications" icon="bell" href="/features/alerts-notifications">
Where `blueprint_drift_detected` and `blueprint_drift_correction_failed` route. Configure channels per notification.
</Card>
</CardGroup>