let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-041", title = "backup-manager lifted out as cloudatasave: standalone backup orchestrator peer project", status = 'Accepted, date = "2026-05-01", context = "provisioning/platform/crates/backup-manager implemented backup orchestration — restic-first snapshots, multi-destination replication, consistency-point group management — inside the provisioning workspace. Two structural problems emerged: (1) backup orchestration domain (snapshot lifecycle, engine abstraction, restore verification, retention policy) is orthogonal to provisioning's core domain (workspace lifecycle management); (2) the implementation was coupled to provisioning's platform_config crate for config loading, preventing standalone use. Any project needing backup operations (workspace CI, scripts, standalone invocation) had to depend on the full provisioning binary. The component passed all four criteria of the ontoref lift-out pattern (ontoref ADR-016): orthogonal concern, consumer plurality, release cadence divergence, config path-agnostic.", decision = "backup-manager is extracted from provisioning as cloudatasave, a standalone backup orchestration project (Forgejo: LibreCloud/cloudDataSave). provisioning retains catalog/cloudatasave/ — the NCL schemas, defaults, and component declarations that allow workspace infras to declare BackupGroups and BackupPolicies. Workspace infras declare their backup intent using cloudatasave's NCL vocabulary; provisioning's runtime calls the cloudatasave binary with the generated policy config. No provisioning crate is imported by cloudatasave as a library dependency. backup-manager workspace member entry is removed from provisioning/platform/Cargo.toml.", rationale = [ { claim = "Backup orchestration evolves on a different cadence than workspace lifecycle", detail = "Adding a new backup destination type (SFTP, Storj, rsync.net), implementing kopia as a second engine, or improving restore verification scheduling should not require a provisioning release. cloudatasave's only coupling to provisioning is the BackupPolicy NCL vocabulary — the schema is stable across provisioning versions once published.", }, { claim = "Portable backup is a general capability, not a provisioning-specific one", detail = "Any project — not just provisioning workspaces — may need to declare backup groups, schedule snapshots, and verify restores. A cloudatasave that does not import provisioning infrastructure can be adopted by standalone scripts, CI pipelines, and other projects without inheriting provisioning's dependency tree.", }, { claim = "Verify-as-provisioning axiom requires cloudatasave to own verification state", detail = "The principle that a backup group is not provisioned until verified is a cloudatasave invariant. If backup-manager remained inside provisioning, this invariant would be implemented as a provisioning concern, creating tight coupling between provisioning's provisioning-state tracking and backup verification. As a standalone project, cloudatasave owns the invariant completely.", }, { claim = "catalog/cloudatasave/ is the correct integration surface", detail = "provisioning loads cloudatasave's BackupPolicy and BackupGroup schemas as an extension, making them available to workspace infra NCL. The workspace declares backup policy; provisioning validates it against cloudatasave's schema and passes it to the binary. This is the correct dependency direction: provisioning knows about cloudatasave's vocabulary, but cloudatasave does not know about provisioning's internal structures.", }, ], consequences = { positive = [ "cloudatasave releases independently: engine additions, verification improvements, destination types do not block provisioning", "Any project can adopt cloudatasave by declaring a BackupPolicy in its NCL vocabulary — not only provisioning workspaces", "provisioning/platform/Cargo.toml shrinks: backup-manager workspace member removed", "BackupEngine trait boundary is declared at project inception, forcing the engine abstraction to be correct from day one", ], negative = [ "Two schema maintenance surfaces: cloudatasave/schemas/ (source of truth) and provisioning/catalog/cloudatasave/ (consumer-side reference)", "Workspace infras that previously used backup_manager component definitions must migrate to cloudatasave's BackupPolicy schema", "cloudatasave must implement its own config loading without platform_config — one-time cost at extraction", ], }, alternatives_considered = [ { option = "Keep backup-manager in provisioning, expose as prvng backup subcommand only", why_rejected = "Prevents standalone invocation, CI pipeline integration without provisioning, and blocks the verify-as-provisioning model from being a cloudatasave-internal invariant. Four-criterion test (ontoref ADR-016) makes the extraction correct.", }, { option = "Use a managed Kubernetes backup solution (Velero)", why_rejected = "Velero targets in-cluster resource backup (PVCs, manifests). cloudatasave targets data backup: application snapshots, database dumps, object storage replication. These are complementary, not substitutes. cloudatasave's engine abstraction can later add a Velero-backend for PVC-class workloads.", }, ], constraints = [ { id = "extensions-not-binary-dep", claim = "provisioning must not import cloudatasave as a Rust library dependency; interaction is via CLI invocation with NCL-generated config", scope = "provisioning/platform/Cargo.toml, provisioning/catalog/", severity = 'Hard, check = { tag = 'Grep, pattern = "cloudatasave|backup-manager", paths = ["provisioning/platform/Cargo.toml"], must_be_empty = true, }, rationale = "Library dependency would re-couple provisioning's build cycle to cloudatasave's. The integration surface is the CLI binary + NCL schema, not the Rust crate graph.", }, { id = "minimum-two-destinations-enforced", claim = "provisioning's catalog/cloudatasave/ schema must declare minimum-two-destinations as a hard contract so workspace infras cannot declare single-destination groups", scope = "provisioning/catalog/cloudatasave/backup_group.ncl", severity = 'Hard, check = { tag = 'FileExists, path = "provisioning/catalog/cloudatasave/backup_group.ncl", present = true, }, rationale = "The multi-destination-custody axiom must be enforced at the workspace infra declaration layer, not only at cloudatasave runtime. Early validation prevents misconfigured groups from reaching the orchestrator.", }, ], related_adrs = ["adr-037-ops-contract-dual-mode", "ontoref:adr-016-component-lift-out-pattern"], ontology_check = { decision_string = "backup-manager extracted as cloudatasave standalone project; provisioning retains catalog/cloudatasave/ as integration surface; no Rust library dependency from provisioning to cloudatasave", invariants_at_risk = [], verdict = 'Safe, }, }