{
  "available": true,
  "status": "proposed",
  "note": "Where every store this estate cannot afford to lose is written, where its copies are, how each one is fed, and what a schema change does to all of it. Recovery points are derived from the copies, never declared, and they are measured against what is running today — a copy that is planned protects nothing yet.",
  "policy": {
    "copiesRequired": 3,
    "domainsRequired": 2,
    "offsiteRequired": true,
    "drillMaxAgeDays": 90,
    "quorumMinDomains": 3,
    "uplinkEntities": [
      "isp-uplink"
    ],
    "notes": [
      "Two recovery points are derived for every durable store, not one. The first is the best copy anywhere: it survives a dead disk in home1. The second is the best copy outside the primary's failure domain: it is the only one that survives the room. A WAL archive on the NAS is excellent at the first and worth nothing for the second.",
      "Off-site protection must not share one uplink. If every copy that leaves the estate is pushed over the home internet connection, then a home internet outage stops all off-site protection advancing while the primary keeps accepting writes — and nothing looks broken until the site is gone. The fix is a second off-site path that starts somewhere else, which is why the VPS pushes its own copy.",
      "This estate has two compute domains. A majority needs three, so nothing here promotes itself: every promotion is a human act with a fencing step, and automatic promotion of durable state is a finding rather than a feature.",
      "A restore nobody has run is a hypothesis. Drill evidence carries a date, an outcome and a measured duration, and it is the measurement that is checked against the RTO.",
      "Findings describe the estate as it runs. Where the design closes one, that appears as a note rather than as a clean bill of health."
    ]
  },
  "summary": {
    "stores": 5,
    "durable": 4,
    "copies": 11,
    "copiesRunning": 6,
    "meetingRpo": 3,
    "withOffsite": 4,
    "withOffsiteToday": 1,
    "drilled": 1,
    "uplinkDependent": 2,
    "gaps": 21,
    "accepted": 1,
    "notes": 6,
    "scenarios": 5,
    "cellsSurvivingTarget": 19,
    "cellsSurvivingCurrent": 19,
    "cellsTotal": 25,
    "migrations": 3,
    "migrationsReversible": 2
  },
  "paths": [
    {
      "id": "home-lan",
      "name": "Home LAN",
      "description": "home1 to nas over the rack switch. Crosses nothing that leaves the room, which is exactly why it protects against nothing that happens to the room.",
      "transits": [],
      "dependsOnUplink": false
    },
    {
      "id": "mesh",
      "name": "WireGuard mesh",
      "description": "home1 to vin over the tunnel. The tunnel depends on the ISP uplink, so anything sent this way stops when the home internet does — whether or not the copy says so.",
      "transits": [
        "wireguard-tunnel"
      ],
      "dependsOnUplink": true
    },
    {
      "id": "home-egress",
      "name": "Home uplink to the internet",
      "description": "home1 or nas pushing straight out to a provider. One connection, one router, one ISP.",
      "transits": [
        "isp-uplink"
      ],
      "dependsOnUplink": true
    },
    {
      "id": "vps-egress",
      "name": "VPS network to the internet",
      "description": "vin pushing straight out over the provider's network. Shares no equipment with the house, which is the entire reason it exists in this register.",
      "transits": [
        "vin"
      ],
      "dependsOnUplink": false
    },
    {
      "id": "vendor-api",
      "name": "Vendor API from home",
      "description": "home1 pulling from a provider's API over the home uplink.",
      "transits": [
        "isp-uplink"
      ],
      "dependsOnUplink": true
    }
  ],
  "stores": [
    {
      "id": "jarvisos-postgres",
      "name": "JarvisOS Postgres",
      "purpose": "Task state, the scheduler's leader lease, node identities and every record of what JarvisOS has done. Losing it does not degrade the estate, it forgets it.",
      "engine": "postgres",
      "durability": "durable",
      "criticality": "fleet-fatal",
      "writeModel": "single-writer",
      "entityId": "postgres-primary",
      "unmonitored": null,
      "proposed": false,
      "status": "unknown",
      "primary": {
        "node": "home1"
      },
      "homeDomain": "fd-home-rack",
      "domains": [
        "fd-vps",
        "fd-home-rack",
        "fd-offsite"
      ],
      "writers": [
        {
          "id": "jarvisos-control-plane",
          "node": "home1",
          "mode": "direct",
          "note": "The only process that opens this store. Everything else goes through it."
        },
        {
          "id": "jarvisos-worker",
          "node": "vin",
          "mode": "queued",
          "path": "mesh",
          "via": "jarvisos-vin-journal",
          "note": "Results and telemetry from VIN land in the local journal first and drain over the mesh. That is what keeps VIN working through a home internet outage instead of stalling on it."
        }
      ],
      "objectives": {
        "rpoMinutes": 15,
        "siteRpoMinutes": 60,
        "rtoMinutes": 25,
        "retentionDays": 30
      },
      "design": {
        "rpoMinutes": 0.5,
        "siteRpoMinutes": 0.5,
        "offsiteRpoMinutes": 360,
        "hasOffsite": true,
        "copyCount": 6
      },
      "today": {
        "rpoMinutes": 0.5,
        "siteRpoMinutes": 0.5,
        "hasOffsite": true,
        "copyCount": 5
      },
      "uplinkExposure": {
        "writes": false,
        "offsite": true,
        "offsiteIndependent": false,
        "offsiteIndependentPlanned": true
      },
      "copies": [
        {
          "id": "pg-replica-vin",
          "kind": "streaming-replica",
          "target": "postgres-replica",
          "node": "vin",
          "path": "mesh",
          "domain": "fd-vps",
          "offDomain": true,
          "mode": "continuous",
          "lagSec": 30,
          "intervalMinutes": null,
          "exposureMinutes": 0.5,
          "promotable": true,
          "restorable": false,
          "serves": "reads",
          "encrypted": true,
          "status": "running",
          "verifiedBy": "pg_stat_replication on home1, pulled by the telemetry loop; replay lag over 300s raises the replica's own health."
        },
        {
          "id": "pg-wal-nas",
          "kind": "wal-archive",
          "target": "nas-backups",
          "node": "nas",
          "path": "home-lan",
          "domain": "fd-home-rack",
          "offDomain": false,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 5,
          "exposureMinutes": 5,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "running",
          "verifiedBy": "archive_command exit status, plus a weekly pg_verifybackup against the newest base backup."
        },
        {
          "id": "pg-base-nas",
          "kind": "snapshot",
          "target": "nas-backups",
          "node": "nas",
          "path": "home-lan",
          "domain": "fd-home-rack",
          "offDomain": false,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 360,
          "exposureMinutes": 360,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "running",
          "verifiedBy": "pg_basebackup exit status and a size floor; the WAL archive above is what turns it into a point in time."
        },
        {
          "id": "pg-offsite-home",
          "kind": "snapshot",
          "target": "gwscli-s3-offsite",
          "node": "nas",
          "path": "home-egress",
          "domain": "fd-offsite",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "running",
          "verifiedBy": "restic snapshot list and a monthly restore drill; the repository password lives in the operator's password manager and on no host."
        },
        {
          "id": "pg-offsite-vin",
          "kind": "snapshot",
          "target": "gwscli-s3-offsite",
          "node": "vin",
          "path": "vps-egress",
          "domain": "fd-offsite",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 360,
          "exposureMinutes": 360,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "planned",
          "verifiedBy": "Nothing yet. Once running: restic snapshot list from the VIN unit, and inclusion in the quarterly drill."
        }
      ],
      "promotion": {
        "trigger": "manual",
        "from": "pg-replica-vin",
        "minutes": 20,
        "fencing": "home1 is fenced before the replica is touched: stop the JarvisOS control-plane unit, `systemctl stop postgresql`, and take the mesh route down from VIN. Only then does the replica get promoted. The old primary never returns as a primary — it is rebuilt as a replica of the new one.",
        "steps": [
          "Confirm home1 is genuinely gone, not merely slow: the mesh is down and the console shows no heartbeat for 3 intervals.",
          "Fence: from VIN, drop the mesh route to home1 so a returning primary cannot be written to.",
          "Record the replica's last replayed LSN in the incident. That number is the data loss.",
          "pg_ctl promote on postgres-replica; wait for it to leave recovery.",
          "Repoint the JarvisOS control-plane standby unit on VIN at the local Postgres and start it.",
          "Announce read-write on VIN in the incident, with the LSN and the loss window."
        ],
        "verify": "The console's own writes land: create and resolve a scratch incident, then confirm it survives a restart of the control-plane unit.",
        "dataLoss": "Everything committed on home1 and not yet replayed on the replica — 30s of writes at the declared lag, more if the mesh was already degraded when home1 died. The promotion records the LSN so the number is known rather than guessed.",
        "reprotect": "Redundancy is gone the moment the replica is promoted: there is one copy again. Within 24h, take a fresh base backup from the new primary on VIN to gwscli-s3-offsite, and rebuild home1 as a streaming replica of VIN when the site returns. The estate stays at reduced protection until both are done, and the incident stays open until then."
      },
      "localFirst": null,
      "rebuild": null,
      "vendor": null,
      "restore": {
        "from": "pg-wal-nas",
        "steps": [
          "Pick the target time. For corruption or a bad migration, that is the minute before it landed; for hardware loss it is 'latest'.",
          "Restore the newest pg-base-nas snapshot into a scratch data directory — never over the live one.",
          "Set restore_command against the WAL archive and recovery_target_time, then start Postgres and let it replay.",
          "Verify (below) before anything is repointed at it.",
          "Stop the control plane, swap the data directories, start Postgres, start the control plane."
        ],
        "verify": "Row counts on the three tables that matter (tasks, node_identities, incidents) against the last known-good figures in the incident, plus `SELECT pg_is_in_recovery()` returning false and one write that survives a restart.",
        "drills": [
          {
            "date": "2026-07-12",
            "outcome": "pass",
            "measuredMinutes": 22,
            "restoredTo": "scratch instance on home1, port 5433",
            "note": "Point-in-time restore to 24h earlier from pg-base-nas + pg-wal-nas. 22 minutes wall clock, of which 14 was WAL replay. Off-site copies were not exercised."
          }
        ]
      },
      "lastDrill": {
        "date": "2026-07-12",
        "outcome": "pass",
        "measuredMinutes": 22,
        "restoredTo": "scratch instance on home1, port 5433",
        "note": "Point-in-time restore to 24h earlier from pg-base-nas + pg-wal-nas. 22 minutes wall clock, of which 14 was WAL replay. Off-site copies were not exercised."
      },
      "hostsLeaseFor": [
        "cp-scheduling"
      ],
      "worst": "read-only",
      "worstToday": "read-only"
    },
    {
      "id": "jarvisos-vin-journal",
      "name": "JarvisOS VIN journal",
      "purpose": "Lets VIN keep working when the home uplink does not. Task results, telemetry and evidence are written here first and drained into Postgres when the mesh returns, so a home internet outage costs latency rather than work.",
      "engine": "sqlite",
      "durability": "durable",
      "criticality": "estate-critical",
      "writeModel": "local-first-queue",
      "entityId": null,
      "unmonitored": "Planned. Nothing runs on VIN to declare yet, so the inventory has no entity for it and the health engine cannot see it.",
      "proposed": true,
      "status": null,
      "primary": {
        "node": "vin"
      },
      "homeDomain": "fd-vps",
      "domains": [
        "fd-offsite",
        "fd-vps"
      ],
      "writers": [
        {
          "id": "jarvisos-worker",
          "node": "vin",
          "mode": "direct",
          "note": "Appends only. It never updates a row it did not create."
        }
      ],
      "objectives": {
        "rpoMinutes": 15,
        "siteRpoMinutes": 15,
        "rtoMinutes": 30,
        "retentionDays": 7
      },
      "design": {
        "rpoMinutes": 15,
        "siteRpoMinutes": 15,
        "offsiteRpoMinutes": 15,
        "hasOffsite": true,
        "copyCount": 2
      },
      "today": {
        "rpoMinutes": null,
        "siteRpoMinutes": null,
        "hasOffsite": false,
        "copyCount": 1
      },
      "uplinkExposure": {
        "writes": false,
        "offsite": false,
        "offsiteIndependent": false,
        "offsiteIndependentPlanned": true
      },
      "copies": [
        {
          "id": "journal-offsite-s3",
          "kind": "snapshot",
          "target": "gwscli-s3-offsite",
          "node": "vin",
          "path": "vps-egress",
          "domain": "fd-offsite",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 15,
          "exposureMinutes": 15,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "planned",
          "verifiedBy": "Nothing yet. Once running: restic snapshot list from the VIN unit."
        }
      ],
      "promotion": null,
      "localFirst": {
        "drainsTo": "jarvisos-postgres",
        "durable": true,
        "maxQueueMinutes": 4320,
        "maxQueueBytes": 2147483648,
        "onFull": "Stop accepting new work and report the worker as degraded. Dropping the oldest entries would silently lose exactly the results this store exists to keep; refusing new work is visible and reversible.",
        "conflictRule": "There is nothing to merge. Every row carries the node id that wrote it and a monotonic per-node sequence, and only jarvisos-worker on VIN writes them. Postgres is authoritative for ordering across nodes; the drain is an insert that is a no-op if the (node, sequence) pair is already there, so replaying the queue twice is harmless.",
        "drainVerifiedOn": null
      },
      "rebuild": null,
      "vendor": null,
      "restore": {
        "from": "journal-offsite-s3",
        "steps": [
          "Restore the newest snapshot to /var/lib/jarvisos/journal.db on VIN.",
          "Start the worker with drain-only mode so it pushes the backlog before accepting new work.",
          "Watch the drain to zero, then let it take work again."
        ],
        "verify": "Queue depth reaches zero and the same number of rows appears in Postgres. Re-running the drain must change nothing, which is the property the conflict rule promises.",
        "drills": []
      },
      "lastDrill": null,
      "hostsLeaseFor": [],
      "worst": "lost",
      "worstToday": "lost"
    },
    {
      "id": "jarvisos-sessions",
      "name": "JarvisOS sessions & queue (Redis)",
      "purpose": "Session state for the SSO chokepoint and the short-lived job queue. Losing it logs everyone out and drops in-flight jobs; it does not lose anything that was ever committed.",
      "engine": "redis",
      "durability": "rebuildable",
      "criticality": "estate-critical",
      "writeModel": "single-writer",
      "entityId": "redis",
      "unmonitored": null,
      "proposed": false,
      "status": "unknown",
      "primary": {
        "node": "home1"
      },
      "homeDomain": "fd-home-rack",
      "domains": [
        "fd-home-rack"
      ],
      "writers": [
        {
          "id": "jarvisos-auth",
          "node": "vin",
          "mode": "proxied",
          "path": "mesh",
          "note": "The SSO chokepoint runs on VIN and keeps its sessions on home1. That is the single home-internet dependency this register was written to find."
        }
      ],
      "objectives": null,
      "design": {
        "rpoMinutes": null,
        "siteRpoMinutes": null,
        "offsiteRpoMinutes": null,
        "hasOffsite": false,
        "copyCount": 1
      },
      "today": {
        "rpoMinutes": null,
        "siteRpoMinutes": null,
        "hasOffsite": false,
        "copyCount": 1
      },
      "uplinkExposure": {
        "writes": true,
        "offsite": false,
        "offsiteIndependent": false,
        "offsiteIndependentPlanned": false
      },
      "copies": [],
      "promotion": null,
      "localFirst": null,
      "rebuild": {
        "from": "google-oauth",
        "procedure": "Start an empty Redis. Every user re-authenticates against Google on their next request; the job queue refills from the tasks table in jarvisos-postgres, which is authoritative for anything that had actually been accepted.",
        "cost": "Everyone is logged out at once, and any job accepted but not yet started is re-derived from Postgres rather than resumed. Nothing committed is lost."
      },
      "vendor": null,
      "restore": null,
      "lastDrill": null,
      "hostsLeaseFor": [],
      "worst": "restore-required",
      "worstToday": "restore-required"
    },
    {
      "id": "coolify-state",
      "name": "Coolify's own Postgres",
      "purpose": "Every application, server, destination, environment variable and deployment history Coolify holds. Losing it does not stop what is already running — it stops anything from being deployed, redeployed, restarted or rolled back, including the deploy that would replace whatever just failed.",
      "engine": "postgres",
      "durability": "durable",
      "criticality": "fleet-fatal",
      "writeModel": "single-writer",
      "entityId": null,
      "unmonitored": "It runs inside Coolify's compose stack on home1 and is not declared in the inventory, so nothing here probes it. Coolify is the only thing that knows whether it is healthy.",
      "proposed": false,
      "status": null,
      "primary": {
        "node": "home1"
      },
      "homeDomain": "fd-home-rack",
      "domains": [
        "fd-home-rack",
        "fd-offsite"
      ],
      "writers": [
        {
          "id": "coolify",
          "node": "home1",
          "mode": "direct"
        }
      ],
      "objectives": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": 1440,
        "rtoMinutes": 120,
        "retentionDays": 14
      },
      "design": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": 1440,
        "offsiteRpoMinutes": 1440,
        "hasOffsite": true,
        "copyCount": 4
      },
      "today": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": null,
        "hasOffsite": false,
        "copyCount": 2
      },
      "uplinkExposure": {
        "writes": false,
        "offsite": false,
        "offsiteIndependent": false,
        "offsiteIndependentPlanned": false
      },
      "copies": [
        {
          "id": "coolify-dump-local",
          "kind": "logical-dump",
          "target": "home1",
          "node": "home1",
          "path": "home-lan",
          "domain": "fd-home-rack",
          "offDomain": false,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": false,
          "status": "running",
          "verifiedBy": "The pre-upgrade backup receipt written by jarvisos-coolify-fleet.mjs. It records that the dump was taken; it does not record that anyone restored it."
        },
        {
          "id": "coolify-dump-nas",
          "kind": "logical-dump",
          "target": "nas-backups",
          "node": "nas",
          "path": "home-lan",
          "domain": "fd-home-rack",
          "offDomain": false,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "planned",
          "verifiedBy": "Nothing yet. Once running: dump size floor plus a monthly `pg_restore --list` that proves the archive parses."
        },
        {
          "id": "coolify-dump-offsite",
          "kind": "logical-dump",
          "target": "gwscli-s3-offsite",
          "node": "nas",
          "path": "home-egress",
          "domain": "fd-offsite",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "planned",
          "verifiedBy": "Nothing yet. Once running: restic snapshot list, and inclusion in the quarterly drill."
        }
      ],
      "promotion": null,
      "localFirst": null,
      "rebuild": null,
      "vendor": null,
      "restore": {
        "from": "coolify-dump-nas",
        "steps": [
          "Install the same Coolify version the dump came from. A newer one will migrate the schema on boot and the rollback stops existing.",
          "Stop the Coolify stack, drop and recreate its database, `pg_restore` the dump.",
          "Start Coolify and re-enter the credentials it does not hold in the database.",
          "Reconcile against the estate: `scripts/jarvisos-estate.mjs reconcile` must match the register before anything is deployed."
        ],
        "verify": "Coolify lists the same applications and servers as the disposition register captured, and one throwaway redeploy succeeds.",
        "drills": []
      },
      "lastDrill": null,
      "hostsLeaseFor": [],
      "worst": "lost",
      "worstToday": "lost"
    },
    {
      "id": "supabase-edge",
      "name": "Supabase (edge database)",
      "purpose": "The user-facing edge database: auth, row-level security and the tables the public apps read through PostgREST. It is where a browser writes; JarvisOS Postgres is where the estate's own record lives.",
      "engine": "supabase-postgres",
      "durability": "durable",
      "criticality": "estate-critical",
      "writeModel": "vendor-managed",
      "entityId": null,
      "unmonitored": "Vendor-held. There is no host of ours to probe and no entity to declare; what we can see is what the API answers.",
      "proposed": false,
      "status": null,
      "primary": {
        "vendor": "Supabase",
        "region": "eu-central-1",
        "domain": "vendor-supabase"
      },
      "homeDomain": "vendor-supabase",
      "domains": [
        "fd-home-rack",
        "fd-offsite",
        "vendor-supabase"
      ],
      "writers": [],
      "objectives": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": 1440,
        "rtoMinutes": 240,
        "retentionDays": 30
      },
      "design": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": 1440,
        "offsiteRpoMinutes": 1440,
        "hasOffsite": true,
        "copyCount": 3
      },
      "today": {
        "rpoMinutes": 1440,
        "siteRpoMinutes": 1440,
        "hasOffsite": false,
        "copyCount": 2
      },
      "uplinkExposure": {
        "writes": false,
        "offsite": true,
        "offsiteIndependent": false,
        "offsiteIndependentPlanned": false
      },
      "copies": [
        {
          "id": "supabase-dump-nas",
          "kind": "logical-dump",
          "target": "nas-backups",
          "node": "home1",
          "path": "vendor-api",
          "domain": "fd-home-rack",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "running",
          "verifiedBy": "Nightly `pg_dump --format=custom` over the pooler connection, size floor plus a `pg_restore --list` that proves the archive parses. The connection string is read from /etc/jarvisos/config.env and appears in no log."
        },
        {
          "id": "supabase-dump-offsite",
          "kind": "logical-dump",
          "target": "gwscli-s3-offsite",
          "node": "nas",
          "path": "home-egress",
          "domain": "fd-offsite",
          "offDomain": true,
          "mode": "periodic",
          "lagSec": null,
          "intervalMinutes": 1440,
          "exposureMinutes": 1440,
          "promotable": false,
          "restorable": true,
          "serves": "nothing",
          "encrypted": true,
          "status": "planned",
          "verifiedBy": "Nothing yet. Once running: restic snapshot list, and inclusion in the quarterly drill."
        }
      ],
      "promotion": null,
      "localFirst": null,
      "rebuild": null,
      "vendor": {
        "name": "Supabase",
        "controls": "Failover, point-in-time recovery, the Postgres version, the maintenance window and how long any of that takes. None of it is visible from here and none of it can be tested from here.",
        "exitPlan": "The schema is plain Postgres and every table used by the estate is reachable through a logical dump. Moving to jarvisos-postgres means: run both, dual-write from the app for one release, cut reads over, then stop writing to Supabase. What does not move for free is auth — GoTrue's user table and the RLS policies would have to be replaced by jarvisos-auth, which is the reason this is a plan and not a scheduled migration."
      },
      "restore": {
        "from": "supabase-dump-nas",
        "steps": [
          "Decide the target first: back into Supabase (a new project, then repoint the app) or into jarvisos-postgres (the exit plan, under time pressure — do not choose this during an incident unless Supabase itself is the incident).",
          "Create the target database and the roles the dump references. `anon` and `authenticated` do not exist outside Supabase and must be created by hand.",
          "pg_restore the dump. Expect the auth schema to fail against a plain Postgres; that is the part the exit plan calls out.",
          "Repoint the app's connection string and rotate the anon and service keys.",
          "Verify (below) before announcing."
        ],
        "verify": "One row inserted through the app's own path, read back through PostgREST, and one row that RLS should hide staying hidden for an anonymous key.",
        "drills": []
      },
      "lastDrill": null,
      "hostsLeaseFor": [],
      "worst": "restore-required",
      "worstToday": "restore-required"
    }
  ],
  "migrations": [
    {
      "replicaSafe": true,
      "drainedFirst": false,
      "appliedBy": "jarvisos-control-plane on boot, before it takes the lease",
      "id": "m-2026-08-task-owner",
      "name": "Add an owner column to tasks",
      "store": "jarvisos-postgres",
      "style": "expand-contract",
      "reversible": true,
      "steps": [
        "Expand: ADD COLUMN owner text NULL. Nothing reads it yet, and a nullable column is invisible to the running release.",
        "Backfill in batches of 1000 with a sleep between, so replication lag stays under the 30s the recovery point assumes.",
        "Deploy the release that writes and reads the column.",
        "Contract, one release later: SET NOT NULL once the backfill is proven complete."
      ],
      "verify": "SELECT count(*) FROM tasks WHERE owner IS NULL returns zero before the contract step, and the previous release still boots against the expanded schema.",
      "rollback": {
        "method": "inverse-migration",
        "steps": [
          "DROP COLUMN owner"
        ],
        "maxDataLossMinutes": 0,
        "testedOn": "2026-08-03"
      },
      "note": "The shape of every migration to this store: each step is safe on its own, the old release keeps running against the new schema, and no step needs the replica to have caught up."
    },
    {
      "replicaSafe": true,
      "drainedFirst": true,
      "appliedBy": "an operator, by hand, in a maintenance window",
      "id": "m-2026-09-drop-legacy-heartbeats",
      "name": "Drop the legacy heartbeats table",
      "store": "jarvisos-postgres",
      "style": "destructive",
      "reversible": false,
      "backupGate": "pg-base-nas",
      "steps": [
        "Confirm the newest pg-base-nas snapshot is under 6h old and that pg-wal-nas is current.",
        "Drain jarvisos-vin-journal to zero and hold the worker. A queue can be holding rows written by a release that predates this change, and they still have to land.",
        "RENAME the table to heartbeats_legacy_20260901 and leave it for one full retention period.",
        "DROP it only after that period, and only if nothing has asked for it."
      ],
      "verify": "No error in the control-plane log referencing the table for a full retention period after the rename.",
      "rollback": {
        "method": "restore",
        "from": "pg-wal-nas",
        "steps": [
          "Point-in-time restore to the minute before the drop, into a scratch instance.",
          "pg_dump the single table out of the scratch instance and load it back into the live database."
        ],
        "maxDataLossMinutes": 5,
        "testedOn": null
      },
      "note": "The rename-then-drop is what makes an irreversible change reversible for a retention period. The restore path exists for the case where someone skips the wait."
    },
    {
      "replicaSafe": true,
      "drainedFirst": false,
      "appliedBy": "the worker on start, before it opens the queue",
      "id": "m-2026-10-journal-schema-v2",
      "name": "Journal schema v2 — add a node sequence",
      "store": "jarvisos-vin-journal",
      "style": "expand-contract",
      "reversible": true,
      "steps": [
        "Expand: ADD COLUMN node_seq integer NULL and a unique index on (node_id, node_seq) WHERE node_seq IS NOT NULL.",
        "New writes carry the sequence; queued rows written by the previous release do not, and drain unchanged.",
        "Contract only once the queue has drained to zero at least once under the new release."
      ],
      "verify": "A disconnect-and-drain with rows written by both releases in the queue, ending at zero depth with no duplicate rows in Postgres.",
      "rollback": {
        "method": "inverse-migration",
        "steps": [
          "DROP INDEX, DROP COLUMN node_seq"
        ],
        "maxDataLossMinutes": 0,
        "testedOn": null
      },
      "note": "A local-first store makes the expand-contract rule non-negotiable: at any moment the queue may hold rows written by a release that is no longer installed, and they still have to drain."
    }
  ],
  "scenarios": [
    {
      "id": "lose-home1",
      "name": "Home1 is gone",
      "description": "The home server is down or unreachable: hardware, power, or a failed upgrade. The operator works from VIN over SSH. The NAS is still powered but has no compute of interest.",
      "partitions": [
        {
          "id": "vps",
          "nodes": [
            "vin",
            "vps1"
          ],
          "operator": true,
          "vendorReachable": true
        },
        {
          "id": "home-lan",
          "nodes": [
            "nas"
          ],
          "operator": false,
          "vendorReachable": true
        }
      ],
      "lostNodes": [
        "home1"
      ],
      "surviving": 3,
      "survivingToday": 3,
      "lost": 1,
      "total": 5
    },
    {
      "id": "lose-vin",
      "name": "VIN (the VPS) is gone",
      "description": "The VPS is destroyed, suspended or unreachable. Home1 and the NAS are healthy and the home uplink works. Everything published to the internet goes with it.",
      "partitions": [
        {
          "id": "home",
          "nodes": [
            "home1",
            "nas",
            "vps1"
          ],
          "operator": true,
          "vendorReachable": true
        }
      ],
      "lostNodes": [
        "vin"
      ],
      "surviving": 4,
      "survivingToday": 4,
      "lost": 1,
      "total": 5
    },
    {
      "id": "lose-uplink",
      "name": "Home internet is down",
      "description": "The ISP uplink is dead. Home1 and the NAS are healthy but cut off from every vendor and from VIN. VIN is healthy and still on the internet, but nobody at home can reach it. The operator is at home.",
      "partitions": [
        {
          "id": "home",
          "nodes": [
            "home1",
            "nas"
          ],
          "operator": true,
          "vendorReachable": false
        },
        {
          "id": "vps",
          "nodes": [
            "vin",
            "vps1"
          ],
          "operator": false,
          "vendorReachable": true
        }
      ],
      "lostNodes": [],
      "surviving": 3,
      "survivingToday": 3,
      "lost": 1,
      "total": 5
    },
    {
      "id": "partition-mesh",
      "name": "The mesh is partitioned",
      "description": "Both sites are healthy and both have internet, but the WireGuard tunnel between them is down. This is the scenario that punishes automatic promotion: each side can see the world and neither can see the other.",
      "partitions": [
        {
          "id": "home",
          "nodes": [
            "home1",
            "nas"
          ],
          "operator": true,
          "vendorReachable": true
        },
        {
          "id": "vps",
          "nodes": [
            "vin",
            "vps1"
          ],
          "operator": false,
          "vendorReachable": true
        }
      ],
      "lostNodes": [],
      "surviving": 4,
      "survivingToday": 4,
      "lost": 1,
      "total": 5
    },
    {
      "id": "lose-nas",
      "name": "The NAS is gone",
      "description": "Bulk storage and the local backup tier are unavailable. Compute is untouched; recovery capability is not.",
      "partitions": [
        {
          "id": "estate",
          "nodes": [
            "home1",
            "vin",
            "vps1"
          ],
          "operator": true,
          "vendorReachable": true
        }
      ],
      "lostNodes": [
        "nas"
      ],
      "surviving": 5,
      "survivingToday": 5,
      "lost": 0,
      "total": 5
    }
  ],
  "matrix": [
    {
      "posture": "target",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-home1",
      "outcome": "read-only",
      "minutes": 20,
      "notes": [
        "pg-replica-vin keeps serving reads here; writes resume once a human promotes it."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "read-only",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "read-only",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-postgres",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "read-only",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-home1",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-vin",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-uplink",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "partition-mesh",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-home1",
      "outcome": "restore-required",
      "minutes": null,
      "notes": [
        "Start an empty Redis. Every user re-authenticates against Google on their next request; the job queue refills from the tasks table in jarvisos-postgres, which is authoritative for anything that had actually been accepted."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "restore-required",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-sessions",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "coolify-state",
      "scenarioId": "lose-home1",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "coolify-state",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "coolify-state",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "coolify-state",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "coolify-state",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "supabase-edge",
      "scenarioId": "lose-home1",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "supabase-edge",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "supabase-edge",
      "scenarioId": "lose-uplink",
      "outcome": "restore-required",
      "minutes": 240,
      "notes": [
        "Only supabase-dump-nas is reachable; recovery here means restoring it."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "restore-required",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "supabase-edge",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "target",
      "storeId": "supabase-edge",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-home1",
      "outcome": "read-only",
      "minutes": 20,
      "notes": [
        "pg-replica-vin keeps serving reads here; writes resume once a human promotes it."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "read-only",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "read-only",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-postgres",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "read-only",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-postgres",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-home1",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-vin",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-uplink",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "partition-mesh",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-home1",
      "outcome": "restore-required",
      "minutes": null,
      "notes": [
        "Start an empty Redis. Every user re-authenticates against Google on their next request; the job queue refills from the tasks table in jarvisos-postgres, which is authoritative for anything that had actually been accepted."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "restore-required",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-sessions",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "restore-required",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "jarvisos-sessions",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "coolify-state",
      "scenarioId": "lose-home1",
      "outcome": "lost",
      "minutes": null,
      "notes": [
        "No copy of this data is reachable from this side of the break."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "lost",
          "writable": false
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "coolify-state",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "coolify-state",
      "scenarioId": "lose-uplink",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "coolify-state",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "lost",
          "writable": false
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "coolify-state",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 1,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "supabase-edge",
      "scenarioId": "lose-home1",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "vps",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "home-lan",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "supabase-edge",
      "scenarioId": "lose-vin",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "supabase-edge",
      "scenarioId": "lose-uplink",
      "outcome": "restore-required",
      "minutes": 240,
      "notes": [
        "Only supabase-dump-nas is reachable; recovery here means restoring it."
      ],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "restore-required",
          "writable": false
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "supabase-edge",
      "scenarioId": "partition-mesh",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "home",
          "operator": true,
          "outcome": "writable",
          "writable": true
        },
        {
          "id": "vps",
          "operator": false,
          "outcome": "writable",
          "writable": true
        }
      ]
    },
    {
      "posture": "current",
      "storeId": "supabase-edge",
      "scenarioId": "lose-nas",
      "outcome": "writable",
      "minutes": null,
      "notes": [],
      "concurrentWriters": 0,
      "partitions": [
        {
          "id": "estate",
          "operator": true,
          "outcome": "writable",
          "writable": true
        }
      ]
    }
  ],
  "findings": [
    {
      "code": "recovery-depends-on-home-uplink",
      "severity": "gap",
      "title": "Every copy that leaves the primary’s domain is fed over the home uplink.",
      "storeId": "jarvisos-postgres",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Every running copy of JarvisOS Postgres that leaves fd-home-rack is fed over the home uplink (mesh, home-egress). While that uplink is down the primary keeps accepting writes and none of them are protected off-site — and nothing looks broken. pg-offsite-vin would close this once running.",
      "acceptance": null
    },
    {
      "code": "rpo-not-met",
      "severity": "gap",
      "title": "The best copy is fed more slowly than the declared recovery point allows.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal has no running copy anything can be recovered from. Planned: journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "site-rpo-not-met",
      "severity": "gap",
      "title": "No copy outside the primary’s failure domain meets the declared site recovery point.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal has no running copy outside fd-vps, so losing the site loses everything ever written to it. Planned: journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "copies-single-domain",
      "severity": "gap",
      "title": "Every copy of this store sits in the same failure domain as the primary.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal and every running copy of it sit in fd-vps. That domain’s correlated risks are this data’s risks. Planned: journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "no-offsite-copy",
      "severity": "gap",
      "title": "Nothing leaves the estate, so no copy survives the site.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal has no running copy in a domain that hosts none of our compute. Planned: journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "local-first-drain-unverified",
      "severity": "gap",
      "title": "Nobody has proven the queue drains after an outage.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal has never had a disconnect-and-drain proven end to end.",
      "acceptance": null
    },
    {
      "code": "restore-never-drilled",
      "severity": "gap",
      "title": "Nobody has ever restored this store.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Nobody has ever restored JarvisOS VIN journal from journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "store-lost-in-scenario",
      "severity": "gap",
      "title": "A critical store has no reachable copy at all in a declared scenario.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": "lose-vin",
      "migrationId": null,
      "detail": "\"VIN (the VPS) is gone\" destroys vin, and every copy of JarvisOS VIN journal that runs today is on it. The data is gone, not merely unreachable. Planned: journal-offsite-s3.",
      "acceptance": null
    },
    {
      "code": "writes-depend-on-home-uplink",
      "severity": "gap",
      "title": "A writer on another site writes across the home uplink with nothing local to fall back to.",
      "storeId": "jarvisos-sessions",
      "scenarioId": null,
      "migrationId": null,
      "detail": "jarvisos-auth on vin writes through a proxy to JarvisOS sessions & queue (Redis) over \"mesh\", which crosses the home uplink. While that is down, vin cannot write at all.",
      "acceptance": null
    },
    {
      "code": "site-rpo-not-met",
      "severity": "gap",
      "title": "No copy outside the primary’s failure domain meets the declared site recovery point.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres has no running copy outside fd-home-rack, so losing the site loses everything ever written to it. Planned: coolify-dump-nas, coolify-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "copies-below-policy",
      "severity": "gap",
      "title": "Fewer independent copies than policy requires.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres exists in 2 place(s) today; policy wants 3. Planned: coolify-dump-nas, coolify-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "copies-single-domain",
      "severity": "gap",
      "title": "Every copy of this store sits in the same failure domain as the primary.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres and every running copy of it sit in fd-home-rack. That domain’s correlated risks are this data’s risks. Planned: coolify-dump-nas, coolify-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "no-offsite-copy",
      "severity": "gap",
      "title": "Nothing leaves the estate, so no copy survives the site.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres has no running copy in a domain that hosts none of our compute. Planned: coolify-dump-nas, coolify-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "restore-never-drilled",
      "severity": "gap",
      "title": "Nobody has ever restored this store.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Nobody has ever restored Coolify's own Postgres from coolify-dump-nas.",
      "acceptance": null
    },
    {
      "code": "store-lost-in-scenario",
      "severity": "gap",
      "title": "A critical store has no reachable copy at all in a declared scenario.",
      "storeId": "coolify-state",
      "scenarioId": "lose-home1",
      "migrationId": null,
      "detail": "\"Home1 is gone\" destroys home1, and every copy of Coolify's own Postgres that runs today is on it. The data is gone, not merely unreachable. Planned: coolify-dump-nas, coolify-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "copies-below-policy",
      "severity": "gap",
      "title": "Fewer independent copies than policy requires.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Supabase (edge database) exists in 2 place(s) today; policy wants 3. Planned: supabase-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "no-offsite-copy",
      "severity": "gap",
      "title": "Nothing leaves the estate, so no copy survives the site.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Supabase (edge database) has no running copy in a domain that hosts none of our compute. Planned: supabase-dump-offsite.",
      "acceptance": null
    },
    {
      "code": "recovery-depends-on-home-uplink",
      "severity": "gap",
      "title": "Every copy that leaves the primary’s domain is fed over the home uplink.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Every running copy of Supabase (edge database) that leaves vendor-supabase is fed over the home uplink (vendor-api). While that uplink is down the primary keeps accepting writes and none of them are protected off-site — and nothing looks broken.",
      "acceptance": null
    },
    {
      "code": "restore-never-drilled",
      "severity": "gap",
      "title": "Nobody has ever restored this store.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Nobody has ever restored Supabase (edge database) from supabase-dump-nas.",
      "acceptance": null
    },
    {
      "code": "migration-rollback-untested",
      "severity": "gap",
      "title": "A rollback path has never been executed.",
      "storeId": "jarvisos-postgres",
      "scenarioId": null,
      "migrationId": "m-2026-09-drop-legacy-heartbeats",
      "detail": "The rollback for \"Drop the legacy heartbeats table\" has never been executed against anything.",
      "acceptance": null
    },
    {
      "code": "migration-rollback-untested",
      "severity": "gap",
      "title": "A rollback path has never been executed.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": "m-2026-10-journal-schema-v2",
      "detail": "The rollback for \"Journal schema v2 — add a node sequence\" has never been executed against anything.",
      "acceptance": null
    },
    {
      "code": "copies-below-policy",
      "severity": "accepted",
      "title": "Fewer independent copies than policy requires.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal exists in 1 place(s) today; policy wants 3. Planned: journal-offsite-s3.",
      "acceptance": {
        "reason": "This is a bounded queue, not a system of record. Anything in it is either drained into Postgres within minutes — where the three-copy rule does apply — or it is still in flight. A third copy of a queue protects nothing that Postgres is not already protecting, and it would cost a second place for unfinished work to leak from.",
        "acceptedBy": "homelab-operator",
        "reviewOn": "2027-02-01"
      }
    },
    {
      "code": "store-not-monitored",
      "severity": "note",
      "title": "A store has no inventory entity, so nothing here can tell whether it is up.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal has no inventory entity, so nothing here can tell whether it is up: Planned. Nothing runs on VIN to declare yet, so the inventory has no entity for it and the health engine cannot see it.",
      "acceptance": null
    },
    {
      "code": "design-not-adopted",
      "severity": "note",
      "title": "The design meets its objectives only because of copies that are not running yet.",
      "storeId": "jarvisos-vin-journal",
      "scenarioId": null,
      "migrationId": null,
      "detail": "JarvisOS VIN journal reaches 15m / site 15m only once journal-offsite-s3 is running. Today it is none / site none.",
      "acceptance": null
    },
    {
      "code": "store-not-monitored",
      "severity": "note",
      "title": "A store has no inventory entity, so nothing here can tell whether it is up.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres has no inventory entity, so nothing here can tell whether it is up: It runs inside Coolify's compose stack on home1 and is not declared in the inventory, so nothing here probes it. Coolify is the only thing that knows whether it is healthy.",
      "acceptance": null
    },
    {
      "code": "design-not-adopted",
      "severity": "note",
      "title": "The design meets its objectives only because of copies that are not running yet.",
      "storeId": "coolify-state",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Coolify's own Postgres reaches 1d / site 1d only once coolify-dump-nas, coolify-dump-offsite are running. Today it is 1d / site none.",
      "acceptance": null
    },
    {
      "code": "store-not-monitored",
      "severity": "note",
      "title": "A store has no inventory entity, so nothing here can tell whether it is up.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Supabase (edge database) has no inventory entity, so nothing here can tell whether it is up: Vendor-held. There is no host of ours to probe and no entity to declare; what we can see is what the API answers.",
      "acceptance": null
    },
    {
      "code": "design-not-adopted",
      "severity": "note",
      "title": "The design meets its objectives only because of copies that are not running yet.",
      "storeId": "supabase-edge",
      "scenarioId": null,
      "migrationId": null,
      "detail": "Supabase (edge database) reaches 1d / site 1d only once supabase-dump-offsite is running. Today it is 1d / site 1d.",
      "acceptance": null
    }
  ],
  "source": "/app/config/datastores.json"
}