{
  "available": true,
  "status": "proposed",
  "note": "Which bytes belong on which tier, what each tier costs when it fills, and which copies of them survive the house. Occupancy, headroom and the date a tier runs out are derived from what is placed on it — never declared — and the one rule that fails the build rather than raising a finding is that live secrets do not go anywhere that replicates.",
  "policy": {
    "maxMonthlyEur": 0,
    "minHeadroomMonths": 6,
    "integrityMaxAgeDays": 45,
    "restoreTestMaxAgeDays": 180,
    "observationMaxAgeDays": 90,
    "quotaWarnPct": 75,
    "secretPatterns": [
      ".env",
      "*.env",
      "config.env",
      "auth.json",
      "credentials*",
      "*.key",
      "*.pem",
      "id_rsa",
      "id_ed25519",
      "*.kdbx",
      ".netrc",
      "*.pgpass"
    ],
    "notes": [
      "Three roles, and a byte belongs to exactly one of them at a time. Hot storage is local SSD: worktrees, live databases, service state — small, fast, and expensive per gigabyte, so nothing lives there because it was convenient. Bulk is the NAS pool: artifacts, caches and the on-site backups, where size is cheap and the room is a single failure domain. Off-site is GWSCLI/S3: the encrypted subset that has to survive the house, and the only tier with a bill attached to it.",
      "Occupancy is derived from what is placed on a tier, not declared. A tier that says it is 40% full while carrying six datasets nobody sized is a number that will be believed at exactly the wrong moment. Where a size has never been measured the register says so, and the arithmetic that rests on it is marked estimated.",
      "Disk pressure is a schedule, not an alarm. The interesting number is not 'how full is it' but 'how long until it is full at the rate it is filling', so headroom is derived in months and the thresholds exist to say what gets deleted first when that runs out.",
      "The cheapest gigabyte is the one that is never stored. Anything reproducible — build caches, container layers, worktrees — is kept on-site and rebuilt rather than replicated, because paying to move bytes off-site that a `git clone` and a build would recreate is buying a slower version of something we already have.",
      "Live secrets never enter replicated storage. Not encrypted, not excluded-by-convention, not 'the bucket is private': the credential material stays on the node that uses it and in the operator's password manager, and the register refuses to describe an estate where that is not true.",
      "Dedupe and compression are measurements, not adjectives. A dataset claiming a ratio it has never measured is claiming free capacity that will not be there when the tier fills."
    ]
  },
  "summary": {
    "tiers": 4,
    "datasets": 9,
    "flows": 3,
    "flowsRunning": 2,
    "level": "ok",
    "withHeadroom": 3,
    "totalGb": 8598,
    "usedGbToday": 5693.841,
    "reclaimableGb": 5308.027,
    "monthlyEur": 0,
    "backups": 2,
    "backupsOffsiteToday": 2,
    "backupsTested": 1,
    "backupsOffsiteTested": 0,
    "secretsExposed": 0,
    "measured": 3,
    "placements": 21,
    "gaps": 12,
    "accepted": 1,
    "notes": 31
  },
  "tiers": [
    {
      "id": "t-local-vin",
      "name": "VIN local SSD",
      "role": "hot",
      "media": "nvme-ssd",
      "node": "vin",
      "entityId": "vin",
      "mount": "/",
      "purpose": "The edge node's own disk: worker worktrees, service state, the container images it serves from, and the credentials it uses. Small, fast, and the only storage that keeps working when the house is dark.",
      "allows": [
        "worktree",
        "state",
        "artifact",
        "cache",
        "secret"
      ],
      "secretsAllowed": true,
      "replicated": false,
      "status": "unknown",
      "totalGb": 38,
      "baselineGb": 8.7,
      "capacitySource": "df -P / run on the node itself on 2026-08-17: 38 GB total, 8.9 GB used, 27 GB available. The baseline is that reading minus the declared paths that are actually present — the OS, the package cache and everything nobody declared. This is the only tier in the register with a reading rather than a guess, and where the reading and the arithmetic disagree the register raises a finding rather than adjusting the baseline until they agree.",
      "design": {
        "usedGb": 38.84,
        "usedPct": 102.21052631578948,
        "level": "crit",
        "monthsToWarn": 0
      },
      "today": {
        "usedGb": 14.84,
        "usedPct": 39.05263157894737,
        "freeGb": 23.16,
        "growthGbPerMonth": 0.8,
        "level": "ok",
        "monthsToWarn": 17.075,
        "monthsToCrit": 24.200000000000003,
        "reclaimableGb": 6.027,
        "estimated": true,
        "measuredUsedGb": 8.9,
        "driftGb": 5.9399999999999995,
        "placements": [
          {
            "status": "running",
            "observedOn": "2026-08-17",
            "tier": "t-local-vin",
            "path": "/var/lib/jarvisos/workspaces",
            "sizeGb": 0.027,
            "growthGbPerMonth": 0.1,
            "measured": true,
            "note": "du -sk on the node: 27.5 MB across the live workspaces. Small because worktrees are pruned, and the number is worth re-reading whenever that stops being true.",
            "datasetId": "worktrees",
            "datasetName": "Active task worktrees",
            "class": "worktree",
            "reproducible": true
          },
          {
            "status": "running",
            "observedOn": "2026-08-17",
            "tier": "t-local-vin",
            "path": "/var/lib/jarvisos",
            "sizeGb": 0.112,
            "growthGbPerMonth": 0.2,
            "measured": true,
            "note": "du -sk on the node: 115 MB of journal, ledger and snapshot directory. The queue that will dominate this path is bounded at 2 GB by the data register, and that bound — not today's 115 MB — is the ceiling this placement has to leave room for.",
            "datasetId": "jarvisos-state",
            "datasetName": "JarvisOS service state",
            "class": "state",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": "2026-08-17",
            "tier": "t-local-vin",
            "path": "/etc/jarvisos",
            "sizeGb": 0.001,
            "growthGbPerMonth": 0,
            "measured": true,
            "note": "du -sk on the node: 27 KB, carried here as 1 MB because that is the smallest unit this register bothers with. Referred to by name and location; no value from this path appears in this repository or in any report it produces.",
            "datasetId": "host-secrets",
            "datasetName": "Host credential material",
            "class": "secret",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-vin",
            "path": "/var/lib/docker/overlay2",
            "sizeGb": 6,
            "growthGbPerMonth": 0.5,
            "measured": false,
            "note": "Estimated. Fewer workloads, and a much smaller disk to hold them on.",
            "datasetId": "container-images",
            "datasetName": "Container images",
            "class": "artifact",
            "reproducible": true
          }
        ]
      },
      "pressure": {
        "warnPct": 75,
        "critPct": 90,
        "metricEntity": "vin",
        "metricKey": "disk_root",
        "onWarn": [
          "Prune the worker's finished worktrees older than the retention window.",
          "docker image prune for layers no running container references."
        ],
        "onCrit": [
          "Stop accepting new dispatched work: a worker that cannot write its journal must refuse rather than half-run a task.",
          "Escalate. There is nothing else on this node that is safe for automation to delete."
        ],
        "evictionOrder": [
          "worktrees",
          "container-images"
        ]
      },
      "features": {
        "dedupe": false,
        "compression": "none",
        "snapshots": false,
        "checksums": false
      },
      "integrity": null,
      "cost": {
        "model": "included",
        "monthlyEur": 0,
        "quotaGb": null,
        "note": "The VPS plan's disk. Growing it means changing plan, which is a bill and a resize, not a purchase."
      }
    },
    {
      "id": "t-local-home1",
      "name": "Home1 local SSD",
      "role": "hot",
      "media": "nvme-ssd",
      "node": "home1",
      "entityId": "home1",
      "mount": "/",
      "purpose": "The primary node's own disk: the Postgres data directory, Coolify's stack and build cache, container images and service state. Everything the estate writes to first.",
      "allows": [
        "worktree",
        "state",
        "artifact",
        "cache",
        "secret",
        "backup"
      ],
      "secretsAllowed": true,
      "replicated": false,
      "status": "unknown",
      "totalGb": 460,
      "baselineGb": 8,
      "capacitySource": "Estimated from the node's build, not read. Nothing in this repository has an execution path to home1, so this number is a claim until `jarvisos-storage.mjs report` is run there.",
      "design": {
        "usedGb": 126.001,
        "usedPct": 27.391521739130436,
        "level": "ok",
        "monthsToWarn": 24.333222222222222
      },
      "today": {
        "usedGb": 126.001,
        "usedPct": 27.391521739130436,
        "freeGb": 333.999,
        "growthGbPerMonth": 9,
        "level": "ok",
        "monthsToWarn": 24.333222222222222,
        "monthsToCrit": 31.99988888888889,
        "reclaimableGb": 102,
        "estimated": true,
        "measuredUsedGb": null,
        "driftGb": null,
        "placements": [
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/jarvisos/workspaces",
            "sizeGb": 2,
            "growthGbPerMonth": 0.2,
            "measured": false,
            "note": "Estimated. Home1 also builds here, so its worktrees carry build output that VIN's do not.",
            "datasetId": "worktrees",
            "datasetName": "Active task worktrees",
            "class": "worktree",
            "reproducible": true
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/jarvisos",
            "sizeGb": 1,
            "growthGbPerMonth": 0.2,
            "measured": false,
            "note": "Estimated.",
            "datasetId": "jarvisos-state",
            "datasetName": "JarvisOS service state",
            "class": "state",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/etc/jarvisos",
            "sizeGb": 0.001,
            "growthGbPerMonth": 0,
            "measured": false,
            "note": "Same shape on the primary.",
            "datasetId": "host-secrets",
            "datasetName": "Host credential material",
            "class": "secret",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/postgresql/16/main",
            "sizeGb": 12,
            "growthGbPerMonth": 1.5,
            "measured": false,
            "note": "Estimated, and the estimate that matters most on this tier: pg_wal lives inside it, and a stalled archive turns growth into an outage.",
            "datasetId": "postgres-data",
            "datasetName": "Postgres data directories",
            "class": "state",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/coolify/backups",
            "sizeGb": 3,
            "growthGbPerMonth": 0.1,
            "measured": false,
            "note": "Coolify's pre-upgrade dump, on the machine it is protecting. It survives a bad upgrade and nothing else, which is what it was written for — but it occupies the hot tier and nothing prunes or verifies it, so it is accounted for here rather than left to nobody.",
            "datasetId": "config-archive",
            "datasetName": "Configuration and estate archive",
            "class": "backup",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/docker/buildkit",
            "sizeGb": 60,
            "growthGbPerMonth": 5,
            "measured": false,
            "note": "Estimated, and the single largest reclaimable thing on the primary. First in the eviction order for that reason.",
            "datasetId": "artifact-cache",
            "datasetName": "Build and package cache",
            "class": "cache",
            "reproducible": true
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-local-home1",
            "path": "/var/lib/docker/overlay2",
            "sizeGb": 40,
            "growthGbPerMonth": 2,
            "measured": false,
            "note": "Estimated.",
            "datasetId": "container-images",
            "datasetName": "Container images",
            "class": "artifact",
            "reproducible": true
          }
        ]
      },
      "pressure": {
        "warnPct": 75,
        "critPct": 90,
        "metricEntity": "home1",
        "metricKey": "disk_root",
        "onWarn": [
          "Prune the Coolify build cache and dangling image layers.",
          "Confirm the WAL archive is draining to the NAS: a stalled archive_command turns a full pg_wal into an outage of the primary."
        ],
        "onCrit": [
          "A full disk under the Postgres primary is data loss, not a capacity problem. Stop deploys first, then prune caches, then escalate.",
          "Never delete out of pg_wal by hand. The archive is what removes those files, and removing them another way removes the recovery point with them."
        ],
        "evictionOrder": [
          "artifact-cache",
          "container-images",
          "worktrees"
        ]
      },
      "features": {
        "dedupe": false,
        "compression": "none",
        "snapshots": false,
        "checksums": false
      },
      "integrity": null,
      "cost": {
        "model": "owned-hardware",
        "monthlyEur": 0,
        "quotaGb": null,
        "note": "Already bought. Replacing the disk is capital, not recurring."
      }
    },
    {
      "id": "t-nas-pool",
      "name": "NAS storage pool",
      "role": "bulk",
      "media": "spinning-raidz",
      "node": "nas",
      "entityId": "nas-backups",
      "mount": "/mnt/pool",
      "purpose": "Bulk on-site storage: the on-site backups, the shared artifact and image cache, and the media library. Cheap per gigabyte, redundant against a dead drive, and in the same room as everything it protects.",
      "allows": [
        "artifact",
        "cache",
        "backup",
        "media",
        "state"
      ],
      "secretsAllowed": false,
      "replicated": true,
      "status": "unknown",
      "totalGb": 8000,
      "baselineGb": 40,
      "capacitySource": "Estimated usable capacity after parity. Not read from the pool — `zpool list` runs on the NAS and nothing here has a path to it.",
      "design": {
        "usedGb": 5788,
        "usedPct": 72.35000000000001,
        "level": "ok",
        "monthsToWarn": 12.439024390243901
      },
      "today": {
        "usedGb": 5488,
        "usedPct": 68.60000000000001,
        "freeGb": 2512,
        "growthGbPerMonth": 44.2,
        "level": "ok",
        "monthsToWarn": 20.633484162895925,
        "monthsToCrit": 42.35294117647059,
        "reclaimableGb": 5200,
        "estimated": true,
        "measuredUsedGb": null,
        "driftGb": null,
        "placements": [
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-nas-pool",
            "path": "/mnt/pool/backups",
            "sizeGb": 240,
            "growthGbPerMonth": 4,
            "measured": false,
            "note": "Estimated: thirty days of base backups plus the WAL between them, and the nightly dumps. The WAL is the part that grows with write volume rather than with database size.",
            "datasetId": "db-backups",
            "datasetName": "Database backups and WAL archive",
            "class": "backup",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-nas-pool",
            "path": "/mnt/pool/backups/config",
            "sizeGb": 8,
            "growthGbPerMonth": 0.2,
            "measured": false,
            "note": "Estimated. Dominated by Coolify's dump rather than by the configuration files.",
            "datasetId": "config-archive",
            "datasetName": "Configuration and estate archive",
            "class": "backup",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-nas-pool",
            "path": "/mnt/pool/media",
            "sizeGb": 5200,
            "growthGbPerMonth": 40,
            "measured": false,
            "note": "Estimated, and by far the largest thing in the estate. It is also the fastest-growing, which makes it the dataset that decides when the pool needs drives.",
            "datasetId": "media",
            "datasetName": "Media library",
            "class": "media",
            "reproducible": true
          }
        ]
      },
      "pressure": {
        "warnPct": 80,
        "critPct": 92,
        "metricEntity": "nas",
        "metricKey": "disk_pool",
        "onWarn": [
          "Prune the shared artifact cache to its retention window; it is rebuilt on the next build.",
          "Check the media dataset's growth rate before touching anything backup-shaped."
        ],
        "onCrit": [
          "Never prune a backup dataset to make room. Delete reproducible bytes — cache, images, media — or add capacity.",
          "A pool that cannot accept a WAL segment stops protecting the primary, which is the failure this tier exists to prevent."
        ],
        "evictionOrder": [
          "artifact-cache",
          "media"
        ]
      },
      "features": {
        "dedupe": false,
        "compression": "zstd",
        "snapshots": true,
        "checksums": true
      },
      "integrity": {
        "method": "zpool scrub of the whole pool",
        "intervalDays": 30,
        "lastRunOn": "2026-08-02",
        "outcome": "pass",
        "note": "Reads every allocated block and compares it against the stored checksum. It is the only thing that finds a bit that rotted quietly, and a degraded drive found by a scrub is a replacement rather than a restore."
      },
      "cost": {
        "model": "owned-hardware",
        "monthlyEur": 0,
        "quotaGb": null,
        "note": "Drives are bought once. The recurring cost of this tier is electricity, not storage."
      }
    },
    {
      "id": "t-offsite-gwscli",
      "name": "GWSCLI / S3 off-site",
      "role": "offsite",
      "media": "object-store",
      "node": null,
      "entityId": "gwscli-s3-offsite",
      "mount": null,
      "purpose": "The encrypted copy that survives losing the house. Deliberately the smallest tier: only what cannot be reconstructed goes here, because this is the one tier where a gigabyte has a price and a quota.",
      "allows": [
        "backup",
        "state"
      ],
      "secretsAllowed": false,
      "replicated": true,
      "status": "unknown",
      "totalGb": 100,
      "baselineGb": 0,
      "capacitySource": "The free quota, not a measurement. `restic stats` needs the repository password, which is deliberately on no host here, so the occupancy below is the sum of what is pushed rather than what the provider reports.",
      "design": {
        "usedGb": 77,
        "usedPct": 77,
        "level": "warn",
        "monthsToWarn": 0
      },
      "today": {
        "usedGb": 65,
        "usedPct": 65,
        "freeGb": 35,
        "growthGbPerMonth": 2.2,
        "level": "ok",
        "monthsToWarn": 4.545454545454545,
        "monthsToCrit": 11.363636363636363,
        "reclaimableGb": 0,
        "estimated": true,
        "measuredUsedGb": null,
        "driftGb": null,
        "placements": [
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-offsite-gwscli",
            "path": "restic:jarvisos/backups",
            "sizeGb": 62,
            "growthGbPerMonth": 2,
            "measured": false,
            "note": "Smaller than the on-site copy by design and by dedupe: the off-site retention is shorter, and restic stores one copy of the blocks the daily dumps share.",
            "datasetId": "db-backups",
            "datasetName": "Database backups and WAL archive",
            "class": "backup",
            "reproducible": false
          },
          {
            "status": "running",
            "observedOn": null,
            "tier": "t-offsite-gwscli",
            "path": "restic:jarvisos/config",
            "sizeGb": 3,
            "growthGbPerMonth": 0.2,
            "measured": false,
            "note": "Estimated, after dedupe. Configuration barely changes between snapshots, which is exactly the shape restic is good at.",
            "datasetId": "config-archive",
            "datasetName": "Configuration and estate archive",
            "class": "backup",
            "reproducible": false
          }
        ]
      },
      "pressure": {
        "warnPct": 75,
        "critPct": 90,
        "metricEntity": "gwscli-s3",
        "metricKey": "quota",
        "onWarn": [
          "Shorten the retention on the off-site copy before adding capacity: the on-site tier keeps the long tail, and off-site only has to cover losing the house.",
          "Confirm nothing reproducible has crept into a flow. Off-site is for what cannot be rebuilt."
        ],
        "onCrit": [
          "A full off-site repository stops accepting the copy that survives the site while everything on-site still looks healthy. Treat it as a protection outage, not a storage one.",
          "Paying for capacity here is a legitimate answer; it is a decision with a monthly cost and belongs in the register, not in an incident."
        ],
        "evictionOrder": []
      },
      "features": {
        "dedupe": true,
        "compression": "zstd",
        "snapshots": true,
        "checksums": true
      },
      "integrity": {
        "method": "restic check --read-data-subset=5%",
        "intervalDays": 30,
        "lastRunOn": null,
        "outcome": null,
        "note": "Not yet run by anybody. A `restic check` without --read-data verifies the index and the structure; the subset form actually reads and re-hashes a sample of the packs, which is the only way to notice that the provider has quietly lost one."
      },
      "cost": {
        "model": "free-tier",
        "monthlyEur": 0,
        "quotaGb": 100,
        "note": "Free up to the quota. This tier is the estate's only recurring-cost risk, and it becomes one silently: the bill arrives because a retention window nobody revisited kept growing."
      }
    }
  ],
  "datasets": [
    {
      "id": "worktrees",
      "name": "Active task worktrees",
      "class": "worktree",
      "owner": "jarvisos-worker",
      "purpose": "Where dispatched work actually happens: one checkout per task, plus whatever the task builds while it runs. Every byte of it is either committed and pushed, or it was never wanted.",
      "secrets": "none",
      "scrubbedBy": null,
      "reproducible": true,
      "mustSurviveSite": false,
      "recovery": {
        "method": "regenerate",
        "procedure": "The control plane re-dispatches the task; the worker clones the repository again.",
        "cost": "The in-flight task restarts. Tasks are at-least-once by design, so this is the normal path rather than a recovery."
      },
      "protectedBy": null,
      "holdsCopies": [],
      "retention": {
        "mode": "age",
        "keepDays": 14,
        "prunedBy": "jarvisos-worker on task completion, and a weekly sweep for anything an interrupted run left behind",
        "enforcedBy": "The sweep is what actually enforces this. A worktree whose task died is exactly the one nothing deletes on the happy path.",
        "note": "Fourteen days is long enough to look at what a failed task left and short enough that nobody has to."
      },
      "retentionDays": 14,
      "dedupe": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": null,
      "design": {
        "sizeGb": 2.027,
        "growthGbPerMonth": 0.30000000000000004,
        "hasOffsite": false
      },
      "today": {
        "sizeGb": 2.027,
        "growthGbPerMonth": 0.30000000000000004,
        "hasOffsite": false,
        "tiers": [
          "t-local-vin",
          "t-local-home1"
        ]
      },
      "placements": [
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/var/lib/jarvisos/workspaces",
          "sizeGb": 0.027,
          "growthGbPerMonth": 0.1,
          "status": "running",
          "measured": true,
          "observedOn": "2026-08-17",
          "domain": "fd-vps",
          "replicated": false
        },
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/jarvisos/workspaces",
          "sizeGb": 2,
          "growthGbPerMonth": 0.2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "jarvisos-state",
      "name": "JarvisOS service state",
      "class": "state",
      "owner": "jarvisos-control-plane",
      "purpose": "The console's durable state on each node: the incident journal, the remediation ledger, discovery snapshots and — on VIN — the local-first queue that keeps the worker working while the house is dark.",
      "secrets": "none",
      "scrubbedBy": "The journal records incident text and entity ids. Node identities are hashes and live in the secrets dataset, not here.",
      "reproducible": false,
      "mustSurviveSite": true,
      "recovery": {
        "method": "restore",
        "procedure": "Restore the newest snapshot from the off-site copy onto the node, then let the worker drain its queue before it takes new work.",
        "cost": "Anything written since the last push is gone. For the journal that is history; for the VIN queue it is finished work that has not reached Postgres, which is why that copy is the one with the shortest interval."
      },
      "protectedBy": {
        "register": "datastores",
        "store": "jarvisos-vin-journal"
      },
      "holdsCopies": [
        "journal-offsite-s3"
      ],
      "retention": {
        "mode": "age",
        "keepDays": 90,
        "prunedBy": "restic forget --keep-daily 7 --keep-weekly 8 --keep-monthly 3 on the off-site copy; the local journal is bounded by the data register instead",
        "enforcedBy": "A prune that never runs is a retention policy that does not exist. The flow runs it after every push and its exit status is the evidence.",
        "note": "Ninety days of state costs almost nothing and is the difference between reconstructing an incident and remembering it."
      },
      "retentionDays": 90,
      "dedupe": {
        "enabled": true,
        "mechanism": "restic content-defined chunking",
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": true,
        "mechanism": "zstd inside restic",
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": {
        "method": "SQLite PRAGMA integrity_check on the queue before each push, plus restic's own pack hashes",
        "intervalDays": 1,
        "lastRunOn": null,
        "outcome": null,
        "note": "Nothing runs this yet: the flow that would carry it is planned. Pushing a corrupt queue file every 15 minutes would replicate the corruption faithfully."
      },
      "design": {
        "sizeGb": 3.112,
        "growthGbPerMonth": 0.6000000000000001,
        "hasOffsite": true
      },
      "today": {
        "sizeGb": 1.112,
        "growthGbPerMonth": 0.4,
        "hasOffsite": false,
        "tiers": [
          "t-local-vin",
          "t-local-home1"
        ]
      },
      "placements": [
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/var/lib/jarvisos",
          "sizeGb": 0.112,
          "growthGbPerMonth": 0.2,
          "status": "running",
          "measured": true,
          "observedOn": "2026-08-17",
          "domain": "fd-vps",
          "replicated": false
        },
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/jarvisos",
          "sizeGb": 1,
          "growthGbPerMonth": 0.2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        },
        {
          "tier": "t-offsite-gwscli",
          "role": "offsite",
          "path": "jarvisos/state",
          "sizeGb": 2,
          "growthGbPerMonth": 0.2,
          "status": "planned",
          "measured": false,
          "observedOn": null,
          "domain": "fd-offsite",
          "replicated": true
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "host-secrets",
      "name": "Host credential material",
      "class": "secret",
      "owner": "homelab-operator",
      "purpose": "The live credentials each node needs to be itself: the operator env file, node identity material, SSH host keys and the WireGuard private key. Named here so the rule about them is enforceable, never copied here.",
      "secrets": "live",
      "scrubbedBy": null,
      "reproducible": false,
      "mustSurviveSite": false,
      "recovery": {
        "method": "reissue",
        "procedure": "Nothing is restored. Each credential is re-issued from its own authority: the OAuth client secret from the provider console, node identities by re-enrolling the node, host and tunnel keys by regenerating them and re-pinning the peer. The operator's password manager holds the off-line copies of what cannot be regenerated, including the off-site repository passphrase.",
        "cost": "Every session is signed out, the tunnel needs a new peer key on both ends, and any node whose identity was re-issued must re-enrol before it can report. Minutes of work, not data loss."
      },
      "protectedBy": null,
      "holdsCopies": [],
      "retention": {
        "mode": "none",
        "keepDays": null,
        "prunedBy": "Rotation, not retention. A superseded credential is revoked at its authority and deleted, never archived.",
        "enforcedBy": "Nothing automated may touch this dataset. That is the point.",
        "note": "An archived secret is a live secret with a longer lifetime."
      },
      "retentionDays": null,
      "dedupe": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": null,
      "design": {
        "sizeGb": 0.002,
        "growthGbPerMonth": 0,
        "hasOffsite": false
      },
      "today": {
        "sizeGb": 0.002,
        "growthGbPerMonth": 0,
        "hasOffsite": false,
        "tiers": [
          "t-local-vin",
          "t-local-home1"
        ]
      },
      "placements": [
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/etc/jarvisos",
          "sizeGb": 0.001,
          "growthGbPerMonth": 0,
          "status": "running",
          "measured": true,
          "observedOn": "2026-08-17",
          "domain": "fd-vps",
          "replicated": false
        },
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/etc/jarvisos",
          "sizeGb": 0.001,
          "growthGbPerMonth": 0,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "postgres-data",
      "name": "Postgres data directories",
      "class": "state",
      "owner": "postgres-primary",
      "purpose": "The live data directory of the primary on home1 and of the streaming replica on VIN. This is the estate's system of record while it is being written to.",
      "secrets": "none",
      "scrubbedBy": null,
      "reproducible": false,
      "mustSurviveSite": false,
      "recovery": {
        "method": "restore",
        "procedure": "Owned by the data register: promote the replica, or point-in-time restore from the base backup plus the WAL archive on the NAS. Nothing here duplicates those steps.",
        "cost": "Stated by the data register, in the recovery point it derives from the copies."
      },
      "protectedBy": {
        "register": "datastores",
        "store": "jarvisos-postgres"
      },
      "holdsCopies": [
        "pg-offsite-vin"
      ],
      "retention": {
        "mode": "gfs",
        "keepDays": 30,
        "keep": {
          "daily": 7,
          "weekly": 4,
          "monthly": 1
        },
        "prunedBy": "Two different things, deliberately. The live directories are pruned by Postgres itself through checkpointing and the archive, and nothing else may delete from them. The off-site snapshots taken from the replica are pruned by `restic forget --prune` on the VIN unit.",
        "enforcedBy": "archive_command success is what allows WAL to be recycled: if it fails, the live directory grows until the disk is full, which is why this dataset appears in the primary's pressure response. The off-site prune runs inside the push and fails it if it fails.",
        "note": "Thirty days off-site to match what the data register requires of the store. On the live directories, deleting a file to free space is a data-loss event rather than a retention decision."
      },
      "retentionDays": 30,
      "dedupe": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": null,
      "design": {
        "sizeGb": 34,
        "growthGbPerMonth": 3.5,
        "hasOffsite": true
      },
      "today": {
        "sizeGb": 12,
        "growthGbPerMonth": 1.5,
        "hasOffsite": false,
        "tiers": [
          "t-local-home1"
        ]
      },
      "placements": [
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/postgresql/16/main",
          "sizeGb": 12,
          "growthGbPerMonth": 1.5,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        },
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/var/lib/postgresql/16/main",
          "sizeGb": 12,
          "growthGbPerMonth": 1.5,
          "status": "planned",
          "measured": false,
          "observedOn": "2026-08-17",
          "domain": "fd-vps",
          "replicated": false
        },
        {
          "tier": "t-offsite-gwscli",
          "role": "offsite",
          "path": "restic:jarvisos/pg-vin",
          "sizeGb": 10,
          "growthGbPerMonth": 0.5,
          "status": "planned",
          "measured": false,
          "observedOn": null,
          "domain": "fd-offsite",
          "replicated": true
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "db-backups",
      "name": "Database backups and WAL archive",
      "class": "backup",
      "owner": "postgres-primary",
      "purpose": "Everything the data register calls a copy that lands on storage: the WAL archive, the base backups, the Supabase dump and — once running — the Coolify dump. The bytes a restore actually reads.",
      "secrets": "redacted",
      "scrubbedBy": "The dumps are database content, and the connection strings that produce them are read from /etc/jarvisos/config.env at run time and never written into the archive. The flow that pushes this dataset off-site excludes every secret path pattern in the policy.",
      "reproducible": false,
      "mustSurviveSite": true,
      "recovery": {
        "method": "restore",
        "procedure": "Restore from the off-site repository onto whichever node survives, then follow the restore steps in the data register for the store being recovered.",
        "cost": "Bounded by the copy interval, which the data register derives. What this register adds is the time to pull the bytes back over a link that was sized for uploading them slowly."
      },
      "protectedBy": null,
      "holdsCopies": [
        "pg-wal-nas",
        "pg-base-nas",
        "supabase-dump-nas",
        "pg-offsite-home",
        "supabase-dump-offsite"
      ],
      "retention": {
        "mode": "gfs",
        "keepDays": 30,
        "keep": {
          "daily": 7,
          "weekly": 5,
          "monthly": 6
        },
        "prunedBy": "restic forget --prune off-site; a dated-directory sweep on the NAS",
        "enforcedBy": "The prune runs inside the same flow as the push and fails it if it fails. A retention window enforced by a separate timer is a window that stops being enforced the first time that timer breaks.",
        "note": "Thirty days on-site covers the failure everybody plans for. Six monthly points off-site cover the one nobody does: a corruption that was faithfully replicated for weeks before anyone noticed."
      },
      "retentionDays": 180,
      "dedupe": {
        "enabled": true,
        "mechanism": "restic content-defined chunking across snapshots",
        "measuredRatio": null,
        "measuredOn": null,
        "note": "Expected to be worth several times its cost on daily dumps of a slowly changing database, and never measured. `restic stats --mode raw-data` against the repository is the measurement, and it needs the passphrase."
      },
      "compression": {
        "enabled": true,
        "mechanism": "zstd in restic, and zstd on the NAS dataset",
        "measuredRatio": null,
        "measuredOn": null,
        "note": "Postgres dumps and WAL compress well; the ratio is unmeasured on this estate."
      },
      "integrity": {
        "method": "pg_verifybackup against the newest base backup, and pg_restore --list against each dump",
        "intervalDays": 7,
        "lastRunOn": "2026-08-10",
        "outcome": "pass",
        "note": "Proves the archive parses and the backup is internally consistent. It does not prove anyone can restore it — that is what the restore test below is for, and the two are routinely confused."
      },
      "design": {
        "sizeGb": 302,
        "growthGbPerMonth": 6,
        "hasOffsite": true
      },
      "today": {
        "sizeGb": 302,
        "growthGbPerMonth": 6,
        "hasOffsite": true,
        "tiers": [
          "t-nas-pool",
          "t-offsite-gwscli"
        ]
      },
      "placements": [
        {
          "tier": "t-nas-pool",
          "role": "bulk",
          "path": "/mnt/pool/backups",
          "sizeGb": 240,
          "growthGbPerMonth": 4,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": true
        },
        {
          "tier": "t-offsite-gwscli",
          "role": "offsite",
          "path": "restic:jarvisos/backups",
          "sizeGb": 62,
          "growthGbPerMonth": 2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-offsite",
          "replicated": true
        }
      ],
      "restoreTests": [
        {
          "date": "2026-07-12",
          "from": "t-nas-pool",
          "outcome": "pass",
          "measuredMinutes": 22,
          "sampleGb": 6.2,
          "restoredTo": "scratch Postgres instance on home1, port 5433",
          "note": "Point-in-time restore to 24h earlier from the base backup plus the WAL archive. 22 minutes wall clock, of which 14 was replay. Restored from the copy in the same room as the primary."
        }
      ],
      "lastTest": {
        "date": "2026-07-12",
        "from": "t-nas-pool",
        "outcome": "pass",
        "measuredMinutes": 22,
        "sampleGb": 6.2,
        "restoredTo": "scratch Postgres instance on home1, port 5433",
        "note": "Point-in-time restore to 24h earlier from the base backup plus the WAL archive. 22 minutes wall clock, of which 14 was replay. Restored from the copy in the same room as the primary."
      },
      "offsiteTested": false
    },
    {
      "id": "config-archive",
      "name": "Configuration and estate archive",
      "class": "backup",
      "owner": "jarvisos-control-plane",
      "purpose": "The small, unglamorous bytes that make a rebuild possible: Coolify's dump, the node configuration under /etc, discovery snapshots and the estate registers as they were on the day. Losing it does not lose data — it loses the ability to put the estate back the way it was.",
      "secrets": "redacted",
      "scrubbedBy": "The collector copies /etc/jarvisos/*.json and the unit files, and excludes every pattern in policy.secretPatterns before the archive is written. The env file is named in the manifest and its contents are not in the archive.",
      "reproducible": false,
      "mustSurviveSite": true,
      "recovery": {
        "method": "restore",
        "procedure": "Pull the newest archive, restore the configuration into place on a rebuilt node, then re-issue the credentials it references rather than looking for them in the archive.",
        "cost": "The credentials are not in it, so a restore is followed by a rotation. That is the trade this dataset makes on purpose."
      },
      "protectedBy": null,
      "holdsCopies": [
        "coolify-dump-local",
        "coolify-dump-nas",
        "coolify-dump-offsite"
      ],
      "retention": {
        "mode": "gfs",
        "keepDays": 90,
        "keep": {
          "daily": 14,
          "weekly": 8,
          "monthly": 12
        },
        "prunedBy": "restic forget --prune, in the same flow that writes it",
        "enforcedBy": "Same flow, same exit status.",
        "note": "A year of monthly points, because the question this dataset answers is usually 'what did this look like before the change nobody remembers making'."
      },
      "retentionDays": 360,
      "dedupe": {
        "enabled": true,
        "mechanism": "restic content-defined chunking",
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": true,
        "mechanism": "zstd inside restic",
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": {
        "method": "restic check on the config paths, plus pg_restore --list of the Coolify dump",
        "intervalDays": 30,
        "lastRunOn": null,
        "outcome": null,
        "note": "Never run. The Coolify dump is the estate's single lifecycle point and nothing has ever confirmed the archive of it parses."
      },
      "design": {
        "sizeGb": 14,
        "growthGbPerMonth": 0.5,
        "hasOffsite": true
      },
      "today": {
        "sizeGb": 14,
        "growthGbPerMonth": 0.5,
        "hasOffsite": true,
        "tiers": [
          "t-local-home1",
          "t-nas-pool",
          "t-offsite-gwscli"
        ]
      },
      "placements": [
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/coolify/backups",
          "sizeGb": 3,
          "growthGbPerMonth": 0.1,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        },
        {
          "tier": "t-nas-pool",
          "role": "bulk",
          "path": "/mnt/pool/backups/config",
          "sizeGb": 8,
          "growthGbPerMonth": 0.2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": true
        },
        {
          "tier": "t-offsite-gwscli",
          "role": "offsite",
          "path": "restic:jarvisos/config",
          "sizeGb": 3,
          "growthGbPerMonth": 0.2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-offsite",
          "replicated": true
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "artifact-cache",
      "name": "Build and package cache",
      "class": "cache",
      "owner": "coolify",
      "purpose": "Docker build layers, the npm and pip stores, and whatever else a build reads twice. Pure speed: every byte is reconstructible by running the build again.",
      "secrets": "redacted",
      "scrubbedBy": "Build arguments are the classic way a token ends up in a cached layer. Builds take secrets through Coolify's environment at run time rather than as build args, and this dataset is in no flow that leaves a node, so a leak here stays on the node that made it.",
      "reproducible": true,
      "mustSurviveSite": false,
      "recovery": {
        "method": "regenerate",
        "procedure": "Delete it. The next build repopulates what it needs.",
        "cost": "One slow build per project. Nothing else."
      },
      "protectedBy": null,
      "holdsCopies": [],
      "retention": {
        "mode": "age",
        "keepDays": 30,
        "prunedBy": "docker buildx prune --filter until=720h, weekly",
        "enforcedBy": "The weekly timer, and the tier's own pressure response, which reaches for this dataset first.",
        "note": "A cache with no eviction is not a cache, it is a disk leak with good intentions."
      },
      "retentionDays": 30,
      "dedupe": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": true,
        "mechanism": "zstd on the NAS dataset",
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": null,
      "design": {
        "sizeGb": 372,
        "growthGbPerMonth": 11,
        "hasOffsite": false
      },
      "today": {
        "sizeGb": 60,
        "growthGbPerMonth": 5,
        "hasOffsite": false,
        "tiers": [
          "t-local-home1"
        ]
      },
      "placements": [
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/docker/buildkit",
          "sizeGb": 60,
          "growthGbPerMonth": 5,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        },
        {
          "tier": "t-nas-pool",
          "role": "bulk",
          "path": "/mnt/pool/cache",
          "sizeGb": 300,
          "growthGbPerMonth": 5,
          "status": "planned",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": true
        },
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/var/lib/minio/jarvisos-artifacts",
          "sizeGb": 12,
          "growthGbPerMonth": 1,
          "status": "planned",
          "measured": false,
          "observedOn": null,
          "domain": "fd-vps",
          "replicated": false
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "container-images",
      "name": "Container images",
      "class": "artifact",
      "owner": "coolify",
      "purpose": "The image layers each node needs to start what it runs. Reproducible from the registry, but not instantly: a node with no images and no uplink starts nothing.",
      "secrets": "none",
      "scrubbedBy": null,
      "reproducible": true,
      "mustSurviveSite": false,
      "recovery": {
        "method": "regenerate",
        "procedure": "docker pull, or a rebuild from the source repository if the registry no longer has the tag.",
        "cost": "Bandwidth and minutes — unless the registry is unreachable, in which case a node that pruned its images cannot start what it was already running."
      },
      "protectedBy": null,
      "holdsCopies": [],
      "retention": {
        "mode": "age",
        "keepDays": 60,
        "prunedBy": "docker image prune --filter until=1440h, weekly, dangling and unreferenced layers only",
        "enforcedBy": "The weekly timer. Pruning by reference rather than by age is what keeps the rollback target on disk.",
        "note": "Sixty days keeps the previous release pullable locally, which is what a rollback needs when the registry is the thing that is down."
      },
      "retentionDays": 60,
      "dedupe": {
        "enabled": true,
        "mechanism": "overlay2 layer sharing between images",
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "integrity": null,
      "design": {
        "sizeGb": 46,
        "growthGbPerMonth": 2.5,
        "hasOffsite": false
      },
      "today": {
        "sizeGb": 46,
        "growthGbPerMonth": 2.5,
        "hasOffsite": false,
        "tiers": [
          "t-local-home1",
          "t-local-vin"
        ]
      },
      "placements": [
        {
          "tier": "t-local-home1",
          "role": "hot",
          "path": "/var/lib/docker/overlay2",
          "sizeGb": 40,
          "growthGbPerMonth": 2,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": false
        },
        {
          "tier": "t-local-vin",
          "role": "hot",
          "path": "/var/lib/docker/overlay2",
          "sizeGb": 6,
          "growthGbPerMonth": 0.5,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-vps",
          "replicated": false
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    },
    {
      "id": "media",
      "name": "Media library",
      "class": "media",
      "owner": "homelab-operator",
      "purpose": "Bulk media. Large, replaceable, and deliberately not off-sited — the inventory already says so, and this register says what that costs in capacity.",
      "secrets": "none",
      "scrubbedBy": null,
      "reproducible": true,
      "mustSurviveSite": false,
      "recovery": {
        "method": "regenerate",
        "procedure": "Re-acquired from source. There is no restore path and none is wanted.",
        "cost": "Time and effort, no data that matters. This is a policy decision recorded in the inventory, not an oversight."
      },
      "protectedBy": null,
      "holdsCopies": [],
      "retention": {
        "mode": "none",
        "keepDays": null,
        "prunedBy": "The operator, by hand, when the pool needs room.",
        "enforcedBy": "Nothing automated. Deleting media on a threshold would be automation making a taste decision.",
        "note": "Last in the eviction order rather than absent from it: when the pool is critical and the alternative is pruning backups, media is what goes."
      },
      "retentionDays": null,
      "dedupe": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null
      },
      "compression": {
        "enabled": false,
        "mechanism": null,
        "measuredRatio": null,
        "measuredOn": null,
        "note": "Media is already compressed. Compressing it again spends CPU to save nothing."
      },
      "integrity": null,
      "design": {
        "sizeGb": 5200,
        "growthGbPerMonth": 40,
        "hasOffsite": false
      },
      "today": {
        "sizeGb": 5200,
        "growthGbPerMonth": 40,
        "hasOffsite": false,
        "tiers": [
          "t-nas-pool"
        ]
      },
      "placements": [
        {
          "tier": "t-nas-pool",
          "role": "bulk",
          "path": "/mnt/pool/media",
          "sizeGb": 5200,
          "growthGbPerMonth": 40,
          "status": "running",
          "measured": false,
          "observedOn": null,
          "domain": "fd-home-rack",
          "replicated": true
        }
      ],
      "restoreTests": [],
      "lastTest": null,
      "offsiteTested": false
    }
  ],
  "flows": [
    {
      "id": "f-home-to-nas",
      "name": "Home1 to the NAS backup dataset",
      "from": "t-local-home1",
      "to": "t-nas-pool",
      "datasets": [
        "db-backups",
        "config-archive"
      ],
      "mechanism": "archive_command, pg_basebackup and pg_dump writing to the NAS export over the rack switch",
      "selection": "produced",
      "runsOn": "home1",
      "schedule": "WAL continuously, base backups every 6h, dumps nightly at 01:00",
      "intervalMinutes": 5,
      "encryption": {
        "inTransit": "none",
        "atRest": "dataset-level",
        "keyLocation": null,
        "keyOnSourceNode": false,
        "note": "Plaintext over the rack switch between two hosts in one room, on a LAN with no other tenants. Encrypting this hop would protect against an attacker who is already inside the failure domain both ends live in."
      },
      "excludes": [],
      "dedupe": false,
      "compression": true,
      "verifiedBy": "archive_command exit status, a size floor on each base backup, and the weekly pg_verifybackup",
      "status": "running",
      "leavesEstate": false
    },
    {
      "id": "f-nas-to-offsite",
      "name": "NAS to the off-site repository",
      "from": "t-nas-pool",
      "to": "t-offsite-gwscli",
      "datasets": [
        "db-backups",
        "config-archive"
      ],
      "mechanism": "restic backup to the S3-compatible endpoint, followed by forget --prune",
      "selection": "paths",
      "runsOn": "nas",
      "schedule": "nightly at 02:30",
      "intervalMinutes": 1440,
      "encryption": {
        "inTransit": "tls",
        "atRest": "client-side",
        "keyLocation": "operator password manager, off-line; deliberately on no host in this inventory",
        "keyOnSourceNode": false,
        "note": "Client-side means the provider stores ciphertext it cannot read, and an account compromise is an availability problem rather than a disclosure. The cost is symmetrical: losing the passphrase loses the copy."
      },
      "excludes": [
        ".env",
        "*.env",
        "config.env",
        "auth.json",
        "credentials*",
        "*.key",
        "*.pem",
        "id_rsa",
        "id_ed25519",
        "*.kdbx",
        ".netrc",
        "*.pgpass"
      ],
      "dedupe": true,
      "compression": true,
      "verifiedBy": "restic snapshot list after each run, and the monthly restore test that this register tracks",
      "status": "running",
      "leavesEstate": true
    },
    {
      "id": "f-vin-to-offsite",
      "name": "VIN to the off-site repository",
      "from": "t-local-vin",
      "to": "t-offsite-gwscli",
      "datasets": [
        "jarvisos-state",
        "postgres-data"
      ],
      "mechanism": "restic backup from the edge node straight to the provider",
      "selection": "paths",
      "runsOn": "vin",
      "schedule": "every 15 minutes",
      "intervalMinutes": 15,
      "encryption": {
        "inTransit": "tls",
        "atRest": "client-side",
        "keyLocation": "operator password manager, off-line; the same repository as the NAS flow, with a node-scoped path",
        "keyOnSourceNode": false,
        "note": "Same repository, same passphrase discipline. The node holds the object-store credential — which can only write — and never the passphrase."
      },
      "excludes": [
        ".env",
        "*.env",
        "config.env",
        "auth.json",
        "credentials*",
        "*.key",
        "*.pem",
        "id_rsa",
        "id_ed25519",
        "*.kdbx",
        ".netrc",
        "*.pgpass"
      ],
      "dedupe": true,
      "compression": true,
      "verifiedBy": "Nothing yet. Once running: restic snapshot list from the VIN unit, and inclusion in the quarterly restore test.",
      "status": "planned",
      "leavesEstate": true
    }
  ],
  "findings": [
    {
      "code": "occupancy-disagrees-with-measurement",
      "severity": "gap",
      "title": "What the register places on a tier and what the disk reports are different.",
      "tierId": "t-local-vin",
      "datasetId": null,
      "flowId": null,
      "detail": "VIN local SSD: this register places 14.8 GB here and the tier itself reported 8.9 GB used on 2026-08-17 — 5.9 GB more on paper than on the disk. Something declared as running here is not, or is far smaller than declared. A capacity plan built on it is planning for a different machine.",
      "acceptance": null
    },
    {
      "code": "tier-integrity-undeclared",
      "severity": "gap",
      "title": "A tier holds backups and declares no integrity check.",
      "tierId": "t-local-home1",
      "datasetId": null,
      "flowId": null,
      "detail": "Home1 local SSD holds backups and declares no integrity check, so nothing here would notice a rotted block.",
      "acceptance": null
    },
    {
      "code": "headroom-below-policy",
      "severity": "gap",
      "title": "A tier reaches its warn threshold sooner than policy allows.",
      "tierId": "t-offsite-gwscli",
      "datasetId": null,
      "flowId": null,
      "detail": "GWSCLI / S3 off-site reaches its 75% line in about 4.5 months at 2.2 GB/month; policy wants 6 months of warning. Most of that growth is Database backups and WAL archive (2 GB/month). The projection rests on sizes nobody has measured.",
      "acceptance": null
    },
    {
      "code": "integrity-never-run",
      "severity": "gap",
      "title": "An integrity check has never been run.",
      "tierId": "t-offsite-gwscli",
      "datasetId": null,
      "flowId": null,
      "detail": "GWSCLI / S3 off-site: \"restic check --read-data-subset=5%\" has never been run, so nothing has ever confirmed these bytes are readable.",
      "acceptance": null
    },
    {
      "code": "no-offsite-placement",
      "severity": "gap",
      "title": "A dataset that must survive the site has no copy outside it.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state is declared as having to survive the site and has no running copy outside it. Planned: t-offsite-gwscli.",
      "acceptance": null
    },
    {
      "code": "restore-test-never",
      "severity": "gap",
      "title": "Nobody has restored anything from this dataset.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "Nobody has ever restored anything from JarvisOS service state.",
      "acceptance": null
    },
    {
      "code": "offsite-restore-never-tested",
      "severity": "gap",
      "title": "Every restore test read from a tier that does not survive the site.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "Every restore test of JarvisOS service state read from nowhere. The restore that matters reads from the off-site tier onto a node with nothing at home reachable, and it has never been run.",
      "acceptance": null
    },
    {
      "code": "integrity-never-run",
      "severity": "gap",
      "title": "An integrity check has never been run.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state: \"SQLite PRAGMA integrity_check on the queue before each push, plus restic's own pack hashes\" has never been run, so nothing has ever confirmed these bytes are readable.",
      "acceptance": null
    },
    {
      "code": "offsite-restore-never-tested",
      "severity": "gap",
      "title": "Every restore test read from a tier that does not survive the site.",
      "tierId": null,
      "datasetId": "db-backups",
      "flowId": null,
      "detail": "Every restore test of Database backups and WAL archive read from t-nas-pool. The restore that matters reads from the off-site tier onto a node with nothing at home reachable, and it has never been run.",
      "acceptance": null
    },
    {
      "code": "restore-test-never",
      "severity": "gap",
      "title": "Nobody has restored anything from this dataset.",
      "tierId": null,
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Nobody has ever restored anything from Configuration and estate archive.",
      "acceptance": null
    },
    {
      "code": "offsite-restore-never-tested",
      "severity": "gap",
      "title": "Every restore test read from a tier that does not survive the site.",
      "tierId": null,
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Every restore test of Configuration and estate archive read from nowhere. The restore that matters reads from the off-site tier onto a node with nothing at home reachable, and it has never been run.",
      "acceptance": null
    },
    {
      "code": "integrity-never-run",
      "severity": "gap",
      "title": "An integrity check has never been run.",
      "tierId": null,
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive: \"restic check on the config paths, plus pg_restore --list of the Coolify dump\" has never been run, so nothing has ever confirmed these bytes are readable.",
      "acceptance": null
    },
    {
      "code": "cache-without-eviction",
      "severity": "accepted",
      "title": "A reclaimable dataset has no retention, so it only ever grows.",
      "tierId": null,
      "datasetId": "media",
      "flowId": null,
      "detail": "Media library is reproducible and has no retention, so it only ever grows on every tier it is on.",
      "acceptance": {
        "reason": "Deleting media on a threshold would be automation making a taste decision. It is last in the pool's eviction order instead, which is the honest version: when the choice is between pruning media and pruning backups, media goes — by hand, and with somebody deciding which of it. Off-siting 5.2 TB is separately declined: it would cost more per month than everything else in this estate combined, to protect bytes whose loss costs effort rather than information.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-06-01"
      }
    },
    {
      "code": "occupancy-estimated",
      "severity": "note",
      "title": "A tier’s occupancy rests on sizes nobody has measured.",
      "tierId": "t-local-vin",
      "datasetId": null,
      "flowId": null,
      "detail": "VIN local SSD is 39.1% full by arithmetic that includes 1 unmeasured size(s): Container images. Run `jarvisos-storage.mjs report` on vin to replace the guess.",
      "acceptance": null
    },
    {
      "code": "occupancy-estimated",
      "severity": "note",
      "title": "A tier’s occupancy rests on sizes nobody has measured.",
      "tierId": "t-local-home1",
      "datasetId": null,
      "flowId": null,
      "detail": "Home1 local SSD is 27.4% full by arithmetic that includes 7 unmeasured size(s): Active task worktrees, JarvisOS service state, Host credential material, Postgres data directories, Configuration and estate archive, Build and package cache, Container images. Run `jarvisos-storage.mjs report` on home1 to replace the guess.",
      "acceptance": null
    },
    {
      "code": "occupancy-estimated",
      "severity": "note",
      "title": "A tier’s occupancy rests on sizes nobody has measured.",
      "tierId": "t-nas-pool",
      "datasetId": null,
      "flowId": null,
      "detail": "NAS storage pool is 68.6% full by arithmetic that includes 3 unmeasured size(s): Database backups and WAL archive, Configuration and estate archive, Media library. Run `jarvisos-storage.mjs report` on nas to replace the guess.",
      "acceptance": null
    },
    {
      "code": "occupancy-estimated",
      "severity": "note",
      "title": "A tier’s occupancy rests on sizes nobody has measured.",
      "tierId": "t-offsite-gwscli",
      "datasetId": null,
      "flowId": null,
      "detail": "GWSCLI / S3 off-site is 65% full by arithmetic that includes 2 unmeasured size(s): Database backups and WAL archive, Configuration and estate archive. Run `jarvisos-storage.mjs report` on the node holding it to replace the guess.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "worktrees",
      "flowId": null,
      "detail": "Active task worktrees on Home1 local SSD is estimated at 2 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state counts on dedupe (restic content-defined chunking) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state counts on compression (zstd inside restic) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state on Home1 local SSD is estimated at 1 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-offsite-gwscli",
      "datasetId": "jarvisos-state",
      "flowId": null,
      "detail": "JarvisOS service state on GWSCLI / S3 off-site is estimated at 2 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "host-secrets",
      "flowId": null,
      "detail": "Host credential material on Home1 local SSD is estimated at 1 MB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "postgres-data",
      "flowId": null,
      "detail": "Postgres data directories on Home1 local SSD is estimated at 12 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-vin",
      "datasetId": "postgres-data",
      "flowId": null,
      "detail": "Postgres data directories on VIN local SSD is estimated at 12 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-offsite-gwscli",
      "datasetId": "postgres-data",
      "flowId": null,
      "detail": "Postgres data directories on GWSCLI / S3 off-site is estimated at 10 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "db-backups",
      "flowId": null,
      "detail": "Database backups and WAL archive counts on dedupe (restic content-defined chunking across snapshots) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "db-backups",
      "flowId": null,
      "detail": "Database backups and WAL archive counts on compression (zstd in restic, and zstd on the NAS dataset) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-nas-pool",
      "datasetId": "db-backups",
      "flowId": null,
      "detail": "Database backups and WAL archive on NAS storage pool is estimated at 240 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-offsite-gwscli",
      "datasetId": "db-backups",
      "flowId": null,
      "detail": "Database backups and WAL archive on GWSCLI / S3 off-site is estimated at 62 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive counts on dedupe (restic content-defined chunking) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive counts on compression (zstd inside restic) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive on Home1 local SSD is estimated at 3 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-nas-pool",
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive on NAS storage pool is estimated at 8 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-offsite-gwscli",
      "datasetId": "config-archive",
      "flowId": null,
      "detail": "Configuration and estate archive on GWSCLI / S3 off-site is estimated at 3 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "artifact-cache",
      "flowId": null,
      "detail": "Build and package cache counts on compression (zstd on the NAS dataset) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "artifact-cache",
      "flowId": null,
      "detail": "Build and package cache on Home1 local SSD is estimated at 60 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-nas-pool",
      "datasetId": "artifact-cache",
      "flowId": null,
      "detail": "Build and package cache on NAS storage pool is estimated at 300 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-vin",
      "datasetId": "artifact-cache",
      "flowId": null,
      "detail": "Build and package cache on VIN local SSD is estimated at 12 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "dedupe-unmeasured",
      "severity": "note",
      "title": "A dataset claims dedupe or compression it has never measured.",
      "tierId": null,
      "datasetId": "container-images",
      "flowId": null,
      "detail": "Container images counts on dedupe (overlay2 layer sharing between images) that nobody has measured, so the capacity it saves is assumed.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-home1",
      "datasetId": "container-images",
      "flowId": null,
      "detail": "Container images on Home1 local SSD is estimated at 40 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-local-vin",
      "datasetId": "container-images",
      "flowId": null,
      "detail": "Container images on VIN local SSD is estimated at 6 GB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "size-never-measured",
      "severity": "note",
      "title": "A placement’s size has never been measured.",
      "tierId": "t-nas-pool",
      "datasetId": "media",
      "flowId": null,
      "detail": "Media library on NAS storage pool is estimated at 5.08 TB; nobody has read it.",
      "acceptance": null
    },
    {
      "code": "flow-not-running",
      "severity": "note",
      "title": "A declared flow is not running yet, so it moves nothing today.",
      "tierId": "t-offsite-gwscli",
      "datasetId": null,
      "flowId": "f-vin-to-offsite",
      "detail": "\"VIN to the off-site repository\" is planned: jarvisos-state, postgres-data do not reach GWSCLI / S3 off-site today.",
      "acceptance": null
    }
  ],
  "source": "/app/config/storage.json"
}