openvibers/openvibe.chat Commits
Retire the Live-as-chat-authority rollback from Chat's runbook (#15)
Changes
@@@ -44,8 +44,8 @@ OV_LIVE_INTERNAL_URL=http://127.0.0.1:3000#LIVE_TIMEOUT_MS=4000# /ready reads the Live sync degraded once its last clean pass is older than this (ms)#LIVE_SYNC_STALE_MS=60000--# Copy Chat's changes into Live's chat tables (read mirror + rollback path). ON from the cutover;--# OFF for rehearsals (and Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat).++# Copy Chat's changes into Live's chat tables (read mirror for Live's remaining readers). ON from++# the cutover; OFF for rehearsals (Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat).LIVE_MIRROR=0#LIVE_MIRROR_INTERVAL_MS=1000@@@ -3,9 +3,10 @@> Rooms, messages, DMs, calls, TTS and audio queues, moderation and presence — one identity, every conversation.**Status:** alpha — deployed. Since the cutover on 2026-09-23 at 02:03 UTC (`docs/cutover.md`),--Chat is the authority for openvibe.live's chat: the service runs on `openvibe-ovh` (unit--`openvibe-chat`, 127.0.0.1:4400), Live runs with `CHAT_AUTHORITY=chat` and keeps a read mirror, and--chat events go to OpenVibe.Events. `openvibe.chat` is Chat's own site (global chat, messages, rooms,++Chat is the authority for openvibe.live's chat in every mode: the service runs on `openvibe-ovh`++(unit `openvibe-chat`, 127.0.0.1:4400), Live runs with `CHAT_AUTHORITY=chat`, mounts no local chat++routes and keeps a read mirror, and chat events go to OpenVibe.Events. `openvibe.chat` is Chat's own++site (global chat, messages, rooms,settings; the service manifest records it live since 2026-09-24).**Domain:** `openvibe.chat` ([deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)); Chatis also served on Live's origin: `https://openvibe.live/ws/chat`, `/api/chat/`, `/api/dm/`,@@@ -373,7 +374,7 @@ npm run test:pg # the same on PostgreSQL + PgBouncer + Valkey (OV_# OV_TEST_VALKEY_URL: openvibe-sdk scripts/test-services.sh up)node scripts/import-from-live.js --live-db /tmp/live-snapshot.db # dry run; --apply writesnode scripts/parity-check.js --live https://openvibe.live --chat http://127.0.0.1:4401 --before "…"--node scripts/mirror-flush.js # rollback helper: push queued mirror rows to Live++node scripts/mirror-flush.js # drain queued mirror rows to Live without the servicenode scripts/subscribe-events.js --dry-run # Chat's Events subscriptions (boot creates missing ones)node scripts/parity.js # chat parity scenarios: a dry run; --apply only on test accounts (docs/parity.md)N1_LIVE_REF=<live sha> npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit@@@ -554,8 +555,9 @@ The unit is `openvibe-chat.service` on `127.0.0.1:4400`, the env file `/etc/open`/ready` (not `/api/ready`). nginx: [deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)for openvibe.chat and [deploy/nginx/openvibe.live-chat.locations.conf](deploy/nginx/openvibe.live-chat.locations.conf),included in openvibe.live's vhost, for Live's chat paths. After a deploy, record the N-1 fixtures--(`npm run n-1:record`). Switching chat back to Live is in `docs/cutover.md` (with--`scripts/mirror-flush.js`).++(`npm run n-1:record`). Chat is required in every mode and Live's chat routes are retired: a++rollback is a Chat release rollback, not a switch back to Live (`docs/cutover.md`;++`scripts/mirror-flush.js` drains a stuck mirror).Rollback: ovhost puts the previous sha back by itself when `/ready` does not answer 2xx after therestart; afterwards `sudo ovhost rollback chat --to <sha>`. Nothing blocks a rollback: the schema@@@ -24,7 +24,7 @@"capabilitiesRegistered": true,"events": ["chat.message.created", "chat.message.deleted", "chat.dm.created", "chat.moderation.action", "chat.room.message.created", "chat.room.message.deleted"],"eventsConsumed": "live.release.deployed, network.module.updated, network.user.token_valid_after, vip.membership.changed, network.block.changed, network.subject.merged, network.account.export_requested and network.account.deleted at POST /internal/events (signature v2, openvibe-sdk inbox); subscriptions created at boot when missing (CHAT_EVENTS_SECRET and the Network grant chat events.subscription.manage openvibe.events)",-- "liveAuthorityFlag": "CHAT_AUTHORITY=chat is set in production live.env since the cutover (2026-09-23 02:03 UTC); Live's chat tables are a read mirror Chat keeps current",++ "liveAuthorityFlag": "CHAT_AUTHORITY=chat is required in every mode since the cutover (2026-09-23 02:03 UTC): Live mounts no local chat routes and there is no rollback to Live; Live's chat tables are a read mirror Chat keeps current for Live's remaining readers","vip": "Member badges from OpenVibe.VIP (server/vip/badges.js: entitlements/check with product chat, cached 60 s, never blocks sending, fails closed; vip.membership.changed drops the cache). Needs the Network grant chat vip.entitlement.check openvibe.vip","preferences": "Chat preferences in the Network user module chat.preferences, plus chat.tts_defaults, chat.dm_settings and chat.presence_prefs (server/prefs/: /api/chat/preferences and siblings with Chat's service token, cached 60 s, If-Match on every write; scripts/migrate-chat-preferences.js moves the choices in Live's user_preferences). Needs the grants chat network.modules.read/write","calls": "Live's call server, call routes and stream hooks ported to server/calls/ with a calls lifecycle table (WS-I task 1). Off unless CHAT_CALLS=1; the switch is CHAT_CALLS=1, Live CALLS_AUTHORITY=chat and the nginx include deploy/nginx/calls/openvibe.live-calls.locations.conf (docs/calls-cutover.md)",@@@ -4,7 +4,7 @@"owner": "live","status": "planned","visibility": "internal",-- "description": "OpenVibe.Chat copies every change to the tables it owns into Live's tables of the same name and ids (a read mirror for Live's remaining readers and the rollback path). Only the twelve chat tables; only columns Live has; Live-only columns are kept; refused unless CHAT_AUTHORITY=chat. Retired when Live stops reading chat tables.",++ "description": "OpenVibe.Chat copies every change to the tables it owns into Live's tables of the same name and ids (a read mirror for Live's remaining readers; there is no rollback to Live). Only the twelve chat tables; only columns Live has; Live-only columns are kept; refused unless CHAT_AUTHORITY=chat. Retired when Live stops reading chat tables.","permissions": ["live:write"],@@@ -2,8 +2,13 @@What changes at the cutover: browsers keep calling `https://openvibe.live`, but nginx sends the chatsocket and the chat REST prefixes to OpenVibe.Chat (127.0.0.1:4400), and Live runs with--`CHAT_AUTHORITY=chat`. Nothing in Live's browser code changes. Everything below is reversible--until the Live-side read mirror is retired (a later wave).++`CHAT_AUTHORITY=chat`. Nothing in Live's browser code changes. Chat is required in every mode:++Live's local chat routes and mounts are inert under the flag and are being removed, and the rollback++that returned chat to Live is retired, so there is no unset-flag mode in which Live serves chat++again (see [Rollback](#rollback)). The Live-side read mirror stays only for Live's own readers,++until a later wave retires them.++++The cutover itself ran on 2026-09-23; the rehearsal, flip and check steps below record that deploy.- [What is served where](#what-is-served-where)- [Data authority](#data-authority)@@@ -184,25 +189,29 @@ streamers; `deploy.sh --wait-idle` waits for none).- Live features that read chat: home page chat stats, a recap, AI viewers answering in a teststream, `/api/mod` chat logs and deletes (the deleted line disappears in Chat).- `node scripts/parity-check.js --live http://127.0.0.1:3000 --chat http://127.0.0.1:4400 …` is no-- longer meaningful (Live's chat routes answer 503 now); compare against the mirror instead with++ longer meaningful (Live mounts no chat routes); compare against the mirror instead with`sqlite3` counts: Live `chat_messages` ≈ Chat `chat_messages`.## Rollback--Live becomes the chat authority again on its own tables, which the mirror kept complete.----1. Remove the include from nginx; `sudo nginx -t && sudo systemctl reload nginx` (chat traffic goes-- to Live, which answers 503 until step 3 — clients retry).--2. Make sure everything Chat wrote is in Live: `curl -s 127.0.0.1:4400/ready | jq .mirror.pending`-- → 0. If the service cannot run: `cd /opt/openvibe.chat && sudo -u ubuntu env $(sudo cat-- /etc/openvibe/chat.env | xargs) node scripts/mirror-flush.js` (the env names Chat's database: `DATABASE_URL`)-- (exit 0 = drained). Live accepts mirror writes only while it still has the flag, so do this-- before step 3.--3. Remove `CHAT_AUTHORITY` from `/etc/openvibe/live.env`, restart Live. At boot Live applies any-- chat writes it had queued for Chat and never got acknowledged (`chat_bridge_outbox`).--4. `sudo systemctl stop openvibe-chat`. Keep `/var/lib/openvibe-chat/chat.db` (a later cutover starts-- from a fresh database and a fresh import; `subject_id` values are recomputed from Live's-- `linked_accounts`).++There is no rollback to Live. Chat is required in every mode: Live runs with `CHAT_AUTHORITY=chat`++and mounts no chat routes, so unsetting the flag would leave `/ws/chat` and the chat REST prefixes++unanswered, not served by Live. The nginx include stays in place through any rollback.++++1. A bad Chat release is undone on Chat's side: `sudo ovhost rollback chat --to <sha>` (README,++ "Deploys") restarts the previous release. nginx, the routing include and `/etc/openvibe/live.env`++ are untouched, so chat keeps being served — a socket reconnects after the restart. Nothing blocks++ this: the migrations are additive and the previous release reads the same database.++2. If Chat cannot run at all, chat is down until it is restored: Live has no copy of the sockets,++ the DMs, the TTS and sound queue or the moderation log to serve from. Live's own readers stay++ correct through the read mirror. Restore `openvibe-chat.service`, then let the mirror drain++ (`curl -s 127.0.0.1:4400/ready | jq .mirror.pending` → 0); when the service still cannot start,++ `cd /opt/openvibe.chat && sudo -u ubuntu env $(sudo cat /etc/openvibe/chat.env | xargs) node++ scripts/mirror-flush.js` (`DATABASE_URL` names Chat's database; exit 0 = drained).++3. Keep the database and the sounds. `/var/lib/openvibe-chat/chat.db` (PostgreSQL after the switch++ below) is the only copy of chat's own state — rooms, DMs and their read state, the audio queue,++ calls — and the clips under `SOUNDS_PATH` are the only copy of the sound files; keep both for++ any later recovery.## Behaviour that is not byte-for-byte identical@@@ -1,8 +1,8 @@#!/usr/bin/env node/*** Send whatever is still queued in live_mirror_outbox to Live, once, without starting the service.-- * For a rollback when the service cannot run: every change Chat made reaches Live's tables before-- * Live becomes the chat authority again (docs/cutover.md). Uses the same env as the service++ * A recovery helper when the service cannot run: Live's readers see every change Chat made++ * (docs/cutover.md). There is no rollback to Live to drain for. Uses the same env as the service* (/etc/openvibe/chat.env: DATABASE_URL, DATABASE_DIRECT_URL); LIVE_MIRROR is forced on.** node scripts/mirror-flush.js → { sent, pending, error }@@@ -2,9 +2,10 @@* OpenVibe.Chat → OpenVibe.Live read mirror.** After the cutover Chat is the only writer of its tables, but Live still reads some of them in-- * place (home-page stats, recaps, AI chat context, VOD chat replay, /api/mod queues, analytics),-- * and rollback means Live becomes the authority again. So every change Chat makes to its tables-- * (C-02) is copied into Live's tables of the same name, same ids:++ * place (home-page stats, recaps, AI chat context, VOD chat replay, /api/mod queues, analytics).++ * Chat is required in every mode and Live's local chat routes are retired, so this is no longer a++ * rollback path: it exists for Live's remaining readers until a later wave retires them. Every++ * change Chat makes to its tables (C-02) is copied into Live's tables of the same name, same ids:** - PostgreSQL triggers on the twelve tables (migrations/0001_initial.sql) record each change in* live_mirror_outbox in the writing transaction — skipped where the transaction set ov.mirror_skip = '1'@@@ -15,8 +16,8 @@* - Live applies them idempotently (INSERT … ON CONFLICT DO UPDATE on the columns it has; it* refuses unless it runs with CHAT_AUTHORITY=chat, so a rehearsal can never touch production).*-- * Rows leave the outbox only when Live acknowledged them; a Live outage just queues. Before a-- * rollback, `pending()` must be 0 (docs/cutover.md).++ * Rows leave the outbox only when Live acknowledged them; a Live outage just queues. `pending()`++ * must be 0 before Live's chat-table readers (and this mirror) are retired (docs/cutover.md).*/'use strict';@@@ -80,8 +80,9 @@ module.exports = {live: {internalUrl: strip(process.env.OV_LIVE_INTERNAL_URL || 'http://127.0.0.1:3000'),audience: 'openvibe.live',-- // Chat's read-mirror of its tables into Live (rollback path + Live's own readers). Only-- // switch on at cutover; Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat.++ // Chat's read-mirror of its tables into Live (for Live's own readers; there is no rollback++ // path to Live any more). Switch on at cutover; Live refuses mirror writes unless it runs++ // with CHAT_AUTHORITY=chat, so a rehearsal can never touch production.mirror: bool(process.env.LIVE_MIRROR, false),mirrorIntervalMs: int(process.env.LIVE_MIRROR_INTERVAL_MS, 1000),requestTimeoutMs: int(process.env.LIVE_TIMEOUT_MS, 4000),@@@ -116,7 +116,7 @@ t('mirror: Live refusing (not yet CHAT_AUTHORITY=chat) keeps the queue; acceptedassert.ok(h.tokenRequests.some((x) => x.audience === 'openvibe.live' && x.scope === 'live.chat_mirror.write'));});--t('scripts/mirror-flush.js drains the queue without the service (rollback)', async () => {++t('scripts/mirror-flush.js drains the queue without the service', async () => {const { spawn } = require('child_process');const path = require('path');h.live.mirrorStatus = 409;
@@ -44,8 +44,8 @@ OV_LIVE_INTERNAL_URL=http://127.0.0.1:3000
@@ -44,8 +44,8 @@ OV_LIVE_INTERNAL_URL=http://127.0.0.1:3000
#LIVE_TIMEOUT_MS=4000
#LIVE_TIMEOUT_MS=4000
# /ready reads the Live sync degraded once its last clean pass is older than this (ms)
# /ready reads the Live sync degraded once its last clean pass is older than this (ms)
#LIVE_SYNC_STALE_MS=60000
#LIVE_SYNC_STALE_MS=60000
-# Copy Chat's changes into Live's chat tables (read mirror + rollback path). ON from the cutover;
-# OFF for rehearsals (and Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat).
+# Copy Chat's changes into Live's chat tables (read mirror for Live's remaining readers). ON from
+# the cutover; OFF for rehearsals (Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat).
LIVE_MIRROR=0
LIVE_MIRROR=0
#LIVE_MIRROR_INTERVAL_MS=1000
#LIVE_MIRROR_INTERVAL_MS=1000
@@ -3,9 +3,10 @@
@@ -3,9 +3,10 @@
> Rooms, messages, DMs, calls, TTS and audio queues, moderation and presence — one identity, every conversation.
> Rooms, messages, DMs, calls, TTS and audio queues, moderation and presence — one identity, every conversation.
**Status:** alpha — deployed. Since the cutover on 2026-09-23 at 02:03 UTC (`docs/cutover.md`),
**Status:** alpha — deployed. Since the cutover on 2026-09-23 at 02:03 UTC (`docs/cutover.md`),
-Chat is the authority for openvibe.live's chat: the service runs on `openvibe-ovh` (unit
-`openvibe-chat`, 127.0.0.1:4400), Live runs with `CHAT_AUTHORITY=chat` and keeps a read mirror, and
-chat events go to OpenVibe.Events. `openvibe.chat` is Chat's own site (global chat, messages, rooms,
+Chat is the authority for openvibe.live's chat in every mode: the service runs on `openvibe-ovh`
+(unit `openvibe-chat`, 127.0.0.1:4400), Live runs with `CHAT_AUTHORITY=chat`, mounts no local chat
+routes and keeps a read mirror, and chat events go to OpenVibe.Events. `openvibe.chat` is Chat's own
+site (global chat, messages, rooms,
settings; the service manifest records it live since 2026-09-24).
settings; the service manifest records it live since 2026-09-24).
**Domain:** `openvibe.chat` ([deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)); Chat
**Domain:** `openvibe.chat` ([deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)); Chat
is also served on Live's origin: `https://openvibe.live/ws/chat`, `/api/chat/`, `/api/dm/`,
is also served on Live's origin: `https://openvibe.live/ws/chat`, `/api/chat/`, `/api/dm/`,
@@ -373,7 +374,7 @@ npm run test:pg # the same on PostgreSQL + PgBouncer + Valkey (OV_
@@ -373,7 +374,7 @@ npm run test:pg # the same on PostgreSQL + PgBouncer + Valkey (OV_
# OV_TEST_VALKEY_URL: openvibe-sdk scripts/test-services.sh up)
# OV_TEST_VALKEY_URL: openvibe-sdk scripts/test-services.sh up)
node scripts/import-from-live.js --live-db /tmp/live-snapshot.db # dry run; --apply writes
node scripts/import-from-live.js --live-db /tmp/live-snapshot.db # dry run; --apply writes
node scripts/parity-check.js --live https://openvibe.live --chat http://127.0.0.1:4401 --before "…"
node scripts/parity-check.js --live https://openvibe.live --chat http://127.0.0.1:4401 --before "…"
-node scripts/mirror-flush.js # rollback helper: push queued mirror rows to Live
+node scripts/mirror-flush.js # drain queued mirror rows to Live without the service
node scripts/subscribe-events.js --dry-run # Chat's Events subscriptions (boot creates missing ones)
node scripts/subscribe-events.js --dry-run # Chat's Events subscriptions (boot creates missing ones)
node scripts/parity.js # chat parity scenarios: a dry run; --apply only on test accounts (docs/parity.md)
node scripts/parity.js # chat parity scenarios: a dry run; --apply only on test accounts (docs/parity.md)
N1_LIVE_REF=<live sha> npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit
N1_LIVE_REF=<live sha> npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit
@@ -554,8 +555,9 @@ The unit is `openvibe-chat.service` on `127.0.0.1:4400`, the env file `/etc/open
@@ -554,8 +555,9 @@ The unit is `openvibe-chat.service` on `127.0.0.1:4400`, the env file `/etc/open
`/ready` (not `/api/ready`). nginx: [deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)
`/ready` (not `/api/ready`). nginx: [deploy/nginx/openvibe.chat.conf](deploy/nginx/openvibe.chat.conf)
for openvibe.chat and [deploy/nginx/openvibe.live-chat.locations.conf](deploy/nginx/openvibe.live-chat.locations.conf),
for openvibe.chat and [deploy/nginx/openvibe.live-chat.locations.conf](deploy/nginx/openvibe.live-chat.locations.conf),
included in openvibe.live's vhost, for Live's chat paths. After a deploy, record the N-1 fixtures
included in openvibe.live's vhost, for Live's chat paths. After a deploy, record the N-1 fixtures
-(`npm run n-1:record`). Switching chat back to Live is in `docs/cutover.md` (with
-`scripts/mirror-flush.js`).
+(`npm run n-1:record`). Chat is required in every mode and Live's chat routes are retired: a
+rollback is a Chat release rollback, not a switch back to Live (`docs/cutover.md`;
+`scripts/mirror-flush.js` drains a stuck mirror).
Rollback: ovhost puts the previous sha back by itself when `/ready` does not answer 2xx after the
Rollback: ovhost puts the previous sha back by itself when `/ready` does not answer 2xx after the
restart; afterwards `sudo ovhost rollback chat --to <sha>`. Nothing blocks a rollback: the schema
restart; afterwards `sudo ovhost rollback chat --to <sha>`. Nothing blocks a rollback: the schema
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
"capabilitiesRegistered": true,
"capabilitiesRegistered": true,
"events": ["chat.message.created", "chat.message.deleted", "chat.dm.created", "chat.moderation.action", "chat.room.message.created", "chat.room.message.deleted"],
"events": ["chat.message.created", "chat.message.deleted", "chat.dm.created", "chat.moderation.action", "chat.room.message.created", "chat.room.message.deleted"],
"eventsConsumed": "live.release.deployed, network.module.updated, network.user.token_valid_after, vip.membership.changed, network.block.changed, network.subject.merged, network.account.export_requested and network.account.deleted at POST /internal/events (signature v2, openvibe-sdk inbox); subscriptions created at boot when missing (CHAT_EVENTS_SECRET and the Network grant chat events.subscription.manage openvibe.events)",
"eventsConsumed": "live.release.deployed, network.module.updated, network.user.token_valid_after, vip.membership.changed, network.block.changed, network.subject.merged, network.account.export_requested and network.account.deleted at POST /internal/events (signature v2, openvibe-sdk inbox); subscriptions created at boot when missing (CHAT_EVENTS_SECRET and the Network grant chat events.subscription.manage openvibe.events)",
- "liveAuthorityFlag": "CHAT_AUTHORITY=chat is set in production live.env since the cutover (2026-09-23 02:03 UTC); Live's chat tables are a read mirror Chat keeps current",
+ "liveAuthorityFlag": "CHAT_AUTHORITY=chat is required in every mode since the cutover (2026-09-23 02:03 UTC): Live mounts no local chat routes and there is no rollback to Live; Live's chat tables are a read mirror Chat keeps current for Live's remaining readers",
"vip": "Member badges from OpenVibe.VIP (server/vip/badges.js: entitlements/check with product chat, cached 60 s, never blocks sending, fails closed; vip.membership.changed drops the cache). Needs the Network grant chat vip.entitlement.check openvibe.vip",
"vip": "Member badges from OpenVibe.VIP (server/vip/badges.js: entitlements/check with product chat, cached 60 s, never blocks sending, fails closed; vip.membership.changed drops the cache). Needs the Network grant chat vip.entitlement.check openvibe.vip",
"preferences": "Chat preferences in the Network user module chat.preferences, plus chat.tts_defaults, chat.dm_settings and chat.presence_prefs (server/prefs/: /api/chat/preferences and siblings with Chat's service token, cached 60 s, If-Match on every write; scripts/migrate-chat-preferences.js moves the choices in Live's user_preferences). Needs the grants chat network.modules.read/write",
"preferences": "Chat preferences in the Network user module chat.preferences, plus chat.tts_defaults, chat.dm_settings and chat.presence_prefs (server/prefs/: /api/chat/preferences and siblings with Chat's service token, cached 60 s, If-Match on every write; scripts/migrate-chat-preferences.js moves the choices in Live's user_preferences). Needs the grants chat network.modules.read/write",
"calls": "Live's call server, call routes and stream hooks ported to server/calls/ with a calls lifecycle table (WS-I task 1). Off unless CHAT_CALLS=1; the switch is CHAT_CALLS=1, Live CALLS_AUTHORITY=chat and the nginx include deploy/nginx/calls/openvibe.live-calls.locations.conf (docs/calls-cutover.md)",
"calls": "Live's call server, call routes and stream hooks ported to server/calls/ with a calls lifecycle table (WS-I task 1). Off unless CHAT_CALLS=1; the switch is CHAT_CALLS=1, Live CALLS_AUTHORITY=chat and the nginx include deploy/nginx/calls/openvibe.live-calls.locations.conf (docs/calls-cutover.md)",
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"owner": "live",
"owner": "live",
"status": "planned",
"status": "planned",
"visibility": "internal",
"visibility": "internal",
- "description": "OpenVibe.Chat copies every change to the tables it owns into Live's tables of the same name and ids (a read mirror for Live's remaining readers and the rollback path). Only the twelve chat tables; only columns Live has; Live-only columns are kept; refused unless CHAT_AUTHORITY=chat. Retired when Live stops reading chat tables.",
+ "description": "OpenVibe.Chat copies every change to the tables it owns into Live's tables of the same name and ids (a read mirror for Live's remaining readers; there is no rollback to Live). Only the twelve chat tables; only columns Live has; Live-only columns are kept; refused unless CHAT_AUTHORITY=chat. Retired when Live stops reading chat tables.",
"permissions": [
"permissions": [
"live:write"
"live:write"
],
],
@@ -2,8 +2,13 @@
@@ -2,8 +2,13 @@
What changes at the cutover: browsers keep calling `https://openvibe.live`, but nginx sends the chat
What changes at the cutover: browsers keep calling `https://openvibe.live`, but nginx sends the chat
socket and the chat REST prefixes to OpenVibe.Chat (127.0.0.1:4400), and Live runs with
socket and the chat REST prefixes to OpenVibe.Chat (127.0.0.1:4400), and Live runs with
-`CHAT_AUTHORITY=chat`. Nothing in Live's browser code changes. Everything below is reversible
-until the Live-side read mirror is retired (a later wave).
+`CHAT_AUTHORITY=chat`. Nothing in Live's browser code changes. Chat is required in every mode:
+Live's local chat routes and mounts are inert under the flag and are being removed, and the rollback
+that returned chat to Live is retired, so there is no unset-flag mode in which Live serves chat
+again (see [Rollback](#rollback)). The Live-side read mirror stays only for Live's own readers,
+until a later wave retires them.
+
+The cutover itself ran on 2026-09-23; the rehearsal, flip and check steps below record that deploy.
- [What is served where](#what-is-served-where)
- [What is served where](#what-is-served-where)
- [Data authority](#data-authority)
- [Data authority](#data-authority)
@@ -184,25 +189,29 @@ streamers; `deploy.sh --wait-idle` waits for none).
@@ -184,25 +189,29 @@ streamers; `deploy.sh --wait-idle` waits for none).
- Live features that read chat: home page chat stats, a recap, AI viewers answering in a test
- Live features that read chat: home page chat stats, a recap, AI viewers answering in a test
stream, `/api/mod` chat logs and deletes (the deleted line disappears in Chat).
stream, `/api/mod` chat logs and deletes (the deleted line disappears in Chat).
- `node scripts/parity-check.js --live http://127.0.0.1:3000 --chat http://127.0.0.1:4400 …` is no
- `node scripts/parity-check.js --live http://127.0.0.1:3000 --chat http://127.0.0.1:4400 …` is no
- longer meaningful (Live's chat routes answer 503 now); compare against the mirror instead with
+ longer meaningful (Live mounts no chat routes); compare against the mirror instead with
`sqlite3` counts: Live `chat_messages` ≈ Chat `chat_messages`.
`sqlite3` counts: Live `chat_messages` ≈ Chat `chat_messages`.
## Rollback
## Rollback
-Live becomes the chat authority again on its own tables, which the mirror kept complete.
-
-1. Remove the include from nginx; `sudo nginx -t && sudo systemctl reload nginx` (chat traffic goes
- to Live, which answers 503 until step 3 — clients retry).
-2. Make sure everything Chat wrote is in Live: `curl -s 127.0.0.1:4400/ready | jq .mirror.pending`
- → 0. If the service cannot run: `cd /opt/openvibe.chat && sudo -u ubuntu env $(sudo cat
- /etc/openvibe/chat.env | xargs) node scripts/mirror-flush.js` (the env names Chat's database: `DATABASE_URL`)
- (exit 0 = drained). Live accepts mirror writes only while it still has the flag, so do this
- before step 3.
-3. Remove `CHAT_AUTHORITY` from `/etc/openvibe/live.env`, restart Live. At boot Live applies any
- chat writes it had queued for Chat and never got acknowledged (`chat_bridge_outbox`).
-4. `sudo systemctl stop openvibe-chat`. Keep `/var/lib/openvibe-chat/chat.db` (a later cutover starts
- from a fresh database and a fresh import; `subject_id` values are recomputed from Live's
- `linked_accounts`).
+There is no rollback to Live. Chat is required in every mode: Live runs with `CHAT_AUTHORITY=chat`
+and mounts no chat routes, so unsetting the flag would leave `/ws/chat` and the chat REST prefixes
+unanswered, not served by Live. The nginx include stays in place through any rollback.
+
+1. A bad Chat release is undone on Chat's side: `sudo ovhost rollback chat --to <sha>` (README,
+ "Deploys") restarts the previous release. nginx, the routing include and `/etc/openvibe/live.env`
+ are untouched, so chat keeps being served — a socket reconnects after the restart. Nothing blocks
+ this: the migrations are additive and the previous release reads the same database.
+2. If Chat cannot run at all, chat is down until it is restored: Live has no copy of the sockets,
+ the DMs, the TTS and sound queue or the moderation log to serve from. Live's own readers stay
+ correct through the read mirror. Restore `openvibe-chat.service`, then let the mirror drain
+ (`curl -s 127.0.0.1:4400/ready | jq .mirror.pending` → 0); when the service still cannot start,
+ `cd /opt/openvibe.chat && sudo -u ubuntu env $(sudo cat /etc/openvibe/chat.env | xargs) node
+ scripts/mirror-flush.js` (`DATABASE_URL` names Chat's database; exit 0 = drained).
+3. Keep the database and the sounds. `/var/lib/openvibe-chat/chat.db` (PostgreSQL after the switch
+ below) is the only copy of chat's own state — rooms, DMs and their read state, the audio queue,
+ calls — and the clips under `SOUNDS_PATH` are the only copy of the sound files; keep both for
+ any later recovery.
## Behaviour that is not byte-for-byte identical
## Behaviour that is not byte-for-byte identical
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
#!/usr/bin/env node
#!/usr/bin/env node
/**
/**
* Send whatever is still queued in live_mirror_outbox to Live, once, without starting the service.
* Send whatever is still queued in live_mirror_outbox to Live, once, without starting the service.
- * For a rollback when the service cannot run: every change Chat made reaches Live's tables before
- * Live becomes the chat authority again (docs/cutover.md). Uses the same env as the service
+ * A recovery helper when the service cannot run: Live's readers see every change Chat made
+ * (docs/cutover.md). There is no rollback to Live to drain for. Uses the same env as the service
* (/etc/openvibe/chat.env: DATABASE_URL, DATABASE_DIRECT_URL); LIVE_MIRROR is forced on.
* (/etc/openvibe/chat.env: DATABASE_URL, DATABASE_DIRECT_URL); LIVE_MIRROR is forced on.
*
*
* node scripts/mirror-flush.js → { sent, pending, error }
* node scripts/mirror-flush.js → { sent, pending, error }
@@ -2,9 +2,10 @@
@@ -2,9 +2,10 @@
* OpenVibe.Chat → OpenVibe.Live read mirror.
* OpenVibe.Chat → OpenVibe.Live read mirror.
*
*
* After the cutover Chat is the only writer of its tables, but Live still reads some of them in
* After the cutover Chat is the only writer of its tables, but Live still reads some of them in
- * place (home-page stats, recaps, AI chat context, VOD chat replay, /api/mod queues, analytics),
- * and rollback means Live becomes the authority again. So every change Chat makes to its tables
- * (C-02) is copied into Live's tables of the same name, same ids:
+ * place (home-page stats, recaps, AI chat context, VOD chat replay, /api/mod queues, analytics).
+ * Chat is required in every mode and Live's local chat routes are retired, so this is no longer a
+ * rollback path: it exists for Live's remaining readers until a later wave retires them. Every
+ * change Chat makes to its tables (C-02) is copied into Live's tables of the same name, same ids:
*
*
* - PostgreSQL triggers on the twelve tables (migrations/0001_initial.sql) record each change in
* - PostgreSQL triggers on the twelve tables (migrations/0001_initial.sql) record each change in
* live_mirror_outbox in the writing transaction — skipped where the transaction set ov.mirror_skip = '1'
* live_mirror_outbox in the writing transaction — skipped where the transaction set ov.mirror_skip = '1'
@@ -15,8 +16,8 @@
@@ -15,8 +16,8 @@
* - Live applies them idempotently (INSERT … ON CONFLICT DO UPDATE on the columns it has; it
* - Live applies them idempotently (INSERT … ON CONFLICT DO UPDATE on the columns it has; it
* refuses unless it runs with CHAT_AUTHORITY=chat, so a rehearsal can never touch production).
* refuses unless it runs with CHAT_AUTHORITY=chat, so a rehearsal can never touch production).
*
*
- * Rows leave the outbox only when Live acknowledged them; a Live outage just queues. Before a
- * rollback, `pending()` must be 0 (docs/cutover.md).
+ * Rows leave the outbox only when Live acknowledged them; a Live outage just queues. `pending()`
+ * must be 0 before Live's chat-table readers (and this mirror) are retired (docs/cutover.md).
*/
*/
'use strict';
'use strict';
@@ -80,8 +80,9 @@ module.exports = {
@@ -80,8 +80,9 @@ module.exports = {
live: {
live: {
internalUrl: strip(process.env.OV_LIVE_INTERNAL_URL || 'http://127.0.0.1:3000'),
internalUrl: strip(process.env.OV_LIVE_INTERNAL_URL || 'http://127.0.0.1:3000'),
audience: 'openvibe.live',
audience: 'openvibe.live',
- // Chat's read-mirror of its tables into Live (rollback path + Live's own readers). Only
- // switch on at cutover; Live refuses mirror writes unless it runs with CHAT_AUTHORITY=chat.
+ // Chat's read-mirror of its tables into Live (for Live's own readers; there is no rollback
+ // path to Live any more). Switch on at cutover; Live refuses mirror writes unless it runs
+ // with CHAT_AUTHORITY=chat, so a rehearsal can never touch production.
mirror: bool(process.env.LIVE_MIRROR, false),
mirror: bool(process.env.LIVE_MIRROR, false),
mirrorIntervalMs: int(process.env.LIVE_MIRROR_INTERVAL_MS, 1000),
mirrorIntervalMs: int(process.env.LIVE_MIRROR_INTERVAL_MS, 1000),
requestTimeoutMs: int(process.env.LIVE_TIMEOUT_MS, 4000),
requestTimeoutMs: int(process.env.LIVE_TIMEOUT_MS, 4000),
@@ -116,7 +116,7 @@ t('mirror: Live refusing (not yet CHAT_AUTHORITY=chat) keeps the queue; accepted
@@ -116,7 +116,7 @@ t('mirror: Live refusing (not yet CHAT_AUTHORITY=chat) keeps the queue; accepted
assert.ok(h.tokenRequests.some((x) => x.audience === 'openvibe.live' && x.scope === 'live.chat_mirror.write'));
assert.ok(h.tokenRequests.some((x) => x.audience === 'openvibe.live' && x.scope === 'live.chat_mirror.write'));
});
});
-t('scripts/mirror-flush.js drains the queue without the service (rollback)', async () => {
+t('scripts/mirror-flush.js drains the queue without the service', async () => {
const { spawn } = require('child_process');
const { spawn } = require('child_process');
const path = require('path');
const path = require('path');
h.live.mirrorStatus = 409;
h.live.mirrorStatus = 409;