openvibers/openvibe.network Commits
Correct the T2 cutover premise to the observed PostgreSQL-only release (#24)
Changes
nnew file mode 100644@@@ -0,0 +1,137 @@++# T2 cutover: closeout evidence++++What the read-only interfaces can actually prove about the OpenVibe.Network PostgreSQL cutover (plan T2,++ADR-035; procedure in [cutover-t2-postgres.md](cutover-t2-postgres.md)). Checked **2026-10-03 (UTC, host++clock)** through `ov access run openvibe-ovh <action>` — `health`, `service-status`, `releases`, `plan`,++`validate`, `env-names`, `env-set`, `show`, `journal`, `certs`. Every result below is a literal output of one++of those actions or of a read-only Git query on this checkout.++++Nothing here was deployed, restarted, rolled back or imported; no credential, URL or env **value** was read or++printed; no check that needs one was guessed. Checks that need an interface the broker does not expose are++marked **pending** in their own section.++++SHAs are printed as `ovhost` reports them (12 hex characters).++++## Release and readiness — verified++++- `health network` (exit 0): `network bad7b106fda0 ready openvibe-network=active`. `ovhost` needs its++ readiness probe (the service's `/api/ready`) to answer before it calls a service `ready`.++- `service-status network` (exit 0): the same line.++- `plan network` (exit 0): `network: bad7b106fda0 → bad7b106fda0 (up to date) on main`; strategy++ `git-checkout`, `0` files changed, `restart no`.++- `validate network` (exit 0): `ok checkout /opt/openvibe.network owned by ubuntu`; `ok checkout clean at++ bad7b106fda0`; `ok units openvibe-network.service active/running from++ /etc/systemd/system/openvibe-network.service`; `ok port 4000 held by node(734262)`; `ok nginx++ /etc/nginx/sites-enabled/openvibe.network.conf enabled`; `ok nginx nginx -t clean`; `ok deps every++ dependency resolves`; lifecycle `network` manifest in openvibe-contracts v0.83.0, checkout++ openvibe-contracts 0.85.0; `network: valid (0 error(s), 0 warning(s))`.++++`bad7b106fda0` is origin/main HEAD (`git ls-remote origin` → `bad7b106fda07771109fdd41295ae481aa373dfb HEAD`).++++**Why this is a PostgreSQL-only release.** `bad7b106fda0` is a descendant of `206ab36`, the commit that made++production refuse a SQLite path. At that commit `server/db/database.js` throws when `NODE_ENV=production` and++`DATABASE_URL` is unset, and `server/config.js` states there is no SQLite file. Locally, `git diff --name-only++4e7b56b bad7b10` (this checkout is one commit behind) touches 26 files and none of `server/db/`,++`server/config.js` or `package.json`, so the running release has that same boot rule. A release that is++`active` and `ready` therefore booted with `DATABASE_URL` set and serves from PostgreSQL.++++**Boot of the running release** (`journal openvibe-network.service`, 2026-10-03T04:37:53–04:37:59Z): the old++process stops in 5 ms, systemd starts `env[734262]`, then `[DB] Synced 35 built-in themes`,++`[Identity] subject ids: 0 users, 0 guests assigned; 3 legacy map rows seeded`, `[DB] Central database++seeded`, `[Auth] RS256 keypair loaded`, `[Email] Resend initialized`, `[Events consumer] on:++POST /internal/events`, `[push] VAPID initialized`, the banner on port 4000. There is no++`DATABASE_URL unset: embedded PGlite database` warning (the development fallback). The `journal` action clips++its answer to a recent window (the call returned about 90 lines, 02:54–04:39Z), so older boots are not++readable.++++### Release history, as observed (`releases network`, from 2026-10-01)++++```++2026-10-01T18:56:31.170Z deploy 20dd7ff02e0c → 5781a6b9daf5 failed-rolled-back ovbroker lockfile++2026-10-01T18:57:19.036Z rollback 20dd7ff02e0c → b5d870737f33 rolled-back ovbroker++2026-10-01T19:03:06.415Z rollback b5d870737f33 → 20dd7ff02e0c rolled-back ubuntu++2026-10-02T01:10:14.248Z deploy 20dd7ff02e0c → 427e196f6276 failed-rolled-back ovbroker lockfile++2026-10-02T14:49:54.343Z deploy 20dd7ff02e0c → 2c6b8b5eae59 failed-rolled-back ovbroker lockfile++2026-10-02T16:56:32.756Z deploy 20dd7ff02e0c → 757dc98b1b67 deployed ubuntu lockfile++2026-10-02T17:19:12.262Z deploy 757dc98b1b67 → cbaacc48bd07 deployed ovbroker++2026-10-03T00:54:33.837Z deploy cbaacc48bd07 → 4e7b56b2046f deployed ovbroker++2026-10-03T04:37:53.267Z deploy 4e7b56b2046f → bad7b106fda0 deployed ovbroker++```++++`20dd7ff` was deployed on 2026-09-29T14:31:31Z (the runbook's "last SQLite release"). All five later SHAs are++descendants of the PostgreSQL-only `206ab36`. Three attempts ended `failed-rolled-back` (the first ran a++rollback to `b5d870737f33`, then a second rollback returned production to `20dd7ff`); the first release that++stayed up is `757dc98b1b67`. `ovhost` records no failure reason, so the runbook's unconfirmed++missing-`DATABASE_URL` explanation stays unconfirmed.++++## Env presence and mode — verified++++- `validate network`: `ok env /etc/openvibe/network.env mode 600`; `ok env 4 required name(s) present and++ non-empty`.++- `env-set network` (names only): file `/etc/openvibe/network.env`, `found: true`, `empty: []`. The set++ includes **`DATABASE_URL`**, **`DATABASE_DIRECT_URL`**, **`VALKEY_URL`** and `VALKEY_PREFIX`, alongside++ `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `NETWORK_EVENTS_SECRET`,++ `RESEND_API_KEY`, `VAPID_PRIVATE_KEY`, `DB_PATH`, `FOLLOWS_AUTHORITY` and the port/host/base-URL names.++ `fromUnits`: `NODE_ENV`, `PATH`.++- `env-names network`: `/opt/openvibe.network/.env.example` declares 30 names, `DATABASE_URL`,++ `DATABASE_DIRECT_URL`, `VALKEY_URL` and `DATA_DIR` among them.++++`DB_PATH` and `DATA_DIR` remain declared/set but are the legacy names the PostgreSQL release no longer reads++for serving (`server/config.js`).++++## Row parity — pending++++The step-7 parity result for the production import cannot be established from the interfaces available.++`scripts/pg-row-parity.js` needs the SQLite source (`$COPY`) and `DATABASE_DIRECT_URL` (or `DATABASE_URL`);++no broker action exposes either, and none runs a query or lists a file. **Not checked.**++++What does exist is the rehearsal: `test/pg-cutover-rehearsal.test.js` runs the whole sequence++(pg-preflight → `migrate-to-postgres.js` → row-count parity → server boot with `/api/ready` green) on a seeded++`20dd7ff` fixture, on PGlite by default and on a scratch database under `npm run test:pg`. That proves the++tools, not production's rows.++++## Valkey ACL scope — pending++++Only presence is provable: `VALKEY_URL` and `VALKEY_PREFIX` are set and non-empty (above). The scope of the++Valkey user — which commands it holds and that it is confined to the service's key prefix — needs a++connection to Valkey, which no read-only action opens. **Not checked.**++++## Rollback copies — pending++++The runbook keeps `$COPY` (`/var/backups/openvibe/network-pre-t2-<T0>.db`, `chmod 0400`) and the live++`/opt/openvibe.network/data/network.db` untouched for 30 days as the rollback and audit trail. No read-only++action lists or stats those paths: `show network` returns only the inventory entry++(`network /opt/openvibe.network units: openvibe-network.service`) and `validate network` does not cover++backups. **Not checked.**++++The nearest interface, `ov access run openvibe-ovh db-backup network`, is a medium (write) action that would++take a *new* backup; it cannot prove the pre-T2 copy or its mode, so it was not run. Note also that, with the++cutover done, the previous release is PostgreSQL-only: a rollback is a code rollback and does not by itself++return serving to SQLite (see the runbook's [Rollback](cutover-t2-postgres.md#rollback) note).++++## Unresolved, owner-only++++- **Cause of the three rollbacks.** `releases` carries no reason and the journal is clipped to recent boots,++ so this stays unconfirmed as the runbook says.++- **Whether steps 3–7 ran**, and with what values for `$T0`, `$T1`, the `cleaned:` count and the parity++ output: not observable from any read-only action.++- **Functional checks** (runbook step 9.3–9.6: sign-in with a pre-cutover account, OAuth authorize on a++ first-party site, notifications, the OpenCoins balance against `$COPY`): need an account and a browser and++ were not run.++++## Reproduce++++```bash++ov access run openvibe-ovh health network++ov access run openvibe-ovh service-status network++ov access run openvibe-ovh plan network++ov access run openvibe-ovh releases network++ov access run openvibe-ovh validate network++ov access run openvibe-ovh env-set network++ov access run openvibe-ovh env-names network++ov access run openvibe-ovh show network++ov access run openvibe-ovh journal openvibe-network.service 200++```++++Every one of these is a low-risk read. `health`, `releases` and `validate` are granted to `rung:*`;++`deploy`, `rollback`, `restart` and `db-backup` are not, and were not used.@@@ -1,14 +1,32 @@# T2 cutover: Network from SQLite to PostgreSQL--The production move of OpenVibe.Network from its SQLite file to PostgreSQL (plan T2, ADR-035). Production--(`openvibe-ovh`) runs **20dd7ff**, the last SQLite release. Every later commit serves from PostgreSQL only:--`DATABASE_URL` is required in production (there is no SQLite path), migrations run as the owner on--`DATABASE_DIRECT_URL`, and the limit counters live on Valkey (`VALKEY_URL`). Two manual deploys of that range--failed readiness and ovhost rolled them back. The most likely cause is that the server refused to boot without--`DATABASE_URL` (`server/db/database.js`), but this is unconfirmed. This runbook covers the data move those--deploys skipped. It was rehearsed with `npm run rehearse-pg-cutover` (`scripts/rehearse-pg-cutover.js`) on--20dd7ff's full schema: the 51 tables `initDb()` makes plus the 26 its modules create at boot. That rehearsal--found two import blockers, fixed in `scripts/migrate-to-postgres.js` with it:++The production move of OpenVibe.Network from its SQLite file to PostgreSQL (plan T2, ADR-035). Every commit++after **20dd7ff** serves from PostgreSQL only: `DATABASE_URL` is required in production (there is no SQLite++path), migrations run as the owner on `DATABASE_DIRECT_URL`, and the limit counters live on Valkey++(`VALKEY_URL`).++++**Observed state (read-only `ov access run openvibe-ovh`, 2026-10-03 UTC).** Production (`openvibe-ovh`) now++runs **bad7b106fda0**, a PostgreSQL-only release: `health` reports it `ready` with `openvibe-network=active`++and `validate` reports the checkout clean at that SHA. The serving code still refuses to boot in production++without `DATABASE_URL` (`server/db/database.js`), so an active, ready release is one serving from PostgreSQL.++`DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL` are present and non-empty in++`/etc/openvibe/network.env` (mode `600`). The per-check detail, and what could not be checked, is in++[cutover-evidence-t2.md](cutover-evidence-t2.md).++++**First-attempt history, kept.** Production ran **20dd7ff**, the last SQLite release, until 2026-10-02. The++deployments of the PostgreSQL-only range did not all come up: `ovhost releases network` records++`failed-rolled-back` for **5781a6b9daf5** (2026-10-01T18:56Z), **427e196f6276** (2026-10-02T01:10Z) and++**2c6b8b5eae59** (2026-10-02T14:49Z), each followed by a rollback to 20dd7ff. (This runbook first recorded two++failed attempts; the release log kept by ovhost shows three.) The first release in that range that stayed up++was **757dc98b1b67** (2026-10-02T16:56Z), then **cbaacc48bd07**, **4e7b56b2046f** and the current++**bad7b106fda0**. The leading explanation for the rollbacks — the server refused to boot without++`DATABASE_URL` (`server/db/database.js`) — was never confirmed and still is not: the journal no longer reaches++those windows, and `ovhost` records `failed-rolled-back` with no reason.++++This runbook covers the data move those first deploys skipped. It was rehearsed with++`npm run rehearse-pg-cutover` (`scripts/rehearse-pg-cutover.js`) on 20dd7ff's full schema: the 51 tables++`initDb()` makes plus the 26 its modules create at boot. That rehearsal found two import blockers, fixed in++`scripts/migrate-to-postgres.js` with it:- `analytics_rate_tracking` (openvibe-shared's old per-IP counters) has no PostgreSQL table, so the import refusedto start. It is now in `SKIP_SOURCE`, so IPs are never carried over.@@@ -16,9 +34,11 @@ found two import blockers, fixed in `scripts/migrate-to-postgres.js` with it:`user_profile_changes` rows, and would have published one profile event per account. The import now turnsthat trigger off while it copies (`QUIET_TRIGGERS`).--Every step ends with a **go / no-go** check. A no-go before step 8 (deploy) leaves production on SQLite with no--data change: restart the old release (`sudo systemctl start openvibe-network`), reopen, and reschedule. A no-go--from step 8 on means [rollback](#rollback).++Every step ends with a **go / no-go** check. The steps are kept as the record of how the move was rehearsed and++run; a re-run (a rebuild, or a second environment) starts from the Prerequisites with the then-current target++SHA. At the time of the move, a no-go before step 8 (deploy) left production on SQLite with no data change:++restart the old release (`sudo systemctl start openvibe-network`), reopen, and reschedule. A no-go from step 8++on meant [rollback](#rollback).Conventions:@@@ -89,8 +109,8 @@ These are done by the owner and never by an agent. Each must be true before the## Step 1: stage the target release (T-1 day, no downtime)--The running checkout `/opt/openvibe.network` stays on 20dd7ff until step 8. The import tools and migrations--come from a separate checkout of the target commit. It is never served from.++At the time of the move, the running checkout `/opt/openvibe.network` stayed on 20dd7ff until step 8. The import++tools and migrations come from a separate checkout of the target commit. It is never served from.```bashTARGET=<merge sha>@@@ -270,7 +290,8 @@ At boot, the migrations find everything applied, and the seed adds only the boot**Go:** the deploy exits 0, and its release is `$TARGET`.**No-go:**--- **ovhost rolled back (exit 3).** Production is on 20dd7ff and SQLite again, and no write reached PostgreSQL.++- **ovhost rolled back (exit 3).** At the time of the move, production was on 20dd7ff and SQLite again, and no++ write had reached PostgreSQL.Read `ovhost releases network` and the boot log(`sudo journalctl -u openvibe-network --since "$T0" | tail -50`). The log names its failure without printingURLs. Then close the window.@@@ -324,13 +345,15 @@ rollback and the audit trail. `$STAGE` may be deleted.ov access run openvibe-ovh rollback network```--This brings back the previous release, **20dd7ff**, which serves from `data/network.db` again. Nothing in this--runbook writes to that file: the import read `$COPY`, and the PostgreSQL release never opens `DB_PATH`. The file--is exactly as it was at T0. The old release ignores `DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL`, so--they stay in `network.env`.++At the time of the move, this brought back the previous release, **20dd7ff**, which served from++`data/network.db` again. Nothing in this runbook writes to that file: the import read `$COPY`, and the++PostgreSQL release never opens `DB_PATH`. The file was exactly as it was at T0. The old release ignores++`DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL`, so they stay in `network.env`. After the move++succeeded, the previous release is a PostgreSQL-only one, so a rollback is a code rollback only — it does not++by itself return serving to SQLite.--**Go (rollback worked):** `ov access run openvibe-ovh health network` is healthy on 20dd7ff, and `/api/ready`--answers 200. Sign-in works.++**Go (rollback worked):** at the time of the move, `ov access run openvibe-ovh health network` was healthy on++20dd7ff, and `/api/ready` answered 200. Sign-in worked.**What is lost:** every write that reached PostgreSQL between T1 and the rollback. That includes:- new accounts, and profile, username and password changes;
new file mode 100644
new file mode 100644
@@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
+# T2 cutover: closeout evidence
+
+What the read-only interfaces can actually prove about the OpenVibe.Network PostgreSQL cutover (plan T2,
+ADR-035; procedure in [cutover-t2-postgres.md](cutover-t2-postgres.md)). Checked **2026-10-03 (UTC, host
+clock)** through `ov access run openvibe-ovh <action>` — `health`, `service-status`, `releases`, `plan`,
+`validate`, `env-names`, `env-set`, `show`, `journal`, `certs`. Every result below is a literal output of one
+of those actions or of a read-only Git query on this checkout.
+
+Nothing here was deployed, restarted, rolled back or imported; no credential, URL or env **value** was read or
+printed; no check that needs one was guessed. Checks that need an interface the broker does not expose are
+marked **pending** in their own section.
+
+SHAs are printed as `ovhost` reports them (12 hex characters).
+
+## Release and readiness — verified
+
+- `health network` (exit 0): `network bad7b106fda0 ready openvibe-network=active`. `ovhost` needs its
+ readiness probe (the service's `/api/ready`) to answer before it calls a service `ready`.
+- `service-status network` (exit 0): the same line.
+- `plan network` (exit 0): `network: bad7b106fda0 → bad7b106fda0 (up to date) on main`; strategy
+ `git-checkout`, `0` files changed, `restart no`.
+- `validate network` (exit 0): `ok checkout /opt/openvibe.network owned by ubuntu`; `ok checkout clean at
+ bad7b106fda0`; `ok units openvibe-network.service active/running from
+ /etc/systemd/system/openvibe-network.service`; `ok port 4000 held by node(734262)`; `ok nginx
+ /etc/nginx/sites-enabled/openvibe.network.conf enabled`; `ok nginx nginx -t clean`; `ok deps every
+ dependency resolves`; lifecycle `network` manifest in openvibe-contracts v0.83.0, checkout
+ openvibe-contracts 0.85.0; `network: valid (0 error(s), 0 warning(s))`.
+
+`bad7b106fda0` is origin/main HEAD (`git ls-remote origin` → `bad7b106fda07771109fdd41295ae481aa373dfb HEAD`).
+
+**Why this is a PostgreSQL-only release.** `bad7b106fda0` is a descendant of `206ab36`, the commit that made
+production refuse a SQLite path. At that commit `server/db/database.js` throws when `NODE_ENV=production` and
+`DATABASE_URL` is unset, and `server/config.js` states there is no SQLite file. Locally, `git diff --name-only
+4e7b56b bad7b10` (this checkout is one commit behind) touches 26 files and none of `server/db/`,
+`server/config.js` or `package.json`, so the running release has that same boot rule. A release that is
+`active` and `ready` therefore booted with `DATABASE_URL` set and serves from PostgreSQL.
+
+**Boot of the running release** (`journal openvibe-network.service`, 2026-10-03T04:37:53–04:37:59Z): the old
+process stops in 5 ms, systemd starts `env[734262]`, then `[DB] Synced 35 built-in themes`,
+`[Identity] subject ids: 0 users, 0 guests assigned; 3 legacy map rows seeded`, `[DB] Central database
+seeded`, `[Auth] RS256 keypair loaded`, `[Email] Resend initialized`, `[Events consumer] on:
+POST /internal/events`, `[push] VAPID initialized`, the banner on port 4000. There is no
+`DATABASE_URL unset: embedded PGlite database` warning (the development fallback). The `journal` action clips
+its answer to a recent window (the call returned about 90 lines, 02:54–04:39Z), so older boots are not
+readable.
+
+### Release history, as observed (`releases network`, from 2026-10-01)
+
+```
+2026-10-01T18:56:31.170Z deploy 20dd7ff02e0c → 5781a6b9daf5 failed-rolled-back ovbroker lockfile
+2026-10-01T18:57:19.036Z rollback 20dd7ff02e0c → b5d870737f33 rolled-back ovbroker
+2026-10-01T19:03:06.415Z rollback b5d870737f33 → 20dd7ff02e0c rolled-back ubuntu
+2026-10-02T01:10:14.248Z deploy 20dd7ff02e0c → 427e196f6276 failed-rolled-back ovbroker lockfile
+2026-10-02T14:49:54.343Z deploy 20dd7ff02e0c → 2c6b8b5eae59 failed-rolled-back ovbroker lockfile
+2026-10-02T16:56:32.756Z deploy 20dd7ff02e0c → 757dc98b1b67 deployed ubuntu lockfile
+2026-10-02T17:19:12.262Z deploy 757dc98b1b67 → cbaacc48bd07 deployed ovbroker
+2026-10-03T00:54:33.837Z deploy cbaacc48bd07 → 4e7b56b2046f deployed ovbroker
+2026-10-03T04:37:53.267Z deploy 4e7b56b2046f → bad7b106fda0 deployed ovbroker
+```
+
+`20dd7ff` was deployed on 2026-09-29T14:31:31Z (the runbook's "last SQLite release"). All five later SHAs are
+descendants of the PostgreSQL-only `206ab36`. Three attempts ended `failed-rolled-back` (the first ran a
+rollback to `b5d870737f33`, then a second rollback returned production to `20dd7ff`); the first release that
+stayed up is `757dc98b1b67`. `ovhost` records no failure reason, so the runbook's unconfirmed
+missing-`DATABASE_URL` explanation stays unconfirmed.
+
+## Env presence and mode — verified
+
+- `validate network`: `ok env /etc/openvibe/network.env mode 600`; `ok env 4 required name(s) present and
+ non-empty`.
+- `env-set network` (names only): file `/etc/openvibe/network.env`, `found: true`, `empty: []`. The set
+ includes **`DATABASE_URL`**, **`DATABASE_DIRECT_URL`**, **`VALKEY_URL`** and `VALKEY_PREFIX`, alongside
+ `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `NETWORK_EVENTS_SECRET`,
+ `RESEND_API_KEY`, `VAPID_PRIVATE_KEY`, `DB_PATH`, `FOLLOWS_AUTHORITY` and the port/host/base-URL names.
+ `fromUnits`: `NODE_ENV`, `PATH`.
+- `env-names network`: `/opt/openvibe.network/.env.example` declares 30 names, `DATABASE_URL`,
+ `DATABASE_DIRECT_URL`, `VALKEY_URL` and `DATA_DIR` among them.
+
+`DB_PATH` and `DATA_DIR` remain declared/set but are the legacy names the PostgreSQL release no longer reads
+for serving (`server/config.js`).
+
+## Row parity — pending
+
+The step-7 parity result for the production import cannot be established from the interfaces available.
+`scripts/pg-row-parity.js` needs the SQLite source (`$COPY`) and `DATABASE_DIRECT_URL` (or `DATABASE_URL`);
+no broker action exposes either, and none runs a query or lists a file. **Not checked.**
+
+What does exist is the rehearsal: `test/pg-cutover-rehearsal.test.js` runs the whole sequence
+(pg-preflight → `migrate-to-postgres.js` → row-count parity → server boot with `/api/ready` green) on a seeded
+`20dd7ff` fixture, on PGlite by default and on a scratch database under `npm run test:pg`. That proves the
+tools, not production's rows.
+
+## Valkey ACL scope — pending
+
+Only presence is provable: `VALKEY_URL` and `VALKEY_PREFIX` are set and non-empty (above). The scope of the
+Valkey user — which commands it holds and that it is confined to the service's key prefix — needs a
+connection to Valkey, which no read-only action opens. **Not checked.**
+
+## Rollback copies — pending
+
+The runbook keeps `$COPY` (`/var/backups/openvibe/network-pre-t2-<T0>.db`, `chmod 0400`) and the live
+`/opt/openvibe.network/data/network.db` untouched for 30 days as the rollback and audit trail. No read-only
+action lists or stats those paths: `show network` returns only the inventory entry
+(`network /opt/openvibe.network units: openvibe-network.service`) and `validate network` does not cover
+backups. **Not checked.**
+
+The nearest interface, `ov access run openvibe-ovh db-backup network`, is a medium (write) action that would
+take a *new* backup; it cannot prove the pre-T2 copy or its mode, so it was not run. Note also that, with the
+cutover done, the previous release is PostgreSQL-only: a rollback is a code rollback and does not by itself
+return serving to SQLite (see the runbook's [Rollback](cutover-t2-postgres.md#rollback) note).
+
+## Unresolved, owner-only
+
+- **Cause of the three rollbacks.** `releases` carries no reason and the journal is clipped to recent boots,
+ so this stays unconfirmed as the runbook says.
+- **Whether steps 3–7 ran**, and with what values for `$T0`, `$T1`, the `cleaned:` count and the parity
+ output: not observable from any read-only action.
+- **Functional checks** (runbook step 9.3–9.6: sign-in with a pre-cutover account, OAuth authorize on a
+ first-party site, notifications, the OpenCoins balance against `$COPY`): need an account and a browser and
+ were not run.
+
+## Reproduce
+
+```bash
+ov access run openvibe-ovh health network
+ov access run openvibe-ovh service-status network
+ov access run openvibe-ovh plan network
+ov access run openvibe-ovh releases network
+ov access run openvibe-ovh validate network
+ov access run openvibe-ovh env-set network
+ov access run openvibe-ovh env-names network
+ov access run openvibe-ovh show network
+ov access run openvibe-ovh journal openvibe-network.service 200
+```
+
+Every one of these is a low-risk read. `health`, `releases` and `validate` are granted to `rung:*`;
+`deploy`, `rollback`, `restart` and `db-backup` are not, and were not used.
@@ -1,14 +1,32 @@
@@ -1,14 +1,32 @@
# T2 cutover: Network from SQLite to PostgreSQL
# T2 cutover: Network from SQLite to PostgreSQL
-The production move of OpenVibe.Network from its SQLite file to PostgreSQL (plan T2, ADR-035). Production
-(`openvibe-ovh`) runs **20dd7ff**, the last SQLite release. Every later commit serves from PostgreSQL only:
-`DATABASE_URL` is required in production (there is no SQLite path), migrations run as the owner on
-`DATABASE_DIRECT_URL`, and the limit counters live on Valkey (`VALKEY_URL`). Two manual deploys of that range
-failed readiness and ovhost rolled them back. The most likely cause is that the server refused to boot without
-`DATABASE_URL` (`server/db/database.js`), but this is unconfirmed. This runbook covers the data move those
-deploys skipped. It was rehearsed with `npm run rehearse-pg-cutover` (`scripts/rehearse-pg-cutover.js`) on
-20dd7ff's full schema: the 51 tables `initDb()` makes plus the 26 its modules create at boot. That rehearsal
-found two import blockers, fixed in `scripts/migrate-to-postgres.js` with it:
+The production move of OpenVibe.Network from its SQLite file to PostgreSQL (plan T2, ADR-035). Every commit
+after **20dd7ff** serves from PostgreSQL only: `DATABASE_URL` is required in production (there is no SQLite
+path), migrations run as the owner on `DATABASE_DIRECT_URL`, and the limit counters live on Valkey
+(`VALKEY_URL`).
+
+**Observed state (read-only `ov access run openvibe-ovh`, 2026-10-03 UTC).** Production (`openvibe-ovh`) now
+runs **bad7b106fda0**, a PostgreSQL-only release: `health` reports it `ready` with `openvibe-network=active`
+and `validate` reports the checkout clean at that SHA. The serving code still refuses to boot in production
+without `DATABASE_URL` (`server/db/database.js`), so an active, ready release is one serving from PostgreSQL.
+`DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL` are present and non-empty in
+`/etc/openvibe/network.env` (mode `600`). The per-check detail, and what could not be checked, is in
+[cutover-evidence-t2.md](cutover-evidence-t2.md).
+
+**First-attempt history, kept.** Production ran **20dd7ff**, the last SQLite release, until 2026-10-02. The
+deployments of the PostgreSQL-only range did not all come up: `ovhost releases network` records
+`failed-rolled-back` for **5781a6b9daf5** (2026-10-01T18:56Z), **427e196f6276** (2026-10-02T01:10Z) and
+**2c6b8b5eae59** (2026-10-02T14:49Z), each followed by a rollback to 20dd7ff. (This runbook first recorded two
+failed attempts; the release log kept by ovhost shows three.) The first release in that range that stayed up
+was **757dc98b1b67** (2026-10-02T16:56Z), then **cbaacc48bd07**, **4e7b56b2046f** and the current
+**bad7b106fda0**. The leading explanation for the rollbacks — the server refused to boot without
+`DATABASE_URL` (`server/db/database.js`) — was never confirmed and still is not: the journal no longer reaches
+those windows, and `ovhost` records `failed-rolled-back` with no reason.
+
+This runbook covers the data move those first deploys skipped. It was rehearsed with
+`npm run rehearse-pg-cutover` (`scripts/rehearse-pg-cutover.js`) on 20dd7ff's full schema: the 51 tables
+`initDb()` makes plus the 26 its modules create at boot. That rehearsal found two import blockers, fixed in
+`scripts/migrate-to-postgres.js` with it:
- `analytics_rate_tracking` (openvibe-shared's old per-IP counters) has no PostgreSQL table, so the import refused
- `analytics_rate_tracking` (openvibe-shared's old per-IP counters) has no PostgreSQL table, so the import refused
to start. It is now in `SKIP_SOURCE`, so IPs are never carried over.
to start. It is now in `SKIP_SOURCE`, so IPs are never carried over.
@@ -16,9 +34,11 @@ found two import blockers, fixed in `scripts/migrate-to-postgres.js` with it:
@@ -16,9 +34,11 @@ found two import blockers, fixed in `scripts/migrate-to-postgres.js` with it:
`user_profile_changes` rows, and would have published one profile event per account. The import now turns
`user_profile_changes` rows, and would have published one profile event per account. The import now turns
that trigger off while it copies (`QUIET_TRIGGERS`).
that trigger off while it copies (`QUIET_TRIGGERS`).
-Every step ends with a **go / no-go** check. A no-go before step 8 (deploy) leaves production on SQLite with no
-data change: restart the old release (`sudo systemctl start openvibe-network`), reopen, and reschedule. A no-go
-from step 8 on means [rollback](#rollback).
+Every step ends with a **go / no-go** check. The steps are kept as the record of how the move was rehearsed and
+run; a re-run (a rebuild, or a second environment) starts from the Prerequisites with the then-current target
+SHA. At the time of the move, a no-go before step 8 (deploy) left production on SQLite with no data change:
+restart the old release (`sudo systemctl start openvibe-network`), reopen, and reschedule. A no-go from step 8
+on meant [rollback](#rollback).
Conventions:
Conventions:
@@ -89,8 +109,8 @@ These are done by the owner and never by an agent. Each must be true before the
@@ -89,8 +109,8 @@ These are done by the owner and never by an agent. Each must be true before the
## Step 1: stage the target release (T-1 day, no downtime)
## Step 1: stage the target release (T-1 day, no downtime)
-The running checkout `/opt/openvibe.network` stays on 20dd7ff until step 8. The import tools and migrations
-come from a separate checkout of the target commit. It is never served from.
+At the time of the move, the running checkout `/opt/openvibe.network` stayed on 20dd7ff until step 8. The import
+tools and migrations come from a separate checkout of the target commit. It is never served from.
```bash
```bash
TARGET=<merge sha>
TARGET=<merge sha>
@@ -270,7 +290,8 @@ At boot, the migrations find everything applied, and the seed adds only the boot
@@ -270,7 +290,8 @@ At boot, the migrations find everything applied, and the seed adds only the boot
**Go:** the deploy exits 0, and its release is `$TARGET`.
**Go:** the deploy exits 0, and its release is `$TARGET`.
**No-go:**
**No-go:**
-- **ovhost rolled back (exit 3).** Production is on 20dd7ff and SQLite again, and no write reached PostgreSQL.
+- **ovhost rolled back (exit 3).** At the time of the move, production was on 20dd7ff and SQLite again, and no
+ write had reached PostgreSQL.
Read `ovhost releases network` and the boot log
Read `ovhost releases network` and the boot log
(`sudo journalctl -u openvibe-network --since "$T0" | tail -50`). The log names its failure without printing
(`sudo journalctl -u openvibe-network --since "$T0" | tail -50`). The log names its failure without printing
URLs. Then close the window.
URLs. Then close the window.
@@ -324,13 +345,15 @@ rollback and the audit trail. `$STAGE` may be deleted.
@@ -324,13 +345,15 @@ rollback and the audit trail. `$STAGE` may be deleted.
ov access run openvibe-ovh rollback network
ov access run openvibe-ovh rollback network
```
```
-This brings back the previous release, **20dd7ff**, which serves from `data/network.db` again. Nothing in this
-runbook writes to that file: the import read `$COPY`, and the PostgreSQL release never opens `DB_PATH`. The file
-is exactly as it was at T0. The old release ignores `DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL`, so
-they stay in `network.env`.
+At the time of the move, this brought back the previous release, **20dd7ff**, which served from
+`data/network.db` again. Nothing in this runbook writes to that file: the import read `$COPY`, and the
+PostgreSQL release never opens `DB_PATH`. The file was exactly as it was at T0. The old release ignores
+`DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL`, so they stay in `network.env`. After the move
+succeeded, the previous release is a PostgreSQL-only one, so a rollback is a code rollback only — it does not
+by itself return serving to SQLite.
-**Go (rollback worked):** `ov access run openvibe-ovh health network` is healthy on 20dd7ff, and `/api/ready`
-answers 200. Sign-in works.
+**Go (rollback worked):** at the time of the move, `ov access run openvibe-ovh health network` was healthy on
+20dd7ff, and `/api/ready` answered 200. Sign-in worked.
**What is lost:** every write that reached PostgreSQL between T1 and the rollback. That includes:
**What is lost:** every write that reached PostgreSQL between T1 and the rollback. That includes:
- new accounts, and profile, username and password changes;
- new accounts, and profile, username and password changes;