openvibers/openvibe.host Commits
Wave 22 operator docs: compatibility register, storage state, final cutover runbook
Changes
nnew file mode 100644@@@ -0,0 +1,148 @@++# Compatibility register (roadmap Wave 22)++++The Wave 22 exit rule is: "every compatibility shim is gone or has a dated, owned removal plan". This page++lists every compatibility shim, legacy route, dual write or mirror, rollback flag and legacy secret found++in the estate on 2026-09-23. For each one it gives the owner, the removal condition, a target date and the++rollback lever that the shim keeps available until it is removed.++++**How this was built.** Source was read in every repository under `~/OpenVibers` and in Live at++`~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`), excluding `node_modules`. The search terms were++`_AUTHORITY`, `_LEGACY`, `legacy`, `deprecated`, `compat`, `fallback`, `shim`, `INTERNAL_API_KEY`,++`X-Internal-Key` and `frozen`. Paths are relative to each repository, and `file:N` is a line number.++Production facts come from the operator notes of 2026-09-23.++++**Conditions and dates.**++- *documented*: the condition is quoted from the cited doc or ADR.++- *proposed*: the condition was set by this register. No repo recorded one before.++- Every date is a target. A target moves only when this page is edited, and the edit gives the reason.++- Where a condition needs telemetry that does not exist yet, the entry says so.++++**Owner.** The owner is the service that must make the change. In an entry, "owner" alone means that++service's maintainers, which today means the OpenVibe operator (`OpenVibers`).++++Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compatibility/deprecations.json`,++`compatibility/aliases.json` are both empty).++++## Summary++++| Status on 2026-09-23 | Entries |++|---|---|++| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |++| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRe) |++| Running in parallel, waiting on telemetry | C-50–C-58 (internal key), C-60–C-61 (Events v1 signatures) |++| Legacy secret or credential still accepted | C-50–C-57 (`INTERNAL_API_KEY`), C-59 (hard-coded), C-42, C-70, C-71 (Media app keys), Games `EDITOR_KEY` |++| Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |++| No removal condition in any repo before this register (proposed here) | C-06, C-14, C-24, C-54, C-58, C-59, C-70, C-73, C-74, C-77, C-80, C-82, Games `EDITOR_KEY` |++++## Chat (Wave 6: cut over 2026-09-23 02:03 UTC)++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-01 | **`CHAT_AUTHORITY`**, Live's switch. Unset: Live runs chat itself. `chat`: `/api/chat`, `/api/dm`, `/api/tts`, `/api/sounds` answer 503, `/ws/chat` is refused, and Live's remaining chat writers forward to Chat. Production: `chat`. | Live `server/chat/chat-authority.js:19-21`, `server/chat/chat-remote.js`, `server/index.js:644-691` | Live | *documented*: the Live read mirror "retires in a later wave" (ADR-010 §Migration consequences; Chat `docs/cutover.md:5-6`). *Proposed*: every Live reader of the mirror tables reads Chat's API instead, and C-02/C-03 have both drained with no rollback for 30 days. | 2026-11-06 | Chat `docs/cutover.md:169-184`: remove the nginx include, drain the mirror, unset `CHAT_AUTHORITY`, restart Live. |++| C-02 | **Live chat read mirror.** Chat pushes 12 tables into Live's database: `chat_messages`, `dm_*` (5 tables), `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`. Chat's `LIVE_MIRROR=1` (default 0) drives it through `live_mirror_outbox`. It arrives at Live's `POST /internal/chat-effects/mirror`, capability `live.chat_mirror.write`. | Chat `server/config.js:54`, `server/bridge/live-mirror.js`, `server/db/database.js:25-38`; Live `server/chat/live-context-routes.js:599-659` | Chat (sender), Live (tables) | Same as C-01. Then set `LIVE_MIRROR=0`, drop the mirror tables from Live after a backup, and remove the route. | 2026-11-06 | The mirror is what makes C-01's rollback lossless. `scripts/mirror-flush.js` drains it if Chat is down (Chat `docs/cutover.md:175-179`). |++| C-03 | **`chat_bridge_outbox`**: Live's forwarded writes to Chat. It is replayed on boot. With `CHAT_AUTHORITY` unset, `drainToLocal()` applies unacknowledged rows to Live's own tables. | Live `server/chat/chat-remote.js:93-122, 237-248, 351-372`, `server/index.js:1074-1075` | Live | *proposed*: Live's remaining chat writers call Chat directly, and the outbox has been empty for 14 days. | 2026-11-06 | `drainToLocal()` on rollback. The code warns that a write Chat applied but never acknowledged gets applied twice (`chat-remote.js:354`). |++| C-04 | **Staged tables still owned by Live** (`table_authority = 'live'`): `channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`. | Chat `server/db/database.js:39-47,116-118`; Chat `docs/cutover.md:43`; ADR-010 :17-19 | Chat + Live | *documented*: they move "when their Live writers move" (ADR-010). | 2026-12-18 | They are not moved yet, so Live stays their writer. |++| C-05 | **Legacy chat auth**: the `token` cookie ("legacy openvibelive") next to `ov_token`, plus a `?token=` query fallback on the WebSocket. | Chat `server/auth/auth.js:43-47,62-63,86-87` | Chat | *proposed*: the Live SPA sends only `ov_token`. Needs a counter of cookie and query use, which does not exist yet. | 2026-10-23 | Accepting both keeps old tabs working. |++| C-06 | **REST send fallback** `POST /api/chat/send`, and the legacy `?scope=stream`. | Chat `server/chat/routes.js:7,227,490` | Chat | *proposed*: no client sends them for 14 days. Unknown whether any does: check Chat's access log. | 2026-11-06 | none needed |++++## Pastes (Wave 5: Community is the authority since 2026-09-22 ~23:35 UTC)++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-10 | **Community `PASTES_AUTHORITY`**. The code default is `live`, which proxies to Live and Media. Production is `community` (set in `/etc/openvibe/community.env`). | Community `server/config.js:51-53`, `server/app.js:127-151`, `README.md:16-21` | Community | *documented*: the old Media paste code and Live's Media paste router "stay until usage telemetry says they're dead" (operator notes, Wave 5). *Proposed*: Media's 410 on paste writes (C-12) and Live's Media path (C-11) are both unused for 30 days. Then make `community` the only mode. | 2026-11-06 | ADR-011 §Rollback: set `PASTES_AUTHORITY=live` in Community and Live, remove Media's two flags, restart. Export Community-side writes first. **No Community→Media export tool exists.** |++| C-11 | **Live `PASTES_AUTHORITY=community`** plus the Media paste router it replaces. Unset sends `/api/pastes` to Media. The value is read on every request. `PASTES_ON_COMMUNITY=1` 301s `/p/:slug` and `/pastes` to Community. `PASTES_AUTHORITY` and `OV_COMMUNITY_INTERNAL_URL` are missing from `.env.example`; `PASTES_ON_COMMUNITY=0` is at `.env.example:130-132`. | Live `server/pastes-client.js:5-18`, `server/media-proxy/pastes.js:250-319`, `server/index.js:597-612` | Live | Same as C-10. | 2026-11-06 | Unset `PASTES_AUTHORITY` (no restart needed). `PASTES_ON_COMMUNITY=0` needs a restart, and browsers may have cached the 301s. |++| C-12 | **Media paste writes frozen**: `PASTES_FROZEN_APPS=live` makes every paste write answer 410 `pastes.moved`. `PASTES_MOVED_TO=https://openvibe.community` 301s `/p/:slug` and `/p/:slug/raw`. Screenshot bytes are still served by Media. | Media `server/pastes/routes.js:47-55`, `server/public/routes.js:446-494` | Media | *proposed*: the 410 path is unused for 30 days. The 301s stay as permanent redirects, because old links exist. Delete the paste write code, keep the redirect. | 2026-11-06 (write code); redirects kept | Remove both flags (ADR-011 §Rollback). |++| C-13 | **Tools paste forwarding**: `/api/pastes/*` goes to Community (`OV_COMMUNITY_INTERNAL_URL`) with the visitor's `ov_token` and no service token. `pastes.openvibe.tools` redirects to Community. **There is no flag.** Commit `fd5d318` (2026-09-22) replaced the Live proxy, and only a revert rolls it back. | Tools `apps/gateway/server/index.js:183-207, 296-309` | Tools | *proposed*: keep the forwarding as the product path, and remove the stale comments that say Media backs pastes (`apps/gateway/server/index.js:8,12`, `apps/gateway/README.md:17-59`). `LIVE_URL` is dead apart from the startup banner (`apps/gateway/server/config.js:35-39`). Remove it. | 2026-10-09 | `git revert fd5d318` in Tools, then deploy. |++| C-14 | **Community numeric thread ids for services.** A `/^\d{1,15}$/` id resolves only for service tokens. `threadIdFor` hands services the sequential id and browsers the `cth_` access id. The documented service callers are Blog, Wiki, Deals, Reviews and Live. | Community `server/comments/service.js:63-69,104-110`, `server/comments/api.js:28-40`, `README.md:174-178`; tests `test/comments.test.js:271-340` | Community | **No removal condition in the repo.** The code treats it as the intended design. *Proposed*: every service caller stores and sends `cth_` ids; then refuse numeric ids. This needs a per-principal counter of numeric lookups, which does not exist yet. | 2026-12-18 | Numeric ids keep existing service rows working. |++| C-15 | **Community legacy id columns**: `pastes.legacy_media_id`, `legacy_user_id`, `paste_comments.legacy_media_id`, and `legacy_id_map`. The map also serves old JWTs that have no `subject_id`. The staff censor route also falls back to a numeric id. | Community `server/db.js:49-50,84,118-125`, `server/identity/network.js:90-103`, `server/pastes/service.js:593-596` | Community | The columns are import provenance and stay. *Proposed*: the JWT fallback goes once Network tokens older than their maximum lifetime cannot exist (every token carries `subject_id` since Wave 1, ADR-001). | 2026-10-23 (JWT fallback) | none needed |++++## AI (Wave 13: `AI_SERVICE=remote` in production since 2026-09-23 ~15:40 UTC)++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-20 | **`AI_SERVICE`**, Live's switch. `remote` runs `llm.complete`, translation, paste/frame, overview, media and recap calls as OpenVibe.AI workflows. Unset uses Live's own provider key. | Live `server/ai/ai-service.js:39-40`, `server/ai/llm.js:299-313` | Live | *documented*: keep the shared key in Live's settings "until the cutover has held for a while" (AI `docs/migration.md:101-103`). *Proposed*: 30 days of remote runs with no `[AI service]` warnings in Live's journal. Then remove the local shared-key path. Streamer BYO providers stay (AI `docs/migration.md:105-115`). | 2026-10-23 | Unset `AI_SERVICE` and restart Live (ADR-015 §Rollback). |++| C-21 | **`POST /internal/ai/site-copy`** on Live, key-only (see C-50). Network calls `network.site_copy` directly since `422f8e9`, and falls back to Live's endpoint with the internal key when AI answers nothing. With `AI_SERVICE=remote`, Live forwards the call to AI. | Live `server/internal/routes.js:24-61`; Network `server/chrome/service.js:56-57,136-141` | Network (caller), Live (route) | *proposed*: remove Network's fallback, then Live's route after 14 days with no call (the procedure in Network `docs/retirement.md`). Live has no counter of its own. Network's `principal_usage` counts only Network's routes. | 2026-10-23 | Network's fallback call. |++| C-22 | **`AI_NS_FALLBACK`** (default `live=live.*\|network.site_copy`): a service token with no `ns` gets these namespaces. Its stated reason, that Live's grant names no namespaces (`README.md:76-77`), stopped being true with Network `988330b`, which now grants `live.*` and `network.site_copy` (`server/identity/principals.js:102-103`). | AI `server/config.js:27-44`, `server/auth.js:96-103` | AI | *proposed*: every AI grant names its namespaces. Then set `AI_NS_FALLBACK=none` and remove the code. | 2026-10-09 | Set it back to the default string. |++| C-23 | **`AI_NS_REQUIRED`** (default `true`). `false` restores the old open rule. It is documented as a "rollback lever only". | AI `server/config.js:77-79`, `.env.example:24-25`, `README.md:78-79` | AI | *proposed*: removed together with C-22, once fail-closed has held for 30 days. | 2026-10-23 | Set `AI_NS_REQUIRED=false`. |++| C-24 | **Legacy MediaRef ids** `legacy:<svc>:<kind>:<id>` are still accepted. | AI `server/fetcher.js:17,160-163`, `server/workflows/common-schemas.js:7` | AI | *proposed*: every caller sends Media object ids (ADR-006). Unknown whether any caller still sends legacy refs: check AI `runs` inputs. | 2026-11-06 | none needed |++++## Money (Billing prepared, running in shadow; Live is the authority)++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-30 | **`BILLING_AUTHORITY=live`** (the default). Live holds the money: `openvibe_bucks_*`, `transactions`, `payment_orders`, `subscriptions`. `billing` makes Billing the ledger, and a tripwire (`assertLiveLedger`) makes Live's columns read-only. Any other value refuses every money write. There is no runtime shadow mode. "Shadow" means Billing's repeated shadow imports (Billing `README.md:203-204`). | Live `server/monetization/money-authority.js:26-49`, `server/db/database.js:5406-5555`; Billing `docs/live-cutover.md` | Billing + Live | *documented*: Billing `docs/live-cutover.md` steps 0-10. **Blocked on the owner**: step 4 (re-pointing the PowerChat webhook in the PowerChat dashboard). *Proposed*: after the cutover holds for 30 days, remove Live's money write paths and leave the columns read-only. | Cutover: when the owner re-points (target 2026-10-07). Removal: cutover + 30 days. | Before step 6: re-point PowerChat back to Live. Steps 6-9: unset `BILLING_AUTHORITY` and restore `billing-pre-cutover.db`. After step 9: **no tool exists** (Billing `docs/live-cutover.md:177-200`). |++| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. |++| C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |++++## Stream ingest (OpenRe deployed; no slot switched)++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRe | *documented*: "Live's ingest code stays until the last protocol has run on OpenRe for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRe `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRe `README.md:205`). |++| C-41 | **OpenRe port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRe `server/config.js:24,72`, `README.md:139-145` | OpenRe | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |++| C-42 | **OpenRe `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRe `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRe + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |++++## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`)++++Network `docs/retirement.md` gives the procedure:++- Telemetry in `principal_usage` has run since 2026-09-23. Guarded routes are counted as `auth='internal-key'`, other internal routes as `auth='internal-key-route'`.++- Move a caller to a service token, wait until the route shows no legacy-key calls for **14 days**, remove the key path for that route, and finally delete `INTERNAL_API_KEY` from every env file.++- ADR-003: "the key is retired per route once legacy use reaches zero".++- First observation: `GET /internal/url-registry/resolved`, from Live's registry refresh (2026-09-23 04:50 UTC).++- **No HTTP route lists `principal_usage`.** Read it with the SQL in `docs/retirement.md`.++++The dates below assume each caller moves by **2026-10-09**. The key paths then go on **2026-10-23**, and the variable goes from every env file on **2026-11-06**. Where a route has no counter, the entry says so.++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-50 | **Network key-only `/internal/*` routes**: verify-token, users/*, link-account, user-avatar, audit, stats, url-registry/resolved, coins/stats, notifications/unread\|mark-read\|resolve-users, issue-token, resolve-anon, anon-admin, anon-list, identity/legacy-map. The key defaults to `'change-me-in-production'`, and `OV_INTERNAL_KEY` is a second env name for it. | Network `server/internal/routes.js:39-57,75-720`, `server/config.js:28-29` | Network | *documented*: 14 days with zero `legacy-key` rows per route (`docs/retirement.md`). | 2026-10-23 | The key keeps working per route until that route's key path is removed. |++| C-51 | **Network routes that take a token or the key** (`TOKEN_ROUTES`): identity resolve and resolve-batch, coins credit/debit/transfer, notifications push and push-bulk, events/stream-live, modules. `principals.guard(…, { legacy: true })`. | Network `server/internal/routes.js:59-60`, `server/identity/principals.js:235-247` | Network | Same as C-50, per route. | 2026-10-23 | Callers fall back to the key (ADR-003 §Rollback). |++| C-52 | **Live's key-only outbound calls to Network**: `/internal/url-registry/resolved`, `/internal/identity/legacy-map`, `/internal/coins/stats`, `/internal/resolve-anon`, `/internal/link-account`, `/internal/user-avatar`, `/internal/notifications/mark-read`. | Live `server/config.js:290-300`, `server/auth/identity-sync.js:45-47`, `server/monetization/wallet-client.js:151-152`, `server/chat/chat-server.js:167-171`, `server/utils/notify.js:85,103,143` | Live | Move each call to a service token, then C-50. | callers 2026-10-09 | none needed |++| C-53 | **Live's token-with-key-fallback calls** (`TOKEN_PATHS`). When no token can be had they fall back to the key and pause token attempts for 5 min. This includes the go-live notification (`/internal/events/stream-live`), whose second path is ADR-020's Events consumer. `notify._post` returns null when there is no key, even on token-capable paths. | Live `server/net/network-principal.js:21,47-61`, `server/utils/notify.js:38-39`; ADR-020 §Migration consequences | Live + Network | *documented* (ADR-020): the two paths run side by side "until parity is shown; then the push call is removed". Precondition: Network can read Live's followers (D09 or a capability-guarded read). *Proposed*: drop the key fallback when C-51 goes. | Key fallback 2026-10-23; push call unknown (blocked on D09) | Re-enable the direct push (ADR-020 §Rollback). |++| C-54 | **Live's key-only inbound routes** (loopback only): `POST /internal/ai/site-copy`, `/internal/url-registry/refresh`, `/internal/user-avatar`, `/internal/user-role`, `GET /internal/analytics-summary`, `POST /api/cosmetics/internal-unlock`. | Live `server/internal/routes.js:8-18,30-106`, `server/index.js:557-565`, `server/monetization/cosmetics-routes.js:85-99` | Live (routes), Network (callers) | *proposed*: add service-token guards on Live (`server/net/service-guard.js` exists), move Network's callers, then 14 days with no key use. **Live has no key-use counter.** Add one, or read Live's access log. | 2026-10-23 | none needed |++| C-55 | **Network's outbound key calls**: Live `/internal/user-avatar`, Media `/internal/avatar-ingest`, Live `/internal/analytics-summary`, Live `/internal/ai/site-copy` (C-21), Live `/internal/user-role` (reads `INTERNAL_API_KEY \|\| OV_INTERNAL_KEY`), and admin analytics. | Network `server/profile/avatar.js:66,75`, `server/chrome/service.js:77,138`, `server/admin/routes.js:120,220-224`, `server/admin/analytics-routes.js:16,57` | Network | Move to service tokens with capabilities on the callee. | 2026-10-09 | none needed |++| C-56 | **Media `POST /internal/avatar-ingest`**: key only, loopback, ≥16-character key, otherwise 404. `VIEW_HASH_SECRET` falls back to `MEDIA_SECRET` and then `INTERNAL_API_KEY`. | Media `server/index.js:55-63`, `server/views/service.js:27-33` | Media | *proposed*: a service-token guard on avatar-ingest; set `VIEW_HASH_SECRET` explicitly so removing the key does not change view hashes. | 2026-10-23 | none needed |++| C-57 | **Tools satellites** `/api/internal/analytics` and `/analytics/bots`: key only, alias `OV_INTERNAL_KEY`. `apps/gateway/.env.example:30-31` describes it wrongly. | Tools `apps/_shared/internal-auth.js:11-17` | Tools | *proposed*: the gateway aggregates with a service token, or loopback without a key. | 2026-10-23 | none needed |++| C-58 | **Network's cross-database read of Live.** On every boot, `syncLinkedLiveRoles` opens Live's database read-only, from `OPENVIBELIVE_DB_PATH`, then `/opt/openvibelive/data/live.db`, then `../live/data/live.db`. This breaks ADR-007 ("no service reads or writes another service's database"). None of the default paths is Live's production path (`/opt/openvibe.live/data/live.db`). Whether it runs in production: **unknown**. Check `/etc/openvibe/network.env` for `OPENVIBELIVE_DB_PATH`. | Network `server/db/database.js:28-99`, called at `:751` | Network | *proposed*: remove the function. Roles already reach Network through `/internal/user-role` and the identity map. | 2026-10-09 | none needed |++| C-59 | **Hard-coded legacy secret** `X-Internal-Secret: openvibe-internal-2026`. Live's `/api/cosmetics/internal-unlock` accepts it from loopback. Live's `questApi` sends it to `LEGACY_QUEST_API = http://127.0.0.1:3200`, the pre-OpenVibe game. Games now runs on :8000. Whether anything listens on :3200: **unknown**. Check `ss -ltnp` on the host. | Live `server/monetization/cosmetics-routes.js:86-94`, `server/monetization/cosmetics.js:11,222-275` | Live (+ Games for inventory) | *proposed*: remove the literal secret and the :3200 client. If cosmetics still need the game inventory, Games exposes it behind a service token. | 2026-10-09 | none needed |++++## Events delivery signatures++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-60 | **v1 signature** `X-OpenVibe-Signature` (HMAC of the body, no timestamp) is sent on every delivery next to v2 (`X-OpenVibe-Timestamp`, `X-OpenVibe-Signature-V2`, ±300 s). **There is no env switch to stop v1.** Production: Events sends v2. | Events `server/worker.js:56,66-68`, `lib/client.js:36-64` | Events | *documented*: "v1 stays on the wire until every consumer requires v2, and only then may it be removed" (ADR-004; Events `docs/replay-window-rollout.md` step 5, "optional"). Every consumer has committed `requireV2`/`verifyDeliveryV2`: Live `ae07553`, Search `5a7e59b`, News `1802e7d`, Deals `e72aeec`, Trade `9a55644`, Reviews `3151c05`, Tips `435f32b`, VIP `dc98781`, Examples `6ea9cfd`. Which of these are deployed: **unknown**. Check each `/opt/openvibe.<svc>` HEAD. Condition: all deployed, plus one clean day for Tips and VIP. | 2026-10-07 | Consumers unset `requireV2`. Events keeps sending v1 until then (ADR-004). |++| C-61 | **Codes webhook tester** checks and generates v1 only. The docs page already describes v2. | Codes `server/domain/webhooks.js`; Events `docs/replay-window-rollout.md:58` | Codes | Tester inspects `X-OpenVibe-Signature-V2` (rollout doc follow-up). Needed before C-60. | 2026-10-07 | none needed |++++## Media, Live storage and legacy data++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-70 | **Live `MEDIA_API_KEY`**, the per-app bearer key that Live sends to Media, with `X-OV-User-Id`. **Live has no service-token path to Media.** Media already accepts principal tokens for audience `openvibe.media` (Media `server/auth.js:221-304`). Community, Tools, Games, Wiki and Blog are token-only tenants (`:162-173`). | Live `server/media-client.js:17-51`; Media `server/auth.js:353-396` | Live + Media | *proposed*: Live's media-client moves to a service token; then remove the `live` app key hash from `apps`. **Media has no key-vs-token counter.** Add one before the 14-day wait. | 2026-11-06 | The key keeps working. |++| C-71 | **`MEDIA_APP_KEYS` short form** and `MEDIA_APPS_SEED` key seeding. | Media `.env.example:37-38`, `server/auth.js:370-392` | Media | With C-70 and C-42: when no tenant uses a key. | 2026-11-06 | none needed |++| C-72 | **ADR-021 backups that still hold IPs**: `/opt/openvibe.live/data/backups/analytics-pre-adr021-2026-09-23.db`, `/opt/openvibe.tools/backups/analytics-2026-09-23`, Network `data/backups/network.db.pre-adr021-20260923T1634` (operator notes, not in a repo). | host | Live, Tools, Network | *documented* (operator notes): delete after 2026-10-07. | 2026-10-07 | They are the only way back to pre-ADR-021 analytics. |++| C-73 | **Live's legacy `vods`/`clips`/`pastes` tables**. They were frozen for the Media cutover, and the canonical copies are now in Media and Community. **There is no tripwire; only comments** (`server/db/database.js:3223-3224`). One write path remains: `PUT /api/admin/pastes/:id/nsfw` (`server/admin/routes.js:427-434`). Exported write helpers with no callers remain: `updatePasteAi`, `updateVodHealth`, `repairVodDuration`, `updatePaste`, `deletePaste`, `likePaste`. Live still **reads** them for live pages: `server/ai/chat-ai-routes.js:337`, `server/admin/routes.js:91-92`, `server/arena/arena-service.js:436`, `server/streaming/routes.js:1547`, and about 70 queries in `database.js`. These reads serve pre-cutover data. | Live (above); `docs/vods-and-clips.md:3-5` | Live | **No removal plan in the repo.** *Proposed*: (1) remove the nsfw write and the dead helpers, and add a tripwire like `assertLiveLedger`, by 2026-10-09; (2) move the four readers to Media or Community; (3) back up and drop the tables after 30 days with no reader. | 2026-11-06 | The tables are the only local copy of pre-Media rows. Keep the backup. |++| C-74 | **Pre-Media local paths** `VOD_PATH`, `CLIPS_PATH`, `COLD_STORAGE_PATH`, `THUMBNAILS_PATH` are still read by Live's config. | Live `server/config.js:251-257` | Live | *proposed*: no code reads the directories. **Unknown** whether any does. Grep for each config key. | 2026-11-06 | none needed |++| C-75 | **Media dual write**: the old tables are written first, then synced into `media_objects`. A sync failure only logs. Old routes also stay: `/v/:id` basename fallback, `/api/thumbnails/:name` with app `live` hard-coded, `/f/screenshots/:name`, and `/tos` → `/terms`. Webhooks to Live are published alongside Events. | Media `docs/object-model.md:3-7,88-112`, `server/db/database.js:100-124`, `server/public/routes.js:176-190,346-384`, `server/events.js:1-18` | Media | *documented*: "The object layer is additive; old routes keep working throughout" (ADR-006 §Rollback). Webhooks stay "until every consumer reads events" (`server/events.js`). *Proposed*: Live consumes `media.*` events instead of `/internal/media-webhook`, then remove the webhooks. | 2026-12-18 | Old routes and tables. |++| C-76 | **Live legacy (in-place) deploy layout**, "current production" (Live `docs/deploy.md:31-32`). A rollback resets the checkout but not `node_modules`. `--rollback` is refused in this layout. | Live `deploy/scripts/deploy.sh:347,394-419`, `deploy/systemd/openvibe-live.service` | Live | *documented*: the one-time `deploy/scripts/migrate-to-releases.sh` (one restart). | 2026-10-09 | `git reset` to the old commit. |++| C-77 | **`ALLOW_P2P_FALLBACK`** (default `false`), an "emergency rollback" to legacy P2P relays, which can expose viewer IPs. | Live `server/config.js:87-93`, `SETUP.md:136` | Live | **No condition documented.** *Proposed*: remove the flag and the P2P relay path. | 2026-11-06 | Set the flag to `true` (not recommended). |++++## Shared packages, sites and closed repositories++++| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |++|---|---|---|---|---|---|---|++| C-80 | **`openvibe-shared/analytics.js`** (`AnalyticsTracker`) stores raw IP, `user_id`, city and full UA. **No deployed service uses it.** Live (`server/analytics/`, `0e07aec`), Network (`server/analytics/`) and Tools (`apps/_shared/analytics`, `533c9a3`) run their own ADR-021 trackers. No other repo requires it. The one remaining `require` is the stale local checkout `~/OpenVibers/OpenVibe.Live` (`e266060`, behind `origin/main`). It is still exported (`package.json:10`, `index.js:8,40-44`) with no deprecation marker. Stale docs: Shared `docs/migration-plan.md:29,252,493`; Live `SECURITY_AUDIT.md:98`. | OpenVibe.Shared `analytics.js` | Shared | **No condition documented** (ADR-021 does not mention it). *Proposed*: mark it deprecated in the next Shared minor, then remove it in the next major, since removing an export is breaking (ADR-008). | Deprecate 2026-10-09; remove in Shared v2.0.0, 2026-11-06 | Pin the previous Shared tag. |++| C-81 | **OpenVibe.Sites placeholders**. There are 16, all `"stage": "placeholder"`: openvibe.chat, .news, .reviews, .tips, .vip, .trade, .host, .deals, .coupons, openre.stream, and auth., api., admin., themes., realtime. and ai.openvibe.network. The page says "coming soon · this page is a placeholder, nothing here is live yet". None claims to be a working product. **Two defects:** repos outside `EXISTING_REPOS` are labelled "(charter only, no code yet)", which is false for Tips, VIP, News, Reviews, Trade, Deals, Coupons and Host; and **realtime.openvibe.network advertises a product that ADR-005 closed**. | Sites `sites.json`, `build.js:22,157-161,283`, `deploy/nginx/*.conf` | Sites (each service owns its launch) | *documented*: each entry is "Removed from sites.json in the same release that the real service takes over this domain" (`build.js:283`, `README.md:20-21`, test `test/build.test.js:39-43`). *Proposed*: fix the "charter only" label from the repo's STATUS, and remove the realtime placeholder now. | Realtime and label: 2026-09-30; the rest: at each launch | Re-add the entry. |++| C-82 | **OpenVibe.Realtime**, closed by ADR-005: realtime runs inside Events as SSE `/realtime/stream`. `STATUS.json` says `stage: closed`. Leftovers: the README still says "Decision pending" (`:12,51-53`); the GitHub repo is not archived; Contracts `manifests/services/realtime.json:8` says `"status": "placeholder"`; Events `README.md:6` says "until ADR-005 decides"; and the Sites placeholder (C-81). | OpenVibe.Realtime; Contracts; Sites; Events | Contracts (manifest), Sites, Events | *proposed*: fix the README, mark the manifest `closed` (a Contracts release), remove the Sites placeholder, archive the repo. | 2026-09-30 | ADR-005: revisit only on measured load. |++++## Kept by design (not shims)++++These match the search terms but are permanent. They are listed so the next review does not re-open them.++++- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.++- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.++- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).++- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRe use their own handlers.++- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.++- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.++- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).++- **Games `EDITOR_KEY`**: "Fallback admin secret for the map editor (until openvibe.network SSO)" (`apps/server/src/config.ts:82-83`). This one *is* a legacy secret, with no removal date. *Proposed*: SSO staff check for the editor by 2026-11-06, owner Games.nnew file mode 100644@@@ -0,0 +1,371 @@++# Final cutover runbook (roadmap Wave 22)++++The Wave 22 exit criterion is: "The final cutover runbook, with rollback, is absorbed into OpenVibe.Host."++This page gathers every service cutover of the realignment into one ordered runbook. Each section gives the++cutover's production state and its rollback, and links the per-repository document that stays++authoritative for the details. When a step here and its original disagree, the original wins; fix this++page.++++- Host: `ssh openvibe-ovh` (single host). The public IPv4 is in `~/OpenVibers/RUNBOOK.md` on the++ operator workstation, which is not in a repository.++- Env files: `/etc/openvibe/<service>.env`. Units: `openvibe-<service>.service`. Checkouts:++ `/opt/openvibe.<service>`.++- Live paths are from `~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`).++- Production state is from the operator notes of 2026-09-23.++++State markers:++- **DONE**: executed on production, with the date.++- **IN PROGRESS**: partly on production.++- **BLOCKED (owner)**: waits for something only the owner can do. The action is spelled out.++- **READY**: nothing blocks it.++- **NOT STARTED**++++## Overview++++| # | Cutover | State | Per-repo original | Rollback in one line |++|---|---|---|---|---|++| A | Pastes → OpenVibe.Community (Wave 5) | **DONE** 2026-09-22 ~23:35 UTC | ADR-011; Community `README.md:16-21,354,387-390` | `PASTES_AUTHORITY=live` in Community and Live, remove Media's two flags, restart all three |++| B | Chat → OpenVibe.Chat (Wave 6) | **DONE** 2026-09-23 02:03 UTC | Chat [`docs/cutover.md`](https://github.com/OpenVibers/OpenVibe.Chat/blob/main/docs/cutover.md) | Remove the nginx include, drain the mirror, unset `CHAT_AUTHORITY`, restart Live |++| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live |++| D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ |++| E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |++| F | RTMP ingest → OpenRe.Stream | **BLOCKED (owner)**: DNS `ingest.openre.stream` and port 1936. OpenRe is deployed. | OpenRe [`README.md` "Cutover runbook"](https://github.com/OpenVibers/OpenRe.Stream/blob/main/README.md#cutover-runbook) | Per slot `{"authority":"live"}`. All slots: unset `OPENRE_URL` |++| G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |++++The remaining order is D → E → F → G.++- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.++- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on++ OpenRe.++- G follows each entry's date in the register.++++## What the owner must do++++These actions need the owner's accounts or physical presence. Nothing in this runbook can do them.++++1. **PowerChat dashboard, for step E4.** In the PowerChat developer dashboard, for OpenVibe.Live's app,++ change the webhook URL:++ - from `https://openvibe.live/api/powerchat/webhook`++ - to **`https://billing.openvibe.network/webhooks/powerchat`**++++ Keep the **same signing secret** (Billing's `POWERCHAT_WEBHOOK_SECRET` equals Live's++ `powerchat_webhook_secret`). Do this only at step E4 of the sequence, **not before**: Live freezes++ first, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint++ (Billing `docs/live-cutover.md:131-134, 170-175`).++2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,++ **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRe `README.md:145`,++ `deploy/nginx/openre.stream.conf:15-17`).++3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRe++ `README.md:145,160`).++4. **Broadcaster windows, for step F.** For each slot, agree a maintenance window with its broadcaster.++ The slot is offline, and the broadcaster regenerates the key and pastes the new server into OBS++ (ADR-009).++5. **Cloudflare browser cache TTL** (ADR-016, not a cutover, but it affects every deploy). Set the zone++ to "Respect existing headers". Today Cloudflare raises `/shared/*` `max-age=300` to 4 h.++++## 0. Deploy rules that apply to every step++++These rules are learned from outages. `ovhost` encodes them (see [README](../README.md#purpose)). As of++2026-09-23 it is used read-only on the host, so the per-repo scripts are still what runs.++++- **Live**: `sudo /opt/openvibe.live/deploy/scripts/deploy.sh [--wait-idle|--restart|--force]`++ (`deploy/scripts/deploy.sh:7-12`).++ - Production uses the legacy in-place layout (Live `docs/deploy.md:31`). `--rollback` is refused++ there (`deploy.sh:347`), and a failed-readiness rollback resets the checkout without restoring++ `node_modules`. The release layout (`deploy/scripts/migrate-to-releases.sh`) is register entry C-76.++ - **`openvibe-live.socket` survives restarts.** It has no `PartOf=`++ (`deploy/systemd/openvibe-live.socket:13-23`). The script makes sure it is active and restarts only++ `openvibe-live` (`deploy.sh:205-222`). HTTP queues on the socket during boot. WebSocket, WHIP, WebRTC++ and RTMP sessions drop and reconnect. **Never stop or restart the socket.**++ - Readiness: polls `/api/ready` every 1 s for up to 90 s. If it is not ready, the script rolls back and++ exits 3; if the rollback also fails, it exits 4 (**manual intervention**) (`deploy.sh:31-32,139-147`).++ - `--wait-idle` polls `/api/streams` every 60 s and needs 2 idle checks in a row, for up to 8 h++ (`deploy.sh:120-137`). The script treats a failed `curl` as zero streams (`deploy.sh:121-125`).++ Check by hand when it matters.++ - `public/`, `docs/` and tests deploy without a restart. Schema changes take an online backup to++ `data/backups/live-<ts>.db` first (`deploy.sh:192-203`). `deploy/nginx/` is **never** installed by++ the script.++ - Owner decision, 2026-09-23: restarts while streamers are live are permitted "for now". Prefer++ `--wait-idle` when it costs nothing, and say when a restart hit a live stream.++- **Media**: there is **no deploy script** in the repo. Pull, `npm ci --omit=dev` if dependencies++ changed, then `systemctl restart openvibe-media`.++ - **Do not restart Media while it is recording** (hazard H3,++ `OpenVibe.Network/docs/roadmap-baseline/10-hazards.md:11`). Check first:++ `curl -s 127.0.0.1:4100/api/ready | jq .recordings_in_progress`, or ovhost's probe++ `SELECT count(*) FROM vods WHERE is_recording = 1` run read-only as the service user.++ - If a restart does cut a recording: 5 s after boot, Media finalizes every `is_recording = 1` row that++ has no ffmpeg. It merges chunks, remuxes, falls back to `.master.mkv`, and marks very short results++ `needs_review` (`server/index.js:197-210`, `server/vod/finalize.js`). Interrupted clips are marked++ failed.++ - **Known gap:** the unit's `TimeoutStopSec=30` (`deploy/systemd/openvibe-media.service:16`) is++ shorter than the recorder's ffmpeg stop grace (`VOD_STOP_GRACE_MS`, default 60 000 ms,++ `server/vod/recorder.js:35`). systemd therefore SIGKILLs a stop that is still flushing a recording,++ and the boot sweep has to recover it.++- **Tools**: `deploy/scripts/deploy.sh` (run from the checkout).++ 1. Fetch, and drop untracked lockfiles that the incoming release tracks.++ 2. `git pull --ff-only`, and `npm install --omit=dev` in apps whose `package.json` changed.++ 3. Reinstall any dependency that does not resolve. If it still does not, **ABORT, nothing restarted**.++ 4. Check that img, audio and docs load the jobs runtime.++ 5. Restart **all** `openvibe-tools*` units at once, then curl the gateway's `/api/health`.++++ (`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.++ `apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.++- **OpenRe.Stream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.++ - `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure++ (exit 2).++ - `workers` starts a new generation. The old ones drain for up to 30 min.++ - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRe `README.md:147-160`.)++- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,++ install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own++ `deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).++- **Always**:++ - Stage explicit paths when committing on the host. Never `git add -A`: on 2026-09-23 02:09 it++ committed half-done work and took Network down for two minutes.++ - Production WebSocket probes send `join` only, never `chat` or `dm` frames.++ - Take `sudo ovhost backup <service>` before any data move.++++## A. Pastes → OpenVibe.Community (Wave 5): DONE 2026-09-22++++Community owns pastes and comments (ADR-011). Production values:++++| Service | Setting | Effect |++|---|---|---|++| Community | `PASTES_AUTHORITY=community` in `community.env` | Serves pastes natively. The database is `/var/lib/openvibe-community/community.db`. |++| Live | `PASTES_AUTHORITY=community` in `live.env` | `/api/pastes` forwards to Community with a service token (`server/pastes-client.js`, `server/media-proxy/pastes.js:250-319`). |++| Media | `PASTES_FROZEN_APPS=live` | Paste writes answer 410 (`server/pastes/routes.js:47-55`). |++| Media | `PASTES_MOVED_TO=https://openvibe.community` | 301 for `/p/:slug` and raw text (`server/public/routes.js:446-494`). |++| Tools | none | Forwards `/api/pastes/*` to Community with the visitor's token, and has no switch (`apps/gateway/server/index.js:183-207`). |++++Import: 889+2 pastes, 60 comments, 48 likes. Held: 1 paste and 4 comments, `ambiguous_owner`. Shadow++parity before the flip was 878/878 (ADR-011 §Migration consequences).++++**Verify:** `curl -s 127.0.0.1:4200/api/ready | jq .pastes_authority` must print `"community"`.++++**Rollback** (ADR-011 §Rollback):++1. Export the writes made in Community since the cutover. **No export tool exists.** Write one before you++ need it (register C-10).++2. Set `PASTES_AUTHORITY=live` in `community.env` and `live.env`.++3. Remove `PASTES_FROZEN_APPS` and `PASTES_MOVED_TO` from `media.env`.++4. Restart Community, Live and Media.++++ Tools has no switch: revert `fd5d318` in Tools and deploy. Community deletes are soft and never++ remove Media bytes.++++## B. Chat → OpenVibe.Chat (Wave 6): DONE 2026-09-23 02:03 UTC++++Chat serves `/ws/chat`, `/api/chat/`, `/api/dm/`, `/api/tts/` and `/api/sounds`. It runs on++127.0.0.1:4400 (`openvibe-chat`) with `/var/lib/openvibe-chat/chat.db`, `LIVE_MIRROR=1` and `EVENTS_URL`++set. Traffic reaches it through `include /opt/openvibe.chat/deploy/nginx/*.locations.conf;` in++openvibe.live's vhost. Live runs `CHAT_AUTHORITY=chat` and `OV_CHAT_INTERNAL_URL`. Its chat tables are a++read mirror kept current by Chat (register C-01–C-03).++++Record of the run (Chat `docs/cutover.md`):++- Prerequisites `:51-84`: Network principal `chat` and grants; Live patch deployed without the flag;++ Chat installed and not started.++- Rehearsal `:86-123`.++- Cutover `:125-167`:++ 1. Start Chat and import snapshot 1.++ 2. Set `CHAT_AUTHORITY=chat`, restart Live, add the include, `nginx -t && systemctl reload nginx`.++ 3. Import pass 2. `held` must be 0.++ 4. Check that `mirror.pending` and `chat_bridge_outbox` are near 0, and that no `ChatRemote` refusals++ appear.++- Imports: pass 1 had 70,860 messages, held 0. Parity was 15/15 read paths (ADR-010 §Acceptance tests).++- The pre-cutover nginx backup is `/root/openvibe.live.conf.pre-chat-<ts>` on the host.++++**Verify:** `curl -s 127.0.0.1:4400/ready | jq '.live, .mirror'`.++- `mirror.pending` should be near 0.++- `live.failures` should not grow.++++`/ready` stays 200 while Live syncs fail (see [storage-state.md](storage-state.md) finding 1), so read the++body, not just the status.++++**Rollback** (Chat `docs/cutover.md:169-184`, ADR-010 §Rollback). The order matters:++1. Remove the include from openvibe.live's vhost, then `sudo nginx -t && sudo systemctl reload nginx`.++ Chat traffic returns to Live, which answers 503 until step 3.++2. Make sure everything Chat wrote has reached Live. `curl -s 127.0.0.1:4400/ready | jq .mirror.pending`++ must be `0`. If Chat is down, run `scripts/mirror-flush.js` in the Chat checkout; exit 0 means it is++ drained. This must happen **before** step 3, because Live accepts mirror writes only while++ `CHAT_AUTHORITY=chat`.++3. Remove `CHAT_AUTHORITY` from `/etc/openvibe/live.env` and restart Live++ (`deploy.sh --restart`). At boot, `drainToLocal()` applies `chat_bridge_outbox` to Live's own tables++ (`server/chat/chat-remote.js:351-372`). A write Chat applied but never acknowledged is applied twice++ (`:354`).++4. `sudo systemctl stop openvibe-chat`. Keep `/var/lib/openvibe-chat/chat.db`.++++## C. AI → OpenVibe.AI (Wave 13): DONE 2026-09-23++++OpenVibe.AI runs on 127.0.0.1:4700 as principal `ai`, with `/var/lib/openvibe-ai/ai.db`. Live runs++`AI_SERVICE=remote` (`server/ai/ai-service.js:39`). With it, `llm.complete`, translation, paste/frame,++overview, media and recap calls go through AI workflows. There is **no fallback to Live's own key** when++AI returns nothing (`server/ai/llm.js:299-313`). Streamer BYO providers bypass AI.++++Steps from AI `docs/migration.md:72-99`:++- Steps 1-6 (deploy, import, grants, Live `AI_SERVICE=remote`, verify) are **DONE**.++- Step 7 is **DONE**: Network calls `network.site_copy` itself (Network `422f8e9`). Live's++ `/internal/ai/site-copy` remains as Network's fallback (register C-21).++- The Live grant names its namespaces (`live.*`, `network.site_copy`; Network `988330b`), and AI fails++ closed on a token with no `ns` (register C-22/C-23).++++**Verify:**++- `curl -s 127.0.0.1:4700/api/ready`.++- `GET /api/v1/runs?workflow=live.translate` shows recent succeeded runs.++- `journalctl -u openvibe-live | grep '\[AI service\]'` is empty.++++**Rollback** (AI `docs/migration.md:101-103`, ADR-015 §Rollback):++1. Unset `AI_SERVICE` in `live.env` and restart Live. Live then uses its own provider settings. **Keep the++ shared key in Live's settings** until C-20's removal date. Without it, this rollback has no provider.++2. Namespace problems only: set `AI_NS_FALLBACK` back to its default, or set `AI_NS_REQUIRED=false`, in++ `ai.env`, then restart AI.++++Stopping OpenVibe.AI touches no product data.++++## D. Events delivery signature v2: IN PROGRESS++++Events signs every attempt with v1 (`X-OpenVibe-Signature`) **and** v2 (`X-OpenVibe-Timestamp`,++`X-OpenVibe-Signature-V2` = HMAC of `"<t>.<raw body>"`, ±300 s) (Events `server/worker.js:56,66-68`).++v1 cannot be switched off by env.++++| Step (Events `docs/replay-window-rollout.md:22-34`) | State |++|---|---|++| 1. Tag openvibe-sdk `v0.4.0` | **DONE**: the tag exists. The doc's status line (`:3`) is stale. |++| 2. Deploy Events. Confirm one real delivery carries the v2 headers. Compare `date +%s` with a trusted clock. | **DONE**: Events sends v2 (operator notes). |++| 3. Codes docs | **DONE** in the repo (Codes `a9371c3`). The tester still checks v1 only (register C-61). |++| 4. Consumers, one at a time, in this order: Live (`/internal/openre-events`), Search, News, Deals, Trade, Reviews, then Tips and VIP after a clean day, then Examples | **Committed in every repo**: Live `ae07553`, Search `5a7e59b`, News `1802e7d`, Deals `e72aeec`, Trade `9a55644`, Reviews `3151c05`, Tips `435f32b`, VIP `dc98781`, Examples `6ea9cfd`. **Deployed: unknown.** Check `git -C /opt/openvibe.<svc> log -1` for each. |++| 5. Stop sending v1 (optional; only after every consumer requires v2) | **NOT STARTED** (register C-60, target 2026-10-07) |++++For each consumer deploy:++- Pin `openvibe-sdk` v0.4.0 (Search has no SDK), run `npm test` on Node 22, and deploy.++- Watch that service for `401 *.bad_signature`.++- Watch Events with `GET /api/v1/deliveries?status=failed&subscription_id=…`.++++**Rollback** (rollout doc `:36`, ADR-004): redeploy the consumer's previous release, which unsets++`requireV2`. Events keeps retrying for about 2 h 13 min (1 s, 5 s, 30 s, 2 min, 10 min, 1 h, 1 h), then++moves the delivery to the DLQ. Requeue with `POST /api/v1/deliveries/replay { subscription_id, event_ids }`++or `{ subscription_id, from_seq }`. Replays are signed afresh, and the consumer inbox dedupes on++`event_id`. Rolling back Events itself is safe only while no consumer requires v2.++++## E. Money → OpenVibe.Billing: BLOCKED on the owner (PowerChat dashboard)++++Billing runs in shadow on 127.0.0.1:4600 (principal `billing`), and its staff console is public at++`https://billing.openvibe.network`. Live is the authority: `BILLING_AUTHORITY` unset = `live`++(`server/monetization/money-authority.js:26-31`). Billing's own status is "prepared, **not executed**"++(Billing `docs/live-cutover.md:3`).++++**Before the day** (Billing `docs/live-cutover.md:56-102`):++++| Prerequisite | State |++|---|---|++| 1. `billing.env`: `POWERCHAT_WEBHOOK_SECRET` equal to Live's, `POWERCHAT_SITE_USERNAME`, `POWERCHAT_ALLOW_TEST_FULFILLMENT` off, `BILLING_*` rates equal to Live's | `POWERCHAT_SITE_USERNAME` set 2026-09-23 (operator notes). The rest: unknown. Check with `sudo ovhost validate billing`, which prints names only. |++| 2. Network grants for client `live`: `billing.intent.create`, `billing.transfer.create`, `billing.balance.read`, `billing.cashout.request`, `billing.subscription.manage`, `billing.entitlement.check`, plus `identity.subject.resolve`. **Not** `billing.cashout.manage` or `billing.ledger.admin`. | **DONE** (operator notes: Live granted `billing.*` except `cashout.manage` and `ledger.admin`) |++| 3. Staff console: redirect URI, `BILLING_STAFF_SUBJECTS`, `BILLING_SESSION_SECRET`, vhost, sign-in works | **DONE** (console live; staff subject is the owner only) |++| 4. Live patch deployed with `BILLING_AUTHORITY` unset; `/api/admin/money` shows `authority: "live"` | **DONE** (Live `c384787`) |++| 5. `identity-legacy-sync` has run since the last new account; a fresh shadow import has no holds for anyone with a balance | Shadow import done 2026-09-23: 322 transactions, reconcile OK. Re-run it on the day. |++++**The sequence** (Billing `docs/live-cutover.md:116-168`). Keep the order: it guarantees that every++PowerChat delivery lands in exactly one ledger.++++0. Check who is live: `curl -s https://openvibe.live/api/streams`. Step 6 restarts Live.++1. Freeze Live money writes: `POST /api/admin/money/freeze {on:true, reason:'Billing cutover'}` (owner).++ `GET /api/admin/money` must show `frozen: true`.++2. Wait **at least 60 minutes**, because minted PowerChat links live for an hour. Then run++ `POST /api/powerchat/reconcile`. List `payment_orders` that are `pending` or `paid` from the last 3++ days. A `paid` row is an anomaly; fix it first.++3. Back up Billing to `/var/lib/openvibe-billing/billing-pre-cutover.db`, **keep it for rollback**. Then++ run `node scripts/freeze.js on 'Live cutover'`.++4. **Owner: re-point the PowerChat webhook** (see "What the owner must do", item 1). From here on, Billing++ stores and holds every delivery (202).++5. Take the final snapshot to `/var/backups/openvibe/live-cutover-<date>.db` and **keep it**. Run++ `import-live.js --dry-run`, then the import, then `reconcile.js`. `holds` must be empty for anyone with++ money, and reconciliation must print **OK**.++6. Add `BILLING_AUTHORITY=billing`, `OV_BILLING_INTERNAL_URL=http://127.0.0.1:4600` and++ `OV_BILLING_PUBLIC_URL=https://billing.openvibe.network` to `live.env`. Run++ `sudo /opt/openvibe.live/deploy/scripts/deploy.sh --wait-idle`.++ `GET /api/admin/money` must show `authority: "billing"`, `reachable: true` and `drift: []`.++7. Unfreeze Billing with `node scripts/freeze.js off`. Held deliveries settle once. Deliveries that Live++ already credited are rejected (`billing.intent_settled_in_live`). Then `reconcile.js` must print OK.++8. Verify the reads while Live is still frozen: balances match, and `billing.actions.attention: []`.++9. Unfreeze Live: `POST /api/admin/money/freeze {on:false}`.++10. Make a real 100-Vibes PowerChat purchase. Expect one settled event, +100 exactly once, reconcile OK,++ and Live's legacy columns unchanged.++++**Rollback** (Billing `docs/live-cutover.md:177-200`):++- **Before step 6:**++ 1. Re-point PowerChat back to `https://openvibe.live/api/powerchat/webhook`.++ 2. Leave Billing **frozen**.++ 3. Unfreeze Live. Live's reconciler (every 15 min) backfills payments made in the window.++- **After step 6, before step 9:**++ 1. Remove `BILLING_AUTHORITY` and run `deploy.sh --wait-idle`.++ 2. Re-point PowerChat to Live and unfreeze Live.++ 3. Stop Billing, restore `billing-pre-cutover.db` (delete its `-wal`/`-shm` files), freeze it, and start++ it.++- **After step 9:** ADR-012's rollback re-derives Live's columns from Billing's journal. **No tool for this++ exists.** It is manual and lossy. Prefer fixing forward.++++**What the switch does not preserve** (`:202-239`):++- PowerChat side effects that arrived on Live's webhook (chat lines, alerts, goals) stop at the re-point.++- Displays that read Live's `transactions` table freeze.++- Renewal notices stop.++- The SPA sends no `Idempotency-Key` yet.++++Read the full list before choosing the day. VIP checkout stays closed until this cutover.++++## F. RTMP ingest → OpenRe.Stream: BLOCKED on the owner (DNS, port 1936, broadcaster windows)++++OpenRe is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are++`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The database++is `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsal++with an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`++reached Events, and the key never appeared in logs.++++Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`++defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves++(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRe `README.md:207-209`).++++**Once, before the first slot** (OpenRe `README.md:188-194`):++++| Step | State |++|---|---|++| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |++| 2. Deploy OpenRe; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |++| — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** |++| 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. |++| 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |++| 5. Rehearsal with a test account from OBS to `rtmp://ingest.openre.stream:1936/live`. Deploy Live and `deploy.sh api` during the broadcast, and check that nothing drops. | Needs the DNS and port first |++++**Per slot** (`README.md:196-203`; `migrate-from-live.js --checklist` prints the same list):++1. Agree a window with the broadcaster. The slot must be offline.++2. Run `node scripts/migrate-from-live.js --live-db <fresh snapshot> --apply --slots <id>`. Review held++ destinations on openre.stream.++3. `PUT /api/admin/openre/managed/<id>/ingest-authority {"authority":"openre"}` on Live. From then on,++ Live refuses the old key and has rotated its own copy (`server/openre/authority.js:103-146`).++4. The broadcaster presses **Regenerate stream key** on Go Live (it is shown once) and sets OBS to++ `rtmp://ingest.openre.stream:1936/live`.++5. Rotate the broadcaster's personal Live key (`users.stream_key`). ADR-009 requires every pre-cutover++ key to be rotated.++6. Test a broadcast. Watch `deploy.sh status`, `/api/admin/openre/status` and output health.++++**Rollback:**++- Per slot: `{"authority":"live"}`. The broadcaster regenerates the key on Live and points OBS back at the++ Live RTMP URL.++- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,++ `server/openre/authority.js:47-49`).++- Live's ingest code stays until the last protocol has run on OpenRe for two weeks (ADR-009 §Rollback).++- Port 1935 moves to OpenRe (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening++ (`README.md:139-145`).++++## G. Retiring the shims: NOT STARTED++++Every remaining shim has an owner, a removal condition, a date and a rollback lever in++[compatibility-register.md](compatibility-register.md). The ones on the critical path:++++- **The shared internal key** (C-50–C-58), per Network `docs/retirement.md`:++ 1. Move each caller to a service token by 2026-10-09.++ 2. Remove each key path after 14 days with no legacy use. Check with the `principal_usage` query in++ that doc.++ 3. Delete `INTERNAL_API_KEY` from every env file by 2026-11-06.++- **The hard-coded `openvibe-internal-2026` secret** in Live cosmetics (C-59): remove it by 2026-10-09.++- **Events v1 signatures** (C-60): stop them after every consumer is deployed with `requireV2`.++- **Chat's Live mirror** (C-01–C-03), and Live's frozen legacy tables and their readers (C-73): target++ 2026-11-06.++++Before removing a shim, take `sudo ovhost backup <service>` for any service whose data it touches. Update++the register row in the same change.nnew file mode 100644@@@ -0,0 +1,139 @@++# Storage state (roadmap Wave 22)++++This page covers two Wave 22 exit criteria:++- "No service still queries SQLite for canonical data unless that is its documented and reviewed state."++- "Each service's /ready tells the truth about its actual backing store."++++It has one row per deployed service. Each row gives the service's backing store, whether that store is++canonical, its status under ADR-007, what its readiness handler actually checks (read from the handler,++not from docs), and its backup and drill status from [restore-drills.md](restore-drills.md).++++ADR-007 (`OpenVibe.Contracts/docs/adr/ADR-007-data-ownership.md`) decides:++- "SQLite remains the documented store per service while the platform is single-host; each service's++ `/ready` reports its actual store."++- A service moves to PostgreSQL individually when it needs concurrent writers across processes, a second++ host, or sizes SQLite handles poorly.++- Money paths use SERIALIZABLE transactions and row locks when they move.++++Every service below runs on SQLite (better-sqlite3), with one database per service. No repository has a++PostgreSQL driver or config. **This page is the Wave 22 review of that state.**++++Sources:++- Each repository's `server/config.js`, `.env.example` and `deploy/systemd/*.service`.++- The ovhost inventory example [`host.example.json`](../host.example.json), which lists production paths.++- The operator notes of 2026-09-23, for what is deployed.++- Live was read at `~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`).++++`openvibe-shared/ready` (v1.3.0, `node_modules/openvibe-shared/ready.js:43-124`) behaves like this:++- It has no checks of its own.++- A service passes named checks, and each check is required unless it is marked optional.++- It answers 503 only when a required check fails.++- A failed optional check gives 200 with `status: "degraded"`.++- The default timeout is 2 s.++++## Findings++++1. **Readiness that does not check the store.**++ - **Games** has no `/api/ready`. `/healthz` returns `{ ok: true, tick }` without touching `world.db`++ (`apps/server/src/net/httpServer.ts:596-600`).++ - **Chat** `/ready` is 200 as soon as `ctx.stats.lastSyncAt` is set (`server/app.js:77-86`). That++ timestamp is set even when every Live sync step fails, because `tick()` and `sync()` catch each++ step's error and set it anyway (`server/live-context.js:665-683`). The only database access is the++ `mirror.pending` count (`SELECT COUNT(*) FROM live_mirror_outbox`, `server/bridge/live-mirror.js:34`,++ production runs `LIVE_MIRROR=1`). A broken `chat.db` would therefore surface as a thrown 500, not as a++ named check.++2. **Readiness that checks the main store but not every store.**++ - **Live** runs `SELECT 1` on `live.db`. It does not check `data/analytics.db` or `data/rs-companion.db`.++ - The **Tools** satellites maps, text, yt and food only check `analytics.db`, and only as an optional++ check. By design, that is their only database, and the tools work without it.++ - The **Tools** gateway reports each satellite as an optional upstream. So a failed `jobs.db` on img,++ audio or docs makes the gateway *degraded* (still 200), while that satellite's own `/api/ready`++ answers 503.++3. **ovhost's post-deploy readiness URL is not the truthful endpoint for four services.**++ `host.example.json` polls:++ - Network `/api/health`, which is static (`server/index.js:392-394`), although Network's `/api/ready`++ checks the database and the signing key;++ - Media `/healthz`, which is liveness only, although Media's `/api/ready` checks the database and++ storage;++ - Tools gateway `/api/health`;++ - Games `/healthz`.++++ The inventory should point at `/api/ready` where one exists. Games has none.++4. **Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and++ OpenRe.Stream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron++ and timers.++5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.++ The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,++ `apps/_shared/jobs/index.js:77`) are missing. Tools W11 jobs have been in production since++ 2026-09-23.++6. **SQLite reads outside the documented state.** These go against exit criterion 1, and each has an entry++ in [compatibility-register.md](compatibility-register.md):++ - **Live reads its frozen legacy `vods`/`pastes` tables for live pages**, although Media and Community++ hold the canonical rows (C-73):++ - `server/ai/chat-ai-routes.js:337`++ - `server/admin/routes.js:91-92`++ - `server/arena/arena-service.js:436`++ - `server/streaming/routes.js:1547`++ - **Network may open Live's `live.db`** read-only at boot (`syncLinkedLiveRoles`,++ `server/db/database.js:28-99`), which ADR-007 forbids (C-58). None of its default paths is Live's++ production path. Whether `OPENVIBELIVE_DB_PATH` is set is unknown: check `/etc/openvibe/network.env`.++ - **Chat reads Live's files** at `/opt/openvibe.live/data/sounds`. These are files, not a database++ (`deploy/systemd/openvibe-chat.service:43`).++7. **Canonical money is still in Live.** Billing's `billing.db` is a shadow (Billing++ `STATUS.json: "authoritative": false`) until the cutover in [cutover-runbook.md](cutover-runbook.md)++ step B.++++## Per service++++"Drill" is the latest result in [restore-drills.md](restore-drills.md), all 2026-09-23. "Deployed" is from++the operator notes unless cited. Where a repo's `STATUS.json` disagrees, the operator notes and the drill++log win.++++| Service | Backing store (production path; env, default) | Canonical? | ADR-007 status | Readiness checks (handler) | Store checked? | Backup / drill |++|---|---|---|---|---|---|---|++| **Network** | `/opt/openvibe.network/data/network.db`; `DB_PATH`, default `./data/network.db` (`server/config.js:55`). Analytics lives in the same file (`README.md:306-307`). | Yes: identity, OAuth clients, subjects, OpenCoins wallets, themes, notifications, analytics | SQLite, the reviewed single-host state. `docs/platform-plan.md:74,111` defers PostgreSQL "until a room needs it". | `/api/ready` (`server/observability.js:58-96`). Required: `db` (`SELECT COUNT(*) FROM oauth_clients`), `signing_key` in production (signs and verifies an RS256 probe; fails on the HS256 dev key). Optional: `registry_poll`, `discord_bot`. | **Yes** | Drill passed 05:22. ovhost's deploy readiness polls the static `/api/health` (finding 3). |++| **Live** | `/opt/openvibe.live/data/live.db` (legacy in-place layout, "current production", Live `docs/deploy.md:31`); `DB_PATH`, default `./data/live.db` (`server/config.js:228`). Also `data/rs-companion.db`, and `data/analytics.db`, whose path is hard-coded with no env (`server/index.js:361-363`). | Yes: streams, channels, follows, Live accounts, restreams, **money** (`BILLING_AUTHORITY` unset = `live`), AI state (`vod_ai_state`/`clip_ai_state`), analytics. **Not canonical:** the 12 chat mirror tables (Chat is, since 02:03 UTC) and the frozen `vods`/`clips`/`pastes` (Media and Community are). | SQLite, the reviewed state. No PostgreSQL plan. Money moves to Billing, not to PostgreSQL. | `/api/ready` (`server/index.js:748-756`, `server/web/observability.js:79-106`). Required: `boot`, `db` (`SELECT 1` on `live.db`). Optional: `sfu`, `media` (`MEDIA_URL/healthz`, cached 30 s), `network_key` (PEM loaded, no call). | **Yes** for `live.db`; not `analytics.db` or `rs-companion.db` | `ovhost backup` covers `live.db` and `rs-companion.db`. `analytics.db` is left out on purpose (`host.example.json` `_databasesNote`). Drill **unsupported**: `analytics.db` is opened relative to the checkout, RTMP always binds, and restreams, relays and AI jobs resume from the copy. |++| **Media** | `/opt/openvibe.media/data/media.db`; `DB_PATH`, default `./data/media.db` (`server/config.js:24`). Local `VOD_PATH`, `CLIPS_PATH`, `PASTES_PATH`, `THUMBNAILS_PATH`, `FILES_PATH`, `OBJECTS_PATH` (`config.js:27-58`), plus B2 and R2 object storage. | Yes: media objects, VODs, clips, files, thumbnails, avatars. Paste writes are frozen (Community is canonical). Media still serves paste screenshot bytes. | SQLite, the reviewed state. No PostgreSQL plan. | `/api/ready` (`server/observability.js:72-107`). Required: `db` (`SELECT COUNT(*) FROM apps`), and `storage_{vods,clips,pastes,thumbnails,files,objects}` (writes and unlinks a probe). Optional: `network_jwks`, `remote_b2`/`remote_r2` (HeadBucket, cached 60 s), `events_outbox` (>1000 pending). Also reports `recordings_in_progress`. | **Yes** (database and disk; object storage optional) | `ovhost backup` covers `media.db`. Drill **unsupported**: webhooks to Live come from the `apps` table, and ffmpeg jobs run against production paths. ovhost's deploy readiness polls `/healthz` (finding 3). |++| **Tools** | The gateway has no database. Each satellite has `apps/<app>/data/analytics.db` (relative to `/opt/openvibe.tools`). img, audio and docs also have `data/jobs.db` (`apps/_shared/jobs/index.js:77`), `data/uploads` and `data/output`; yt has `data/downloads`. Job results go to Media when `TOOLS_JOB_RESULTS=media` (default `local`, `apps/_shared/jobs/index.js:32-36`). | Yes: jobs (img, audio, docs) and per-app analytics | SQLite, the reviewed state. One file per process, so ADR-007's "concurrent writers across processes" trigger does not apply. | `/api/ready` on every unit (`apps/_shared/observe.js:71-162`). Gateway: `catalog` required; `network_key`, `service_directory`, `community`, and each `satellite_<name>` optional (`apps/gateway/server/index.js:60-72`). img, docs, audio: `jobs_db` (`SELECT COUNT(*) FROM tool_jobs`), `job_runtime` and data directories required. yt: `downloads_dir`. food: `maps` upstream. maps, text: none required (`analytics_db` optional). | **Partly**: `jobs.db` yes on its satellites; analytics optional by design; the gateway reports satellites as optional (finding 2) | `ovhost backup` covers the seven `analytics.db` files only; **`jobs.db` is missing** (finding 5). Drill **unsupported**: eight units, relative `analytics.db` paths, job runtimes re-queue at start. |++| **Games** | `/opt/openvibe.games/data/world.db`; `DB_PATH`, default `data/world.db` (`apps/server/src/config.ts:79`). No `.env.example`. | Yes: world, accounts, inventory | SQLite, the reviewed state. No PostgreSQL plan. | **No `/api/ready`.** `/healthz` returns `{ ok: true, tick }` (`apps/server/src/net/httpServer.ts:596-600`). | **No** | `ovhost backup` covers `world.db`. Drill **unsupported** (runtime side effects not reviewed). Games is `managed: false` in the inventory. |++| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. |++| **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. |++| **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback, `server/app.js:77-86`, not openvibe-shared). **503 only while `lastSyncAt` is unset.** It reports `live.failures`/`last_error`, `mirror.pending` and `events.enabled`, and ignores them for the status. | **No** (finding 1) | Drill passed 05:22. |++| **OpenRe.Stream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |++| **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. |++| **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. |++| **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |++| **AI** | `/var/lib/openvibe-ai/ai.db` (`deploy/systemd/openvibe-ai.service:30-31`); `AI_DB_PATH`, default `./data/ai.db` (`server/config.js:81`). | Yes: workflows, runs, providers, quotas, usage (Live keeps its AI product state, AI `docs/migration.md:30-35`) | SQLite, the reviewed state. | `/api/ready` (`server/app.js:26-33`, own handler). Required: `db` (`SELECT 1`), `key`, `workflows` (at least one registered). | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |++| **Search** | `/var/lib/openvibe-search/search.db` (`deploy/systemd/openvibe-search.service:28`); `SEARCH_DB_PATH` (`server/config.js:49`). | **No**: a derived FTS5 index, rebuildable from its owners (sources, wiki, blog, news, reviews) | SQLite FTS5. `docs/adr-engine.md` plans PostgreSQL FTS "once a PostgreSQL instance exists" (`server/engine/fts5.js:16` keeps the engine behind an interface). | `/api/ready` (`server/app.js:30-43`): `SELECT 1` and key loaded. | **Yes** | Drill passed 05:23. |++| **Sources** | `/var/lib/openvibe-sources/sources.db` (`deploy/systemd/openvibe-sources.service:29`); `SOURCES_DB_PATH` (`server/config.js:41`). | Yes: source registry, items, revisions, fetch runs | SQLite, the reviewed state. | `/api/ready` (`server/app.js:25+`): `SELECT 1`, key, and the worker when enabled. | **Yes** | Drill passed 05:23. |++| **Wiki** | `/var/lib/openvibe-wiki/wiki.db` (`deploy/systemd/openvibe-wiki.service:29`); `WIKI_DB_PATH` (`server/config.js:27`). | Yes: spaces, pages, revisions | SQLite, the reviewed state. | `/api/ready` (`server/app.js:63-75`). Required: `db` (`wiki_pages` exists). Optional: jwks, events_relay, community, sources, media. | **Yes** | Drill passed 05:23. |++| **Blog** | `/var/lib/openvibe-blog/blog.db` (`deploy/systemd/openvibe-blog.service:28`); `BLOG_DB_PATH` (`server/config.js:32`). | Yes: blogs, posts, revisions | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:19-58`). Required: `db` (charter tables exist). Optional: jwks, events_relay, scheduler. | **Yes** | Drill passed 05:23. |++| **News** | `/var/lib/openvibe-news/news.db` (`deploy/systemd/openvibe-news.service:29`); `NEWS_DB_PATH` (`server/config.js:32`). | Yes: topics, stories, source items | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:21-65`). Required: `db` (charter tables). Optional: jwks, events_relay, events_webhook, sources_pull. | **Yes** | Drill passed 05:23. |++| **Reviews** | `/var/lib/openvibe-reviews/reviews.db` (`deploy/systemd/openvibe-reviews.service:30`); `REVIEWS_DB_PATH` (`server/config.js:28`). | Yes: entities, signals, summaries | SQLite, the reviewed state. | `/api/ready` (`server/app.js:82-99`). Required: `db` (three review tables). Optional: jwks, sources, events relay and webhook, community, editors. | **Yes** | Drill passed 05:23. |++| **Deals** | `/var/lib/openvibe-deals/deals.db` (`deploy/systemd/openvibe-deals.service:28`); `DEALS_DB_PATH` (`server/config.js:34`). | Yes: offers, products, votes | SQLite, the reviewed state. | `/api/ready` (`server/observability.js`). Required: `db` (tables exist, `:26-28`). Optional: jwks, events_relay, sources_import, worker, abuse_keys. | **Yes** | Drill passed 05:23. |++| **Coupons** | `/var/lib/openvibe-coupons/coupons.db` (`deploy/systemd/openvibe-coupons.service:29`); `COUPONS_DB_PATH` (`server/config.js:32`). | Yes: coupons, merchants, domains | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:24-26`). Required: `db`. Optional: jwks, events_relay, sweep, sources_import, reporter_key. | **Yes** | Drill passed 05:23. |++| **Trade** | `/var/lib/openvibe-trade/trade.db` (`deploy/systemd/openvibe-trade.service:29`); `TRADE_DB_PATH` (`server/config.js:32`). | Yes: instruments, observations, source documents (informational, ADR-025) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:23-25`, mounted `server/app.js:121`). Required: `db`. Optional: jwks, events_relay, sources_sync, freshness. | **Yes** | Drill passed 05:23. |++| **Codes** | `/var/lib/openvibe-codes/codes.db` (`deploy/systemd/openvibe-codes.service:29`); `CODES_DB_PATH` (`server/config.js:37`). | Yes: manifests, releases, trust | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:24-32`). Required: `db`, `docs`. Optional: jwks, network, oauth_client, events_relay. | **Yes** | Drill passed 05:23. |++| **Host** | Stage B: `/var/lib/openvibe-host-api/host.db` and `HOST_STORAGE_DIR` blobs (`deploy/systemd/openvibe-host.service:37-38`; `server/config.js:47-49`). Stage A (`ovhost`) keeps JSONL logs in `/var/lib/openvibe-host` and has no database. | Yes: tenant projects, sites, deploys, domains | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:22-29`). Required: `db`, `storage` (`blobs.writable()`, `server/storage.js:98-102`). Optional: jwks, events_relay, domain_checks. | **Yes** (database and blob directory) | Drill passed 05:23 (`host.db`, counts only). `README.md` and `STATUS.json` say Stage B is **not deployed**, while the drill counted a production `host.db`. Whether `openvibe-host.service` is running is unknown: check `systemctl status openvibe-host`. |++++## Deployed-state notes++++- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,++ Codes, AI and OpenRe's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:++ Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whether++ they are public is unknown: check nginx `sites-enabled`. Sites still has placeholders for++ openvibe.deals, .coupons and .trade.++- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRe++ and AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,++ Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.++++## Re-checking++++```++sudo ovhost status <service> # unit, port, readiness as ovhost sees it++curl -s 127.0.0.1:<port>/api/ready | jq .checks # the named checks and their state++sudo ovhost backup <service> && sudo ovhost drill <service>++```++++To re-verify a row, read the handler cited in the row. A readiness change is a code change in that++service, and this page is updated in the same week.
new file mode 100644
new file mode 100644
@@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
+# Compatibility register (roadmap Wave 22)
+
+The Wave 22 exit rule is: "every compatibility shim is gone or has a dated, owned removal plan". This page
+lists every compatibility shim, legacy route, dual write or mirror, rollback flag and legacy secret found
+in the estate on 2026-09-23. For each one it gives the owner, the removal condition, a target date and the
+rollback lever that the shim keeps available until it is removed.
+
+**How this was built.** Source was read in every repository under `~/OpenVibers` and in Live at
+`~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`), excluding `node_modules`. The search terms were
+`_AUTHORITY`, `_LEGACY`, `legacy`, `deprecated`, `compat`, `fallback`, `shim`, `INTERNAL_API_KEY`,
+`X-Internal-Key` and `frozen`. Paths are relative to each repository, and `file:N` is a line number.
+Production facts come from the operator notes of 2026-09-23.
+
+**Conditions and dates.**
+- *documented*: the condition is quoted from the cited doc or ADR.
+- *proposed*: the condition was set by this register. No repo recorded one before.
+- Every date is a target. A target moves only when this page is edited, and the edit gives the reason.
+- Where a condition needs telemetry that does not exist yet, the entry says so.
+
+**Owner.** The owner is the service that must make the change. In an entry, "owner" alone means that
+service's maintainers, which today means the OpenVibe operator (`OpenVibers`).
+
+Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compatibility/deprecations.json`,
+`compatibility/aliases.json` are both empty).
+
+## Summary
+
+| Status on 2026-09-23 | Entries |
+|---|---|
+| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |
+| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRe) |
+| Running in parallel, waiting on telemetry | C-50–C-58 (internal key), C-60–C-61 (Events v1 signatures) |
+| Legacy secret or credential still accepted | C-50–C-57 (`INTERNAL_API_KEY`), C-59 (hard-coded), C-42, C-70, C-71 (Media app keys), Games `EDITOR_KEY` |
+| Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |
+| No removal condition in any repo before this register (proposed here) | C-06, C-14, C-24, C-54, C-58, C-59, C-70, C-73, C-74, C-77, C-80, C-82, Games `EDITOR_KEY` |
+
+## Chat (Wave 6: cut over 2026-09-23 02:03 UTC)
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-01 | **`CHAT_AUTHORITY`**, Live's switch. Unset: Live runs chat itself. `chat`: `/api/chat`, `/api/dm`, `/api/tts`, `/api/sounds` answer 503, `/ws/chat` is refused, and Live's remaining chat writers forward to Chat. Production: `chat`. | Live `server/chat/chat-authority.js:19-21`, `server/chat/chat-remote.js`, `server/index.js:644-691` | Live | *documented*: the Live read mirror "retires in a later wave" (ADR-010 §Migration consequences; Chat `docs/cutover.md:5-6`). *Proposed*: every Live reader of the mirror tables reads Chat's API instead, and C-02/C-03 have both drained with no rollback for 30 days. | 2026-11-06 | Chat `docs/cutover.md:169-184`: remove the nginx include, drain the mirror, unset `CHAT_AUTHORITY`, restart Live. |
+| C-02 | **Live chat read mirror.** Chat pushes 12 tables into Live's database: `chat_messages`, `dm_*` (5 tables), `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`. Chat's `LIVE_MIRROR=1` (default 0) drives it through `live_mirror_outbox`. It arrives at Live's `POST /internal/chat-effects/mirror`, capability `live.chat_mirror.write`. | Chat `server/config.js:54`, `server/bridge/live-mirror.js`, `server/db/database.js:25-38`; Live `server/chat/live-context-routes.js:599-659` | Chat (sender), Live (tables) | Same as C-01. Then set `LIVE_MIRROR=0`, drop the mirror tables from Live after a backup, and remove the route. | 2026-11-06 | The mirror is what makes C-01's rollback lossless. `scripts/mirror-flush.js` drains it if Chat is down (Chat `docs/cutover.md:175-179`). |
+| C-03 | **`chat_bridge_outbox`**: Live's forwarded writes to Chat. It is replayed on boot. With `CHAT_AUTHORITY` unset, `drainToLocal()` applies unacknowledged rows to Live's own tables. | Live `server/chat/chat-remote.js:93-122, 237-248, 351-372`, `server/index.js:1074-1075` | Live | *proposed*: Live's remaining chat writers call Chat directly, and the outbox has been empty for 14 days. | 2026-11-06 | `drainToLocal()` on rollback. The code warns that a write Chat applied but never acknowledged gets applied twice (`chat-remote.js:354`). |
+| C-04 | **Staged tables still owned by Live** (`table_authority = 'live'`): `channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`. | Chat `server/db/database.js:39-47,116-118`; Chat `docs/cutover.md:43`; ADR-010 :17-19 | Chat + Live | *documented*: they move "when their Live writers move" (ADR-010). | 2026-12-18 | They are not moved yet, so Live stays their writer. |
+| C-05 | **Legacy chat auth**: the `token` cookie ("legacy openvibelive") next to `ov_token`, plus a `?token=` query fallback on the WebSocket. | Chat `server/auth/auth.js:43-47,62-63,86-87` | Chat | *proposed*: the Live SPA sends only `ov_token`. Needs a counter of cookie and query use, which does not exist yet. | 2026-10-23 | Accepting both keeps old tabs working. |
+| C-06 | **REST send fallback** `POST /api/chat/send`, and the legacy `?scope=stream`. | Chat `server/chat/routes.js:7,227,490` | Chat | *proposed*: no client sends them for 14 days. Unknown whether any does: check Chat's access log. | 2026-11-06 | none needed |
+
+## Pastes (Wave 5: Community is the authority since 2026-09-22 ~23:35 UTC)
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-10 | **Community `PASTES_AUTHORITY`**. The code default is `live`, which proxies to Live and Media. Production is `community` (set in `/etc/openvibe/community.env`). | Community `server/config.js:51-53`, `server/app.js:127-151`, `README.md:16-21` | Community | *documented*: the old Media paste code and Live's Media paste router "stay until usage telemetry says they're dead" (operator notes, Wave 5). *Proposed*: Media's 410 on paste writes (C-12) and Live's Media path (C-11) are both unused for 30 days. Then make `community` the only mode. | 2026-11-06 | ADR-011 §Rollback: set `PASTES_AUTHORITY=live` in Community and Live, remove Media's two flags, restart. Export Community-side writes first. **No Community→Media export tool exists.** |
+| C-11 | **Live `PASTES_AUTHORITY=community`** plus the Media paste router it replaces. Unset sends `/api/pastes` to Media. The value is read on every request. `PASTES_ON_COMMUNITY=1` 301s `/p/:slug` and `/pastes` to Community. `PASTES_AUTHORITY` and `OV_COMMUNITY_INTERNAL_URL` are missing from `.env.example`; `PASTES_ON_COMMUNITY=0` is at `.env.example:130-132`. | Live `server/pastes-client.js:5-18`, `server/media-proxy/pastes.js:250-319`, `server/index.js:597-612` | Live | Same as C-10. | 2026-11-06 | Unset `PASTES_AUTHORITY` (no restart needed). `PASTES_ON_COMMUNITY=0` needs a restart, and browsers may have cached the 301s. |
+| C-12 | **Media paste writes frozen**: `PASTES_FROZEN_APPS=live` makes every paste write answer 410 `pastes.moved`. `PASTES_MOVED_TO=https://openvibe.community` 301s `/p/:slug` and `/p/:slug/raw`. Screenshot bytes are still served by Media. | Media `server/pastes/routes.js:47-55`, `server/public/routes.js:446-494` | Media | *proposed*: the 410 path is unused for 30 days. The 301s stay as permanent redirects, because old links exist. Delete the paste write code, keep the redirect. | 2026-11-06 (write code); redirects kept | Remove both flags (ADR-011 §Rollback). |
+| C-13 | **Tools paste forwarding**: `/api/pastes/*` goes to Community (`OV_COMMUNITY_INTERNAL_URL`) with the visitor's `ov_token` and no service token. `pastes.openvibe.tools` redirects to Community. **There is no flag.** Commit `fd5d318` (2026-09-22) replaced the Live proxy, and only a revert rolls it back. | Tools `apps/gateway/server/index.js:183-207, 296-309` | Tools | *proposed*: keep the forwarding as the product path, and remove the stale comments that say Media backs pastes (`apps/gateway/server/index.js:8,12`, `apps/gateway/README.md:17-59`). `LIVE_URL` is dead apart from the startup banner (`apps/gateway/server/config.js:35-39`). Remove it. | 2026-10-09 | `git revert fd5d318` in Tools, then deploy. |
+| C-14 | **Community numeric thread ids for services.** A `/^\d{1,15}$/` id resolves only for service tokens. `threadIdFor` hands services the sequential id and browsers the `cth_` access id. The documented service callers are Blog, Wiki, Deals, Reviews and Live. | Community `server/comments/service.js:63-69,104-110`, `server/comments/api.js:28-40`, `README.md:174-178`; tests `test/comments.test.js:271-340` | Community | **No removal condition in the repo.** The code treats it as the intended design. *Proposed*: every service caller stores and sends `cth_` ids; then refuse numeric ids. This needs a per-principal counter of numeric lookups, which does not exist yet. | 2026-12-18 | Numeric ids keep existing service rows working. |
+| C-15 | **Community legacy id columns**: `pastes.legacy_media_id`, `legacy_user_id`, `paste_comments.legacy_media_id`, and `legacy_id_map`. The map also serves old JWTs that have no `subject_id`. The staff censor route also falls back to a numeric id. | Community `server/db.js:49-50,84,118-125`, `server/identity/network.js:90-103`, `server/pastes/service.js:593-596` | Community | The columns are import provenance and stay. *Proposed*: the JWT fallback goes once Network tokens older than their maximum lifetime cannot exist (every token carries `subject_id` since Wave 1, ADR-001). | 2026-10-23 (JWT fallback) | none needed |
+
+## AI (Wave 13: `AI_SERVICE=remote` in production since 2026-09-23 ~15:40 UTC)
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-20 | **`AI_SERVICE`**, Live's switch. `remote` runs `llm.complete`, translation, paste/frame, overview, media and recap calls as OpenVibe.AI workflows. Unset uses Live's own provider key. | Live `server/ai/ai-service.js:39-40`, `server/ai/llm.js:299-313` | Live | *documented*: keep the shared key in Live's settings "until the cutover has held for a while" (AI `docs/migration.md:101-103`). *Proposed*: 30 days of remote runs with no `[AI service]` warnings in Live's journal. Then remove the local shared-key path. Streamer BYO providers stay (AI `docs/migration.md:105-115`). | 2026-10-23 | Unset `AI_SERVICE` and restart Live (ADR-015 §Rollback). |
+| C-21 | **`POST /internal/ai/site-copy`** on Live, key-only (see C-50). Network calls `network.site_copy` directly since `422f8e9`, and falls back to Live's endpoint with the internal key when AI answers nothing. With `AI_SERVICE=remote`, Live forwards the call to AI. | Live `server/internal/routes.js:24-61`; Network `server/chrome/service.js:56-57,136-141` | Network (caller), Live (route) | *proposed*: remove Network's fallback, then Live's route after 14 days with no call (the procedure in Network `docs/retirement.md`). Live has no counter of its own. Network's `principal_usage` counts only Network's routes. | 2026-10-23 | Network's fallback call. |
+| C-22 | **`AI_NS_FALLBACK`** (default `live=live.*\|network.site_copy`): a service token with no `ns` gets these namespaces. Its stated reason, that Live's grant names no namespaces (`README.md:76-77`), stopped being true with Network `988330b`, which now grants `live.*` and `network.site_copy` (`server/identity/principals.js:102-103`). | AI `server/config.js:27-44`, `server/auth.js:96-103` | AI | *proposed*: every AI grant names its namespaces. Then set `AI_NS_FALLBACK=none` and remove the code. | 2026-10-09 | Set it back to the default string. |
+| C-23 | **`AI_NS_REQUIRED`** (default `true`). `false` restores the old open rule. It is documented as a "rollback lever only". | AI `server/config.js:77-79`, `.env.example:24-25`, `README.md:78-79` | AI | *proposed*: removed together with C-22, once fail-closed has held for 30 days. | 2026-10-23 | Set `AI_NS_REQUIRED=false`. |
+| C-24 | **Legacy MediaRef ids** `legacy:<svc>:<kind>:<id>` are still accepted. | AI `server/fetcher.js:17,160-163`, `server/workflows/common-schemas.js:7` | AI | *proposed*: every caller sends Media object ids (ADR-006). Unknown whether any caller still sends legacy refs: check AI `runs` inputs. | 2026-11-06 | none needed |
+
+## Money (Billing prepared, running in shadow; Live is the authority)
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-30 | **`BILLING_AUTHORITY=live`** (the default). Live holds the money: `openvibe_bucks_*`, `transactions`, `payment_orders`, `subscriptions`. `billing` makes Billing the ledger, and a tripwire (`assertLiveLedger`) makes Live's columns read-only. Any other value refuses every money write. There is no runtime shadow mode. "Shadow" means Billing's repeated shadow imports (Billing `README.md:203-204`). | Live `server/monetization/money-authority.js:26-49`, `server/db/database.js:5406-5555`; Billing `docs/live-cutover.md` | Billing + Live | *documented*: Billing `docs/live-cutover.md` steps 0-10. **Blocked on the owner**: step 4 (re-pointing the PowerChat webhook in the PowerChat dashboard). *Proposed*: after the cutover holds for 30 days, remove Live's money write paths and leave the columns read-only. | Cutover: when the owner re-points (target 2026-10-07). Removal: cutover + 30 days. | Before step 6: re-point PowerChat back to Live. Steps 6-9: unset `BILLING_AUTHORITY` and restore `billing-pre-cutover.db`. After step 9: **no tool exists** (Billing `docs/live-cutover.md:177-200`). |
+| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. |
+| C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |
+
+## Stream ingest (OpenRe deployed; no slot switched)
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRe | *documented*: "Live's ingest code stays until the last protocol has run on OpenRe for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRe `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRe `README.md:205`). |
+| C-41 | **OpenRe port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRe `server/config.js:24,72`, `README.md:139-145` | OpenRe | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |
+| C-42 | **OpenRe `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRe `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRe + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |
+
+## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`)
+
+Network `docs/retirement.md` gives the procedure:
+- Telemetry in `principal_usage` has run since 2026-09-23. Guarded routes are counted as `auth='internal-key'`, other internal routes as `auth='internal-key-route'`.
+- Move a caller to a service token, wait until the route shows no legacy-key calls for **14 days**, remove the key path for that route, and finally delete `INTERNAL_API_KEY` from every env file.
+- ADR-003: "the key is retired per route once legacy use reaches zero".
+- First observation: `GET /internal/url-registry/resolved`, from Live's registry refresh (2026-09-23 04:50 UTC).
+- **No HTTP route lists `principal_usage`.** Read it with the SQL in `docs/retirement.md`.
+
+The dates below assume each caller moves by **2026-10-09**. The key paths then go on **2026-10-23**, and the variable goes from every env file on **2026-11-06**. Where a route has no counter, the entry says so.
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-50 | **Network key-only `/internal/*` routes**: verify-token, users/*, link-account, user-avatar, audit, stats, url-registry/resolved, coins/stats, notifications/unread\|mark-read\|resolve-users, issue-token, resolve-anon, anon-admin, anon-list, identity/legacy-map. The key defaults to `'change-me-in-production'`, and `OV_INTERNAL_KEY` is a second env name for it. | Network `server/internal/routes.js:39-57,75-720`, `server/config.js:28-29` | Network | *documented*: 14 days with zero `legacy-key` rows per route (`docs/retirement.md`). | 2026-10-23 | The key keeps working per route until that route's key path is removed. |
+| C-51 | **Network routes that take a token or the key** (`TOKEN_ROUTES`): identity resolve and resolve-batch, coins credit/debit/transfer, notifications push and push-bulk, events/stream-live, modules. `principals.guard(…, { legacy: true })`. | Network `server/internal/routes.js:59-60`, `server/identity/principals.js:235-247` | Network | Same as C-50, per route. | 2026-10-23 | Callers fall back to the key (ADR-003 §Rollback). |
+| C-52 | **Live's key-only outbound calls to Network**: `/internal/url-registry/resolved`, `/internal/identity/legacy-map`, `/internal/coins/stats`, `/internal/resolve-anon`, `/internal/link-account`, `/internal/user-avatar`, `/internal/notifications/mark-read`. | Live `server/config.js:290-300`, `server/auth/identity-sync.js:45-47`, `server/monetization/wallet-client.js:151-152`, `server/chat/chat-server.js:167-171`, `server/utils/notify.js:85,103,143` | Live | Move each call to a service token, then C-50. | callers 2026-10-09 | none needed |
+| C-53 | **Live's token-with-key-fallback calls** (`TOKEN_PATHS`). When no token can be had they fall back to the key and pause token attempts for 5 min. This includes the go-live notification (`/internal/events/stream-live`), whose second path is ADR-020's Events consumer. `notify._post` returns null when there is no key, even on token-capable paths. | Live `server/net/network-principal.js:21,47-61`, `server/utils/notify.js:38-39`; ADR-020 §Migration consequences | Live + Network | *documented* (ADR-020): the two paths run side by side "until parity is shown; then the push call is removed". Precondition: Network can read Live's followers (D09 or a capability-guarded read). *Proposed*: drop the key fallback when C-51 goes. | Key fallback 2026-10-23; push call unknown (blocked on D09) | Re-enable the direct push (ADR-020 §Rollback). |
+| C-54 | **Live's key-only inbound routes** (loopback only): `POST /internal/ai/site-copy`, `/internal/url-registry/refresh`, `/internal/user-avatar`, `/internal/user-role`, `GET /internal/analytics-summary`, `POST /api/cosmetics/internal-unlock`. | Live `server/internal/routes.js:8-18,30-106`, `server/index.js:557-565`, `server/monetization/cosmetics-routes.js:85-99` | Live (routes), Network (callers) | *proposed*: add service-token guards on Live (`server/net/service-guard.js` exists), move Network's callers, then 14 days with no key use. **Live has no key-use counter.** Add one, or read Live's access log. | 2026-10-23 | none needed |
+| C-55 | **Network's outbound key calls**: Live `/internal/user-avatar`, Media `/internal/avatar-ingest`, Live `/internal/analytics-summary`, Live `/internal/ai/site-copy` (C-21), Live `/internal/user-role` (reads `INTERNAL_API_KEY \|\| OV_INTERNAL_KEY`), and admin analytics. | Network `server/profile/avatar.js:66,75`, `server/chrome/service.js:77,138`, `server/admin/routes.js:120,220-224`, `server/admin/analytics-routes.js:16,57` | Network | Move to service tokens with capabilities on the callee. | 2026-10-09 | none needed |
+| C-56 | **Media `POST /internal/avatar-ingest`**: key only, loopback, ≥16-character key, otherwise 404. `VIEW_HASH_SECRET` falls back to `MEDIA_SECRET` and then `INTERNAL_API_KEY`. | Media `server/index.js:55-63`, `server/views/service.js:27-33` | Media | *proposed*: a service-token guard on avatar-ingest; set `VIEW_HASH_SECRET` explicitly so removing the key does not change view hashes. | 2026-10-23 | none needed |
+| C-57 | **Tools satellites** `/api/internal/analytics` and `/analytics/bots`: key only, alias `OV_INTERNAL_KEY`. `apps/gateway/.env.example:30-31` describes it wrongly. | Tools `apps/_shared/internal-auth.js:11-17` | Tools | *proposed*: the gateway aggregates with a service token, or loopback without a key. | 2026-10-23 | none needed |
+| C-58 | **Network's cross-database read of Live.** On every boot, `syncLinkedLiveRoles` opens Live's database read-only, from `OPENVIBELIVE_DB_PATH`, then `/opt/openvibelive/data/live.db`, then `../live/data/live.db`. This breaks ADR-007 ("no service reads or writes another service's database"). None of the default paths is Live's production path (`/opt/openvibe.live/data/live.db`). Whether it runs in production: **unknown**. Check `/etc/openvibe/network.env` for `OPENVIBELIVE_DB_PATH`. | Network `server/db/database.js:28-99`, called at `:751` | Network | *proposed*: remove the function. Roles already reach Network through `/internal/user-role` and the identity map. | 2026-10-09 | none needed |
+| C-59 | **Hard-coded legacy secret** `X-Internal-Secret: openvibe-internal-2026`. Live's `/api/cosmetics/internal-unlock` accepts it from loopback. Live's `questApi` sends it to `LEGACY_QUEST_API = http://127.0.0.1:3200`, the pre-OpenVibe game. Games now runs on :8000. Whether anything listens on :3200: **unknown**. Check `ss -ltnp` on the host. | Live `server/monetization/cosmetics-routes.js:86-94`, `server/monetization/cosmetics.js:11,222-275` | Live (+ Games for inventory) | *proposed*: remove the literal secret and the :3200 client. If cosmetics still need the game inventory, Games exposes it behind a service token. | 2026-10-09 | none needed |
+
+## Events delivery signatures
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-60 | **v1 signature** `X-OpenVibe-Signature` (HMAC of the body, no timestamp) is sent on every delivery next to v2 (`X-OpenVibe-Timestamp`, `X-OpenVibe-Signature-V2`, ±300 s). **There is no env switch to stop v1.** Production: Events sends v2. | Events `server/worker.js:56,66-68`, `lib/client.js:36-64` | Events | *documented*: "v1 stays on the wire until every consumer requires v2, and only then may it be removed" (ADR-004; Events `docs/replay-window-rollout.md` step 5, "optional"). Every consumer has committed `requireV2`/`verifyDeliveryV2`: Live `ae07553`, Search `5a7e59b`, News `1802e7d`, Deals `e72aeec`, Trade `9a55644`, Reviews `3151c05`, Tips `435f32b`, VIP `dc98781`, Examples `6ea9cfd`. Which of these are deployed: **unknown**. Check each `/opt/openvibe.<svc>` HEAD. Condition: all deployed, plus one clean day for Tips and VIP. | 2026-10-07 | Consumers unset `requireV2`. Events keeps sending v1 until then (ADR-004). |
+| C-61 | **Codes webhook tester** checks and generates v1 only. The docs page already describes v2. | Codes `server/domain/webhooks.js`; Events `docs/replay-window-rollout.md:58` | Codes | Tester inspects `X-OpenVibe-Signature-V2` (rollout doc follow-up). Needed before C-60. | 2026-10-07 | none needed |
+
+## Media, Live storage and legacy data
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-70 | **Live `MEDIA_API_KEY`**, the per-app bearer key that Live sends to Media, with `X-OV-User-Id`. **Live has no service-token path to Media.** Media already accepts principal tokens for audience `openvibe.media` (Media `server/auth.js:221-304`). Community, Tools, Games, Wiki and Blog are token-only tenants (`:162-173`). | Live `server/media-client.js:17-51`; Media `server/auth.js:353-396` | Live + Media | *proposed*: Live's media-client moves to a service token; then remove the `live` app key hash from `apps`. **Media has no key-vs-token counter.** Add one before the 14-day wait. | 2026-11-06 | The key keeps working. |
+| C-71 | **`MEDIA_APP_KEYS` short form** and `MEDIA_APPS_SEED` key seeding. | Media `.env.example:37-38`, `server/auth.js:370-392` | Media | With C-70 and C-42: when no tenant uses a key. | 2026-11-06 | none needed |
+| C-72 | **ADR-021 backups that still hold IPs**: `/opt/openvibe.live/data/backups/analytics-pre-adr021-2026-09-23.db`, `/opt/openvibe.tools/backups/analytics-2026-09-23`, Network `data/backups/network.db.pre-adr021-20260923T1634` (operator notes, not in a repo). | host | Live, Tools, Network | *documented* (operator notes): delete after 2026-10-07. | 2026-10-07 | They are the only way back to pre-ADR-021 analytics. |
+| C-73 | **Live's legacy `vods`/`clips`/`pastes` tables**. They were frozen for the Media cutover, and the canonical copies are now in Media and Community. **There is no tripwire; only comments** (`server/db/database.js:3223-3224`). One write path remains: `PUT /api/admin/pastes/:id/nsfw` (`server/admin/routes.js:427-434`). Exported write helpers with no callers remain: `updatePasteAi`, `updateVodHealth`, `repairVodDuration`, `updatePaste`, `deletePaste`, `likePaste`. Live still **reads** them for live pages: `server/ai/chat-ai-routes.js:337`, `server/admin/routes.js:91-92`, `server/arena/arena-service.js:436`, `server/streaming/routes.js:1547`, and about 70 queries in `database.js`. These reads serve pre-cutover data. | Live (above); `docs/vods-and-clips.md:3-5` | Live | **No removal plan in the repo.** *Proposed*: (1) remove the nsfw write and the dead helpers, and add a tripwire like `assertLiveLedger`, by 2026-10-09; (2) move the four readers to Media or Community; (3) back up and drop the tables after 30 days with no reader. | 2026-11-06 | The tables are the only local copy of pre-Media rows. Keep the backup. |
+| C-74 | **Pre-Media local paths** `VOD_PATH`, `CLIPS_PATH`, `COLD_STORAGE_PATH`, `THUMBNAILS_PATH` are still read by Live's config. | Live `server/config.js:251-257` | Live | *proposed*: no code reads the directories. **Unknown** whether any does. Grep for each config key. | 2026-11-06 | none needed |
+| C-75 | **Media dual write**: the old tables are written first, then synced into `media_objects`. A sync failure only logs. Old routes also stay: `/v/:id` basename fallback, `/api/thumbnails/:name` with app `live` hard-coded, `/f/screenshots/:name`, and `/tos` → `/terms`. Webhooks to Live are published alongside Events. | Media `docs/object-model.md:3-7,88-112`, `server/db/database.js:100-124`, `server/public/routes.js:176-190,346-384`, `server/events.js:1-18` | Media | *documented*: "The object layer is additive; old routes keep working throughout" (ADR-006 §Rollback). Webhooks stay "until every consumer reads events" (`server/events.js`). *Proposed*: Live consumes `media.*` events instead of `/internal/media-webhook`, then remove the webhooks. | 2026-12-18 | Old routes and tables. |
+| C-76 | **Live legacy (in-place) deploy layout**, "current production" (Live `docs/deploy.md:31-32`). A rollback resets the checkout but not `node_modules`. `--rollback` is refused in this layout. | Live `deploy/scripts/deploy.sh:347,394-419`, `deploy/systemd/openvibe-live.service` | Live | *documented*: the one-time `deploy/scripts/migrate-to-releases.sh` (one restart). | 2026-10-09 | `git reset` to the old commit. |
+| C-77 | **`ALLOW_P2P_FALLBACK`** (default `false`), an "emergency rollback" to legacy P2P relays, which can expose viewer IPs. | Live `server/config.js:87-93`, `SETUP.md:136` | Live | **No condition documented.** *Proposed*: remove the flag and the P2P relay path. | 2026-11-06 | Set the flag to `true` (not recommended). |
+
+## Shared packages, sites and closed repositories
+
+| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
+|---|---|---|---|---|---|---|
+| C-80 | **`openvibe-shared/analytics.js`** (`AnalyticsTracker`) stores raw IP, `user_id`, city and full UA. **No deployed service uses it.** Live (`server/analytics/`, `0e07aec`), Network (`server/analytics/`) and Tools (`apps/_shared/analytics`, `533c9a3`) run their own ADR-021 trackers. No other repo requires it. The one remaining `require` is the stale local checkout `~/OpenVibers/OpenVibe.Live` (`e266060`, behind `origin/main`). It is still exported (`package.json:10`, `index.js:8,40-44`) with no deprecation marker. Stale docs: Shared `docs/migration-plan.md:29,252,493`; Live `SECURITY_AUDIT.md:98`. | OpenVibe.Shared `analytics.js` | Shared | **No condition documented** (ADR-021 does not mention it). *Proposed*: mark it deprecated in the next Shared minor, then remove it in the next major, since removing an export is breaking (ADR-008). | Deprecate 2026-10-09; remove in Shared v2.0.0, 2026-11-06 | Pin the previous Shared tag. |
+| C-81 | **OpenVibe.Sites placeholders**. There are 16, all `"stage": "placeholder"`: openvibe.chat, .news, .reviews, .tips, .vip, .trade, .host, .deals, .coupons, openre.stream, and auth., api., admin., themes., realtime. and ai.openvibe.network. The page says "coming soon · this page is a placeholder, nothing here is live yet". None claims to be a working product. **Two defects:** repos outside `EXISTING_REPOS` are labelled "(charter only, no code yet)", which is false for Tips, VIP, News, Reviews, Trade, Deals, Coupons and Host; and **realtime.openvibe.network advertises a product that ADR-005 closed**. | Sites `sites.json`, `build.js:22,157-161,283`, `deploy/nginx/*.conf` | Sites (each service owns its launch) | *documented*: each entry is "Removed from sites.json in the same release that the real service takes over this domain" (`build.js:283`, `README.md:20-21`, test `test/build.test.js:39-43`). *Proposed*: fix the "charter only" label from the repo's STATUS, and remove the realtime placeholder now. | Realtime and label: 2026-09-30; the rest: at each launch | Re-add the entry. |
+| C-82 | **OpenVibe.Realtime**, closed by ADR-005: realtime runs inside Events as SSE `/realtime/stream`. `STATUS.json` says `stage: closed`. Leftovers: the README still says "Decision pending" (`:12,51-53`); the GitHub repo is not archived; Contracts `manifests/services/realtime.json:8` says `"status": "placeholder"`; Events `README.md:6` says "until ADR-005 decides"; and the Sites placeholder (C-81). | OpenVibe.Realtime; Contracts; Sites; Events | Contracts (manifest), Sites, Events | *proposed*: fix the README, mark the manifest `closed` (a Contracts release), remove the Sites placeholder, archive the repo. | 2026-09-30 | ADR-005: revisit only on measured load. |
+
+## Kept by design (not shims)
+
+These match the search terms but are permanent. They are listed so the next review does not re-open them.
+
+- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.
+- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.
+- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).
+- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRe use their own handlers.
+- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.
+- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.
+- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).
+- **Games `EDITOR_KEY`**: "Fallback admin secret for the map editor (until openvibe.network SSO)" (`apps/server/src/config.ts:82-83`). This one *is* a legacy secret, with no removal date. *Proposed*: SSO staff check for the editor by 2026-11-06, owner Games.
new file mode 100644
new file mode 100644
@@ -0,0 +1,371 @@
@@ -0,0 +1,371 @@
+# Final cutover runbook (roadmap Wave 22)
+
+The Wave 22 exit criterion is: "The final cutover runbook, with rollback, is absorbed into OpenVibe.Host."
+This page gathers every service cutover of the realignment into one ordered runbook. Each section gives the
+cutover's production state and its rollback, and links the per-repository document that stays
+authoritative for the details. When a step here and its original disagree, the original wins; fix this
+page.
+
+- Host: `ssh openvibe-ovh` (single host). The public IPv4 is in `~/OpenVibers/RUNBOOK.md` on the
+ operator workstation, which is not in a repository.
+- Env files: `/etc/openvibe/<service>.env`. Units: `openvibe-<service>.service`. Checkouts:
+ `/opt/openvibe.<service>`.
+- Live paths are from `~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`).
+- Production state is from the operator notes of 2026-09-23.
+
+State markers:
+- **DONE**: executed on production, with the date.
+- **IN PROGRESS**: partly on production.
+- **BLOCKED (owner)**: waits for something only the owner can do. The action is spelled out.
+- **READY**: nothing blocks it.
+- **NOT STARTED**
+
+## Overview
+
+| # | Cutover | State | Per-repo original | Rollback in one line |
+|---|---|---|---|---|
+| A | Pastes → OpenVibe.Community (Wave 5) | **DONE** 2026-09-22 ~23:35 UTC | ADR-011; Community `README.md:16-21,354,387-390` | `PASTES_AUTHORITY=live` in Community and Live, remove Media's two flags, restart all three |
+| B | Chat → OpenVibe.Chat (Wave 6) | **DONE** 2026-09-23 02:03 UTC | Chat [`docs/cutover.md`](https://github.com/OpenVibers/OpenVibe.Chat/blob/main/docs/cutover.md) | Remove the nginx include, drain the mirror, unset `CHAT_AUTHORITY`, restart Live |
+| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live |
+| D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ |
+| E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |
+| F | RTMP ingest → OpenRe.Stream | **BLOCKED (owner)**: DNS `ingest.openre.stream` and port 1936. OpenRe is deployed. | OpenRe [`README.md` "Cutover runbook"](https://github.com/OpenVibers/OpenRe.Stream/blob/main/README.md#cutover-runbook) | Per slot `{"authority":"live"}`. All slots: unset `OPENRE_URL` |
+| G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |
+
+The remaining order is D → E → F → G.
+- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.
+- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on
+ OpenRe.
+- G follows each entry's date in the register.
+
+## What the owner must do
+
+These actions need the owner's accounts or physical presence. Nothing in this runbook can do them.
+
+1. **PowerChat dashboard, for step E4.** In the PowerChat developer dashboard, for OpenVibe.Live's app,
+ change the webhook URL:
+ - from `https://openvibe.live/api/powerchat/webhook`
+ - to **`https://billing.openvibe.network/webhooks/powerchat`**
+
+ Keep the **same signing secret** (Billing's `POWERCHAT_WEBHOOK_SECRET` equals Live's
+ `powerchat_webhook_secret`). Do this only at step E4 of the sequence, **not before**: Live freezes
+ first, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint
+ (Billing `docs/live-cutover.md:131-134, 170-175`).
+2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,
+ **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRe `README.md:145`,
+ `deploy/nginx/openre.stream.conf:15-17`).
+3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRe
+ `README.md:145,160`).
+4. **Broadcaster windows, for step F.** For each slot, agree a maintenance window with its broadcaster.
+ The slot is offline, and the broadcaster regenerates the key and pastes the new server into OBS
+ (ADR-009).
+5. **Cloudflare browser cache TTL** (ADR-016, not a cutover, but it affects every deploy). Set the zone
+ to "Respect existing headers". Today Cloudflare raises `/shared/*` `max-age=300` to 4 h.
+
+## 0. Deploy rules that apply to every step
+
+These rules are learned from outages. `ovhost` encodes them (see [README](../README.md#purpose)). As of
+2026-09-23 it is used read-only on the host, so the per-repo scripts are still what runs.
+
+- **Live**: `sudo /opt/openvibe.live/deploy/scripts/deploy.sh [--wait-idle|--restart|--force]`
+ (`deploy/scripts/deploy.sh:7-12`).
+ - Production uses the legacy in-place layout (Live `docs/deploy.md:31`). `--rollback` is refused
+ there (`deploy.sh:347`), and a failed-readiness rollback resets the checkout without restoring
+ `node_modules`. The release layout (`deploy/scripts/migrate-to-releases.sh`) is register entry C-76.
+ - **`openvibe-live.socket` survives restarts.** It has no `PartOf=`
+ (`deploy/systemd/openvibe-live.socket:13-23`). The script makes sure it is active and restarts only
+ `openvibe-live` (`deploy.sh:205-222`). HTTP queues on the socket during boot. WebSocket, WHIP, WebRTC
+ and RTMP sessions drop and reconnect. **Never stop or restart the socket.**
+ - Readiness: polls `/api/ready` every 1 s for up to 90 s. If it is not ready, the script rolls back and
+ exits 3; if the rollback also fails, it exits 4 (**manual intervention**) (`deploy.sh:31-32,139-147`).
+ - `--wait-idle` polls `/api/streams` every 60 s and needs 2 idle checks in a row, for up to 8 h
+ (`deploy.sh:120-137`). The script treats a failed `curl` as zero streams (`deploy.sh:121-125`).
+ Check by hand when it matters.
+ - `public/`, `docs/` and tests deploy without a restart. Schema changes take an online backup to
+ `data/backups/live-<ts>.db` first (`deploy.sh:192-203`). `deploy/nginx/` is **never** installed by
+ the script.
+ - Owner decision, 2026-09-23: restarts while streamers are live are permitted "for now". Prefer
+ `--wait-idle` when it costs nothing, and say when a restart hit a live stream.
+- **Media**: there is **no deploy script** in the repo. Pull, `npm ci --omit=dev` if dependencies
+ changed, then `systemctl restart openvibe-media`.
+ - **Do not restart Media while it is recording** (hazard H3,
+ `OpenVibe.Network/docs/roadmap-baseline/10-hazards.md:11`). Check first:
+ `curl -s 127.0.0.1:4100/api/ready | jq .recordings_in_progress`, or ovhost's probe
+ `SELECT count(*) FROM vods WHERE is_recording = 1` run read-only as the service user.
+ - If a restart does cut a recording: 5 s after boot, Media finalizes every `is_recording = 1` row that
+ has no ffmpeg. It merges chunks, remuxes, falls back to `.master.mkv`, and marks very short results
+ `needs_review` (`server/index.js:197-210`, `server/vod/finalize.js`). Interrupted clips are marked
+ failed.
+ - **Known gap:** the unit's `TimeoutStopSec=30` (`deploy/systemd/openvibe-media.service:16`) is
+ shorter than the recorder's ffmpeg stop grace (`VOD_STOP_GRACE_MS`, default 60 000 ms,
+ `server/vod/recorder.js:35`). systemd therefore SIGKILLs a stop that is still flushing a recording,
+ and the boot sweep has to recover it.
+- **Tools**: `deploy/scripts/deploy.sh` (run from the checkout).
+ 1. Fetch, and drop untracked lockfiles that the incoming release tracks.
+ 2. `git pull --ff-only`, and `npm install --omit=dev` in apps whose `package.json` changed.
+ 3. Reinstall any dependency that does not resolve. If it still does not, **ABORT, nothing restarted**.
+ 4. Check that img, audio and docs load the jobs runtime.
+ 5. Restart **all** `openvibe-tools*` units at once, then curl the gateway's `/api/health`.
+
+ (`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.
+ `apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.
+- **OpenRe.Stream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.
+ - `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure
+ (exit 2).
+ - `workers` starts a new generation. The old ones drain for up to 30 min.
+ - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRe `README.md:147-160`.)
+- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,
+ install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own
+ `deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).
+- **Always**:
+ - Stage explicit paths when committing on the host. Never `git add -A`: on 2026-09-23 02:09 it
+ committed half-done work and took Network down for two minutes.
+ - Production WebSocket probes send `join` only, never `chat` or `dm` frames.
+ - Take `sudo ovhost backup <service>` before any data move.
+
+## A. Pastes → OpenVibe.Community (Wave 5): DONE 2026-09-22
+
+Community owns pastes and comments (ADR-011). Production values:
+
+| Service | Setting | Effect |
+|---|---|---|
+| Community | `PASTES_AUTHORITY=community` in `community.env` | Serves pastes natively. The database is `/var/lib/openvibe-community/community.db`. |
+| Live | `PASTES_AUTHORITY=community` in `live.env` | `/api/pastes` forwards to Community with a service token (`server/pastes-client.js`, `server/media-proxy/pastes.js:250-319`). |
+| Media | `PASTES_FROZEN_APPS=live` | Paste writes answer 410 (`server/pastes/routes.js:47-55`). |
+| Media | `PASTES_MOVED_TO=https://openvibe.community` | 301 for `/p/:slug` and raw text (`server/public/routes.js:446-494`). |
+| Tools | none | Forwards `/api/pastes/*` to Community with the visitor's token, and has no switch (`apps/gateway/server/index.js:183-207`). |
+
+Import: 889+2 pastes, 60 comments, 48 likes. Held: 1 paste and 4 comments, `ambiguous_owner`. Shadow
+parity before the flip was 878/878 (ADR-011 §Migration consequences).
+
+**Verify:** `curl -s 127.0.0.1:4200/api/ready | jq .pastes_authority` must print `"community"`.
+
+**Rollback** (ADR-011 §Rollback):
+1. Export the writes made in Community since the cutover. **No export tool exists.** Write one before you
+ need it (register C-10).
+2. Set `PASTES_AUTHORITY=live` in `community.env` and `live.env`.
+3. Remove `PASTES_FROZEN_APPS` and `PASTES_MOVED_TO` from `media.env`.
+4. Restart Community, Live and Media.
+
+ Tools has no switch: revert `fd5d318` in Tools and deploy. Community deletes are soft and never
+ remove Media bytes.
+
+## B. Chat → OpenVibe.Chat (Wave 6): DONE 2026-09-23 02:03 UTC
+
+Chat serves `/ws/chat`, `/api/chat/`, `/api/dm/`, `/api/tts/` and `/api/sounds`. It runs on
+127.0.0.1:4400 (`openvibe-chat`) with `/var/lib/openvibe-chat/chat.db`, `LIVE_MIRROR=1` and `EVENTS_URL`
+set. Traffic reaches it through `include /opt/openvibe.chat/deploy/nginx/*.locations.conf;` in
+openvibe.live's vhost. Live runs `CHAT_AUTHORITY=chat` and `OV_CHAT_INTERNAL_URL`. Its chat tables are a
+read mirror kept current by Chat (register C-01–C-03).
+
+Record of the run (Chat `docs/cutover.md`):
+- Prerequisites `:51-84`: Network principal `chat` and grants; Live patch deployed without the flag;
+ Chat installed and not started.
+- Rehearsal `:86-123`.
+- Cutover `:125-167`:
+ 1. Start Chat and import snapshot 1.
+ 2. Set `CHAT_AUTHORITY=chat`, restart Live, add the include, `nginx -t && systemctl reload nginx`.
+ 3. Import pass 2. `held` must be 0.
+ 4. Check that `mirror.pending` and `chat_bridge_outbox` are near 0, and that no `ChatRemote` refusals
+ appear.
+- Imports: pass 1 had 70,860 messages, held 0. Parity was 15/15 read paths (ADR-010 §Acceptance tests).
+- The pre-cutover nginx backup is `/root/openvibe.live.conf.pre-chat-<ts>` on the host.
+
+**Verify:** `curl -s 127.0.0.1:4400/ready | jq '.live, .mirror'`.
+- `mirror.pending` should be near 0.
+- `live.failures` should not grow.
+
+`/ready` stays 200 while Live syncs fail (see [storage-state.md](storage-state.md) finding 1), so read the
+body, not just the status.
+
+**Rollback** (Chat `docs/cutover.md:169-184`, ADR-010 §Rollback). The order matters:
+1. Remove the include from openvibe.live's vhost, then `sudo nginx -t && sudo systemctl reload nginx`.
+ Chat traffic returns to Live, which answers 503 until step 3.
+2. Make sure everything Chat wrote has reached Live. `curl -s 127.0.0.1:4400/ready | jq .mirror.pending`
+ must be `0`. If Chat is down, run `scripts/mirror-flush.js` in the Chat checkout; exit 0 means it is
+ drained. This must happen **before** step 3, because Live accepts mirror writes only while
+ `CHAT_AUTHORITY=chat`.
+3. Remove `CHAT_AUTHORITY` from `/etc/openvibe/live.env` and restart Live
+ (`deploy.sh --restart`). At boot, `drainToLocal()` applies `chat_bridge_outbox` to Live's own tables
+ (`server/chat/chat-remote.js:351-372`). A write Chat applied but never acknowledged is applied twice
+ (`:354`).
+4. `sudo systemctl stop openvibe-chat`. Keep `/var/lib/openvibe-chat/chat.db`.
+
+## C. AI → OpenVibe.AI (Wave 13): DONE 2026-09-23
+
+OpenVibe.AI runs on 127.0.0.1:4700 as principal `ai`, with `/var/lib/openvibe-ai/ai.db`. Live runs
+`AI_SERVICE=remote` (`server/ai/ai-service.js:39`). With it, `llm.complete`, translation, paste/frame,
+overview, media and recap calls go through AI workflows. There is **no fallback to Live's own key** when
+AI returns nothing (`server/ai/llm.js:299-313`). Streamer BYO providers bypass AI.
+
+Steps from AI `docs/migration.md:72-99`:
+- Steps 1-6 (deploy, import, grants, Live `AI_SERVICE=remote`, verify) are **DONE**.
+- Step 7 is **DONE**: Network calls `network.site_copy` itself (Network `422f8e9`). Live's
+ `/internal/ai/site-copy` remains as Network's fallback (register C-21).
+- The Live grant names its namespaces (`live.*`, `network.site_copy`; Network `988330b`), and AI fails
+ closed on a token with no `ns` (register C-22/C-23).
+
+**Verify:**
+- `curl -s 127.0.0.1:4700/api/ready`.
+- `GET /api/v1/runs?workflow=live.translate` shows recent succeeded runs.
+- `journalctl -u openvibe-live | grep '\[AI service\]'` is empty.
+
+**Rollback** (AI `docs/migration.md:101-103`, ADR-015 §Rollback):
+1. Unset `AI_SERVICE` in `live.env` and restart Live. Live then uses its own provider settings. **Keep the
+ shared key in Live's settings** until C-20's removal date. Without it, this rollback has no provider.
+2. Namespace problems only: set `AI_NS_FALLBACK` back to its default, or set `AI_NS_REQUIRED=false`, in
+ `ai.env`, then restart AI.
+
+Stopping OpenVibe.AI touches no product data.
+
+## D. Events delivery signature v2: IN PROGRESS
+
+Events signs every attempt with v1 (`X-OpenVibe-Signature`) **and** v2 (`X-OpenVibe-Timestamp`,
+`X-OpenVibe-Signature-V2` = HMAC of `"<t>.<raw body>"`, ±300 s) (Events `server/worker.js:56,66-68`).
+v1 cannot be switched off by env.
+
+| Step (Events `docs/replay-window-rollout.md:22-34`) | State |
+|---|---|
+| 1. Tag openvibe-sdk `v0.4.0` | **DONE**: the tag exists. The doc's status line (`:3`) is stale. |
+| 2. Deploy Events. Confirm one real delivery carries the v2 headers. Compare `date +%s` with a trusted clock. | **DONE**: Events sends v2 (operator notes). |
+| 3. Codes docs | **DONE** in the repo (Codes `a9371c3`). The tester still checks v1 only (register C-61). |
+| 4. Consumers, one at a time, in this order: Live (`/internal/openre-events`), Search, News, Deals, Trade, Reviews, then Tips and VIP after a clean day, then Examples | **Committed in every repo**: Live `ae07553`, Search `5a7e59b`, News `1802e7d`, Deals `e72aeec`, Trade `9a55644`, Reviews `3151c05`, Tips `435f32b`, VIP `dc98781`, Examples `6ea9cfd`. **Deployed: unknown.** Check `git -C /opt/openvibe.<svc> log -1` for each. |
+| 5. Stop sending v1 (optional; only after every consumer requires v2) | **NOT STARTED** (register C-60, target 2026-10-07) |
+
+For each consumer deploy:
+- Pin `openvibe-sdk` v0.4.0 (Search has no SDK), run `npm test` on Node 22, and deploy.
+- Watch that service for `401 *.bad_signature`.
+- Watch Events with `GET /api/v1/deliveries?status=failed&subscription_id=…`.
+
+**Rollback** (rollout doc `:36`, ADR-004): redeploy the consumer's previous release, which unsets
+`requireV2`. Events keeps retrying for about 2 h 13 min (1 s, 5 s, 30 s, 2 min, 10 min, 1 h, 1 h), then
+moves the delivery to the DLQ. Requeue with `POST /api/v1/deliveries/replay { subscription_id, event_ids }`
+or `{ subscription_id, from_seq }`. Replays are signed afresh, and the consumer inbox dedupes on
+`event_id`. Rolling back Events itself is safe only while no consumer requires v2.
+
+## E. Money → OpenVibe.Billing: BLOCKED on the owner (PowerChat dashboard)
+
+Billing runs in shadow on 127.0.0.1:4600 (principal `billing`), and its staff console is public at
+`https://billing.openvibe.network`. Live is the authority: `BILLING_AUTHORITY` unset = `live`
+(`server/monetization/money-authority.js:26-31`). Billing's own status is "prepared, **not executed**"
+(Billing `docs/live-cutover.md:3`).
+
+**Before the day** (Billing `docs/live-cutover.md:56-102`):
+
+| Prerequisite | State |
+|---|---|
+| 1. `billing.env`: `POWERCHAT_WEBHOOK_SECRET` equal to Live's, `POWERCHAT_SITE_USERNAME`, `POWERCHAT_ALLOW_TEST_FULFILLMENT` off, `BILLING_*` rates equal to Live's | `POWERCHAT_SITE_USERNAME` set 2026-09-23 (operator notes). The rest: unknown. Check with `sudo ovhost validate billing`, which prints names only. |
+| 2. Network grants for client `live`: `billing.intent.create`, `billing.transfer.create`, `billing.balance.read`, `billing.cashout.request`, `billing.subscription.manage`, `billing.entitlement.check`, plus `identity.subject.resolve`. **Not** `billing.cashout.manage` or `billing.ledger.admin`. | **DONE** (operator notes: Live granted `billing.*` except `cashout.manage` and `ledger.admin`) |
+| 3. Staff console: redirect URI, `BILLING_STAFF_SUBJECTS`, `BILLING_SESSION_SECRET`, vhost, sign-in works | **DONE** (console live; staff subject is the owner only) |
+| 4. Live patch deployed with `BILLING_AUTHORITY` unset; `/api/admin/money` shows `authority: "live"` | **DONE** (Live `c384787`) |
+| 5. `identity-legacy-sync` has run since the last new account; a fresh shadow import has no holds for anyone with a balance | Shadow import done 2026-09-23: 322 transactions, reconcile OK. Re-run it on the day. |
+
+**The sequence** (Billing `docs/live-cutover.md:116-168`). Keep the order: it guarantees that every
+PowerChat delivery lands in exactly one ledger.
+
+0. Check who is live: `curl -s https://openvibe.live/api/streams`. Step 6 restarts Live.
+1. Freeze Live money writes: `POST /api/admin/money/freeze {on:true, reason:'Billing cutover'}` (owner).
+ `GET /api/admin/money` must show `frozen: true`.
+2. Wait **at least 60 minutes**, because minted PowerChat links live for an hour. Then run
+ `POST /api/powerchat/reconcile`. List `payment_orders` that are `pending` or `paid` from the last 3
+ days. A `paid` row is an anomaly; fix it first.
+3. Back up Billing to `/var/lib/openvibe-billing/billing-pre-cutover.db`, **keep it for rollback**. Then
+ run `node scripts/freeze.js on 'Live cutover'`.
+4. **Owner: re-point the PowerChat webhook** (see "What the owner must do", item 1). From here on, Billing
+ stores and holds every delivery (202).
+5. Take the final snapshot to `/var/backups/openvibe/live-cutover-<date>.db` and **keep it**. Run
+ `import-live.js --dry-run`, then the import, then `reconcile.js`. `holds` must be empty for anyone with
+ money, and reconciliation must print **OK**.
+6. Add `BILLING_AUTHORITY=billing`, `OV_BILLING_INTERNAL_URL=http://127.0.0.1:4600` and
+ `OV_BILLING_PUBLIC_URL=https://billing.openvibe.network` to `live.env`. Run
+ `sudo /opt/openvibe.live/deploy/scripts/deploy.sh --wait-idle`.
+ `GET /api/admin/money` must show `authority: "billing"`, `reachable: true` and `drift: []`.
+7. Unfreeze Billing with `node scripts/freeze.js off`. Held deliveries settle once. Deliveries that Live
+ already credited are rejected (`billing.intent_settled_in_live`). Then `reconcile.js` must print OK.
+8. Verify the reads while Live is still frozen: balances match, and `billing.actions.attention: []`.
+9. Unfreeze Live: `POST /api/admin/money/freeze {on:false}`.
+10. Make a real 100-Vibes PowerChat purchase. Expect one settled event, +100 exactly once, reconcile OK,
+ and Live's legacy columns unchanged.
+
+**Rollback** (Billing `docs/live-cutover.md:177-200`):
+- **Before step 6:**
+ 1. Re-point PowerChat back to `https://openvibe.live/api/powerchat/webhook`.
+ 2. Leave Billing **frozen**.
+ 3. Unfreeze Live. Live's reconciler (every 15 min) backfills payments made in the window.
+- **After step 6, before step 9:**
+ 1. Remove `BILLING_AUTHORITY` and run `deploy.sh --wait-idle`.
+ 2. Re-point PowerChat to Live and unfreeze Live.
+ 3. Stop Billing, restore `billing-pre-cutover.db` (delete its `-wal`/`-shm` files), freeze it, and start
+ it.
+- **After step 9:** ADR-012's rollback re-derives Live's columns from Billing's journal. **No tool for this
+ exists.** It is manual and lossy. Prefer fixing forward.
+
+**What the switch does not preserve** (`:202-239`):
+- PowerChat side effects that arrived on Live's webhook (chat lines, alerts, goals) stop at the re-point.
+- Displays that read Live's `transactions` table freeze.
+- Renewal notices stop.
+- The SPA sends no `Idempotency-Key` yet.
+
+Read the full list before choosing the day. VIP checkout stays closed until this cutover.
+
+## F. RTMP ingest → OpenRe.Stream: BLOCKED on the owner (DNS, port 1936, broadcaster windows)
+
+OpenRe is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are
+`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The database
+is `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsal
+with an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`
+reached Events, and the key never appeared in logs.
+
+Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`
+defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves
+(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRe `README.md:207-209`).
+
+**Once, before the first slot** (OpenRe `README.md:188-194`):
+
+| Step | State |
+|---|---|
+| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |
+| 2. Deploy OpenRe; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |
+| — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** |
+| 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. |
+| 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |
+| 5. Rehearsal with a test account from OBS to `rtmp://ingest.openre.stream:1936/live`. Deploy Live and `deploy.sh api` during the broadcast, and check that nothing drops. | Needs the DNS and port first |
+
+**Per slot** (`README.md:196-203`; `migrate-from-live.js --checklist` prints the same list):
+1. Agree a window with the broadcaster. The slot must be offline.
+2. Run `node scripts/migrate-from-live.js --live-db <fresh snapshot> --apply --slots <id>`. Review held
+ destinations on openre.stream.
+3. `PUT /api/admin/openre/managed/<id>/ingest-authority {"authority":"openre"}` on Live. From then on,
+ Live refuses the old key and has rotated its own copy (`server/openre/authority.js:103-146`).
+4. The broadcaster presses **Regenerate stream key** on Go Live (it is shown once) and sets OBS to
+ `rtmp://ingest.openre.stream:1936/live`.
+5. Rotate the broadcaster's personal Live key (`users.stream_key`). ADR-009 requires every pre-cutover
+ key to be rotated.
+6. Test a broadcast. Watch `deploy.sh status`, `/api/admin/openre/status` and output health.
+
+**Rollback:**
+- Per slot: `{"authority":"live"}`. The broadcaster regenerates the key on Live and points OBS back at the
+ Live RTMP URL.
+- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,
+ `server/openre/authority.js:47-49`).
+- Live's ingest code stays until the last protocol has run on OpenRe for two weeks (ADR-009 §Rollback).
+- Port 1935 moves to OpenRe (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening
+ (`README.md:139-145`).
+
+## G. Retiring the shims: NOT STARTED
+
+Every remaining shim has an owner, a removal condition, a date and a rollback lever in
+[compatibility-register.md](compatibility-register.md). The ones on the critical path:
+
+- **The shared internal key** (C-50–C-58), per Network `docs/retirement.md`:
+ 1. Move each caller to a service token by 2026-10-09.
+ 2. Remove each key path after 14 days with no legacy use. Check with the `principal_usage` query in
+ that doc.
+ 3. Delete `INTERNAL_API_KEY` from every env file by 2026-11-06.
+- **The hard-coded `openvibe-internal-2026` secret** in Live cosmetics (C-59): remove it by 2026-10-09.
+- **Events v1 signatures** (C-60): stop them after every consumer is deployed with `requireV2`.
+- **Chat's Live mirror** (C-01–C-03), and Live's frozen legacy tables and their readers (C-73): target
+ 2026-11-06.
+
+Before removing a shim, take `sudo ovhost backup <service>` for any service whose data it touches. Update
+the register row in the same change.
new file mode 100644
new file mode 100644
@@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
+# Storage state (roadmap Wave 22)
+
+This page covers two Wave 22 exit criteria:
+- "No service still queries SQLite for canonical data unless that is its documented and reviewed state."
+- "Each service's /ready tells the truth about its actual backing store."
+
+It has one row per deployed service. Each row gives the service's backing store, whether that store is
+canonical, its status under ADR-007, what its readiness handler actually checks (read from the handler,
+not from docs), and its backup and drill status from [restore-drills.md](restore-drills.md).
+
+ADR-007 (`OpenVibe.Contracts/docs/adr/ADR-007-data-ownership.md`) decides:
+- "SQLite remains the documented store per service while the platform is single-host; each service's
+ `/ready` reports its actual store."
+- A service moves to PostgreSQL individually when it needs concurrent writers across processes, a second
+ host, or sizes SQLite handles poorly.
+- Money paths use SERIALIZABLE transactions and row locks when they move.
+
+Every service below runs on SQLite (better-sqlite3), with one database per service. No repository has a
+PostgreSQL driver or config. **This page is the Wave 22 review of that state.**
+
+Sources:
+- Each repository's `server/config.js`, `.env.example` and `deploy/systemd/*.service`.
+- The ovhost inventory example [`host.example.json`](../host.example.json), which lists production paths.
+- The operator notes of 2026-09-23, for what is deployed.
+- Live was read at `~/orca/workspaces/OpenVibe.Live/seadragon` (`ae07553`).
+
+`openvibe-shared/ready` (v1.3.0, `node_modules/openvibe-shared/ready.js:43-124`) behaves like this:
+- It has no checks of its own.
+- A service passes named checks, and each check is required unless it is marked optional.
+- It answers 503 only when a required check fails.
+- A failed optional check gives 200 with `status: "degraded"`.
+- The default timeout is 2 s.
+
+## Findings
+
+1. **Readiness that does not check the store.**
+ - **Games** has no `/api/ready`. `/healthz` returns `{ ok: true, tick }` without touching `world.db`
+ (`apps/server/src/net/httpServer.ts:596-600`).
+ - **Chat** `/ready` is 200 as soon as `ctx.stats.lastSyncAt` is set (`server/app.js:77-86`). That
+ timestamp is set even when every Live sync step fails, because `tick()` and `sync()` catch each
+ step's error and set it anyway (`server/live-context.js:665-683`). The only database access is the
+ `mirror.pending` count (`SELECT COUNT(*) FROM live_mirror_outbox`, `server/bridge/live-mirror.js:34`,
+ production runs `LIVE_MIRROR=1`). A broken `chat.db` would therefore surface as a thrown 500, not as a
+ named check.
+2. **Readiness that checks the main store but not every store.**
+ - **Live** runs `SELECT 1` on `live.db`. It does not check `data/analytics.db` or `data/rs-companion.db`.
+ - The **Tools** satellites maps, text, yt and food only check `analytics.db`, and only as an optional
+ check. By design, that is their only database, and the tools work without it.
+ - The **Tools** gateway reports each satellite as an optional upstream. So a failed `jobs.db` on img,
+ audio or docs makes the gateway *degraded* (still 200), while that satellite's own `/api/ready`
+ answers 503.
+3. **ovhost's post-deploy readiness URL is not the truthful endpoint for four services.**
+ `host.example.json` polls:
+ - Network `/api/health`, which is static (`server/index.js:392-394`), although Network's `/api/ready`
+ checks the database and the signing key;
+ - Media `/healthz`, which is liveness only, although Media's `/api/ready` checks the database and
+ storage;
+ - Tools gateway `/api/health`;
+ - Games `/healthz`.
+
+ The inventory should point at `/api/ready` where one exists. Games has none.
+4. **Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and
+ OpenRe.Stream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron
+ and timers.
+5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.
+ The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,
+ `apps/_shared/jobs/index.js:77`) are missing. Tools W11 jobs have been in production since
+ 2026-09-23.
+6. **SQLite reads outside the documented state.** These go against exit criterion 1, and each has an entry
+ in [compatibility-register.md](compatibility-register.md):
+ - **Live reads its frozen legacy `vods`/`pastes` tables for live pages**, although Media and Community
+ hold the canonical rows (C-73):
+ - `server/ai/chat-ai-routes.js:337`
+ - `server/admin/routes.js:91-92`
+ - `server/arena/arena-service.js:436`
+ - `server/streaming/routes.js:1547`
+ - **Network may open Live's `live.db`** read-only at boot (`syncLinkedLiveRoles`,
+ `server/db/database.js:28-99`), which ADR-007 forbids (C-58). None of its default paths is Live's
+ production path. Whether `OPENVIBELIVE_DB_PATH` is set is unknown: check `/etc/openvibe/network.env`.
+ - **Chat reads Live's files** at `/opt/openvibe.live/data/sounds`. These are files, not a database
+ (`deploy/systemd/openvibe-chat.service:43`).
+7. **Canonical money is still in Live.** Billing's `billing.db` is a shadow (Billing
+ `STATUS.json: "authoritative": false`) until the cutover in [cutover-runbook.md](cutover-runbook.md)
+ step B.
+
+## Per service
+
+"Drill" is the latest result in [restore-drills.md](restore-drills.md), all 2026-09-23. "Deployed" is from
+the operator notes unless cited. Where a repo's `STATUS.json` disagrees, the operator notes and the drill
+log win.
+
+| Service | Backing store (production path; env, default) | Canonical? | ADR-007 status | Readiness checks (handler) | Store checked? | Backup / drill |
+|---|---|---|---|---|---|---|
+| **Network** | `/opt/openvibe.network/data/network.db`; `DB_PATH`, default `./data/network.db` (`server/config.js:55`). Analytics lives in the same file (`README.md:306-307`). | Yes: identity, OAuth clients, subjects, OpenCoins wallets, themes, notifications, analytics | SQLite, the reviewed single-host state. `docs/platform-plan.md:74,111` defers PostgreSQL "until a room needs it". | `/api/ready` (`server/observability.js:58-96`). Required: `db` (`SELECT COUNT(*) FROM oauth_clients`), `signing_key` in production (signs and verifies an RS256 probe; fails on the HS256 dev key). Optional: `registry_poll`, `discord_bot`. | **Yes** | Drill passed 05:22. ovhost's deploy readiness polls the static `/api/health` (finding 3). |
+| **Live** | `/opt/openvibe.live/data/live.db` (legacy in-place layout, "current production", Live `docs/deploy.md:31`); `DB_PATH`, default `./data/live.db` (`server/config.js:228`). Also `data/rs-companion.db`, and `data/analytics.db`, whose path is hard-coded with no env (`server/index.js:361-363`). | Yes: streams, channels, follows, Live accounts, restreams, **money** (`BILLING_AUTHORITY` unset = `live`), AI state (`vod_ai_state`/`clip_ai_state`), analytics. **Not canonical:** the 12 chat mirror tables (Chat is, since 02:03 UTC) and the frozen `vods`/`clips`/`pastes` (Media and Community are). | SQLite, the reviewed state. No PostgreSQL plan. Money moves to Billing, not to PostgreSQL. | `/api/ready` (`server/index.js:748-756`, `server/web/observability.js:79-106`). Required: `boot`, `db` (`SELECT 1` on `live.db`). Optional: `sfu`, `media` (`MEDIA_URL/healthz`, cached 30 s), `network_key` (PEM loaded, no call). | **Yes** for `live.db`; not `analytics.db` or `rs-companion.db` | `ovhost backup` covers `live.db` and `rs-companion.db`. `analytics.db` is left out on purpose (`host.example.json` `_databasesNote`). Drill **unsupported**: `analytics.db` is opened relative to the checkout, RTMP always binds, and restreams, relays and AI jobs resume from the copy. |
+| **Media** | `/opt/openvibe.media/data/media.db`; `DB_PATH`, default `./data/media.db` (`server/config.js:24`). Local `VOD_PATH`, `CLIPS_PATH`, `PASTES_PATH`, `THUMBNAILS_PATH`, `FILES_PATH`, `OBJECTS_PATH` (`config.js:27-58`), plus B2 and R2 object storage. | Yes: media objects, VODs, clips, files, thumbnails, avatars. Paste writes are frozen (Community is canonical). Media still serves paste screenshot bytes. | SQLite, the reviewed state. No PostgreSQL plan. | `/api/ready` (`server/observability.js:72-107`). Required: `db` (`SELECT COUNT(*) FROM apps`), and `storage_{vods,clips,pastes,thumbnails,files,objects}` (writes and unlinks a probe). Optional: `network_jwks`, `remote_b2`/`remote_r2` (HeadBucket, cached 60 s), `events_outbox` (>1000 pending). Also reports `recordings_in_progress`. | **Yes** (database and disk; object storage optional) | `ovhost backup` covers `media.db`. Drill **unsupported**: webhooks to Live come from the `apps` table, and ffmpeg jobs run against production paths. ovhost's deploy readiness polls `/healthz` (finding 3). |
+| **Tools** | The gateway has no database. Each satellite has `apps/<app>/data/analytics.db` (relative to `/opt/openvibe.tools`). img, audio and docs also have `data/jobs.db` (`apps/_shared/jobs/index.js:77`), `data/uploads` and `data/output`; yt has `data/downloads`. Job results go to Media when `TOOLS_JOB_RESULTS=media` (default `local`, `apps/_shared/jobs/index.js:32-36`). | Yes: jobs (img, audio, docs) and per-app analytics | SQLite, the reviewed state. One file per process, so ADR-007's "concurrent writers across processes" trigger does not apply. | `/api/ready` on every unit (`apps/_shared/observe.js:71-162`). Gateway: `catalog` required; `network_key`, `service_directory`, `community`, and each `satellite_<name>` optional (`apps/gateway/server/index.js:60-72`). img, docs, audio: `jobs_db` (`SELECT COUNT(*) FROM tool_jobs`), `job_runtime` and data directories required. yt: `downloads_dir`. food: `maps` upstream. maps, text: none required (`analytics_db` optional). | **Partly**: `jobs.db` yes on its satellites; analytics optional by design; the gateway reports satellites as optional (finding 2) | `ovhost backup` covers the seven `analytics.db` files only; **`jobs.db` is missing** (finding 5). Drill **unsupported**: eight units, relative `analytics.db` paths, job runtimes re-queue at start. |
+| **Games** | `/opt/openvibe.games/data/world.db`; `DB_PATH`, default `data/world.db` (`apps/server/src/config.ts:79`). No `.env.example`. | Yes: world, accounts, inventory | SQLite, the reviewed state. No PostgreSQL plan. | **No `/api/ready`.** `/healthz` returns `{ ok: true, tick }` (`apps/server/src/net/httpServer.ts:596-600`). | **No** | `ovhost backup` covers `world.db`. Drill **unsupported** (runtime side effects not reviewed). Games is `managed: false` in the inventory. |
+| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. |
+| **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. |
+| **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback, `server/app.js:77-86`, not openvibe-shared). **503 only while `lastSyncAt` is unset.** It reports `live.failures`/`last_error`, `mirror.pending` and `events.enabled`, and ignores them for the status. | **No** (finding 1) | Drill passed 05:22. |
+| **OpenRe.Stream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
+| **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. |
+| **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. |
+| **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |
+| **AI** | `/var/lib/openvibe-ai/ai.db` (`deploy/systemd/openvibe-ai.service:30-31`); `AI_DB_PATH`, default `./data/ai.db` (`server/config.js:81`). | Yes: workflows, runs, providers, quotas, usage (Live keeps its AI product state, AI `docs/migration.md:30-35`) | SQLite, the reviewed state. | `/api/ready` (`server/app.js:26-33`, own handler). Required: `db` (`SELECT 1`), `key`, `workflows` (at least one registered). | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
+| **Search** | `/var/lib/openvibe-search/search.db` (`deploy/systemd/openvibe-search.service:28`); `SEARCH_DB_PATH` (`server/config.js:49`). | **No**: a derived FTS5 index, rebuildable from its owners (sources, wiki, blog, news, reviews) | SQLite FTS5. `docs/adr-engine.md` plans PostgreSQL FTS "once a PostgreSQL instance exists" (`server/engine/fts5.js:16` keeps the engine behind an interface). | `/api/ready` (`server/app.js:30-43`): `SELECT 1` and key loaded. | **Yes** | Drill passed 05:23. |
+| **Sources** | `/var/lib/openvibe-sources/sources.db` (`deploy/systemd/openvibe-sources.service:29`); `SOURCES_DB_PATH` (`server/config.js:41`). | Yes: source registry, items, revisions, fetch runs | SQLite, the reviewed state. | `/api/ready` (`server/app.js:25+`): `SELECT 1`, key, and the worker when enabled. | **Yes** | Drill passed 05:23. |
+| **Wiki** | `/var/lib/openvibe-wiki/wiki.db` (`deploy/systemd/openvibe-wiki.service:29`); `WIKI_DB_PATH` (`server/config.js:27`). | Yes: spaces, pages, revisions | SQLite, the reviewed state. | `/api/ready` (`server/app.js:63-75`). Required: `db` (`wiki_pages` exists). Optional: jwks, events_relay, community, sources, media. | **Yes** | Drill passed 05:23. |
+| **Blog** | `/var/lib/openvibe-blog/blog.db` (`deploy/systemd/openvibe-blog.service:28`); `BLOG_DB_PATH` (`server/config.js:32`). | Yes: blogs, posts, revisions | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:19-58`). Required: `db` (charter tables exist). Optional: jwks, events_relay, scheduler. | **Yes** | Drill passed 05:23. |
+| **News** | `/var/lib/openvibe-news/news.db` (`deploy/systemd/openvibe-news.service:29`); `NEWS_DB_PATH` (`server/config.js:32`). | Yes: topics, stories, source items | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:21-65`). Required: `db` (charter tables). Optional: jwks, events_relay, events_webhook, sources_pull. | **Yes** | Drill passed 05:23. |
+| **Reviews** | `/var/lib/openvibe-reviews/reviews.db` (`deploy/systemd/openvibe-reviews.service:30`); `REVIEWS_DB_PATH` (`server/config.js:28`). | Yes: entities, signals, summaries | SQLite, the reviewed state. | `/api/ready` (`server/app.js:82-99`). Required: `db` (three review tables). Optional: jwks, sources, events relay and webhook, community, editors. | **Yes** | Drill passed 05:23. |
+| **Deals** | `/var/lib/openvibe-deals/deals.db` (`deploy/systemd/openvibe-deals.service:28`); `DEALS_DB_PATH` (`server/config.js:34`). | Yes: offers, products, votes | SQLite, the reviewed state. | `/api/ready` (`server/observability.js`). Required: `db` (tables exist, `:26-28`). Optional: jwks, events_relay, sources_import, worker, abuse_keys. | **Yes** | Drill passed 05:23. |
+| **Coupons** | `/var/lib/openvibe-coupons/coupons.db` (`deploy/systemd/openvibe-coupons.service:29`); `COUPONS_DB_PATH` (`server/config.js:32`). | Yes: coupons, merchants, domains | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:24-26`). Required: `db`. Optional: jwks, events_relay, sweep, sources_import, reporter_key. | **Yes** | Drill passed 05:23. |
+| **Trade** | `/var/lib/openvibe-trade/trade.db` (`deploy/systemd/openvibe-trade.service:29`); `TRADE_DB_PATH` (`server/config.js:32`). | Yes: instruments, observations, source documents (informational, ADR-025) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:23-25`, mounted `server/app.js:121`). Required: `db`. Optional: jwks, events_relay, sources_sync, freshness. | **Yes** | Drill passed 05:23. |
+| **Codes** | `/var/lib/openvibe-codes/codes.db` (`deploy/systemd/openvibe-codes.service:29`); `CODES_DB_PATH` (`server/config.js:37`). | Yes: manifests, releases, trust | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:24-32`). Required: `db`, `docs`. Optional: jwks, network, oauth_client, events_relay. | **Yes** | Drill passed 05:23. |
+| **Host** | Stage B: `/var/lib/openvibe-host-api/host.db` and `HOST_STORAGE_DIR` blobs (`deploy/systemd/openvibe-host.service:37-38`; `server/config.js:47-49`). Stage A (`ovhost`) keeps JSONL logs in `/var/lib/openvibe-host` and has no database. | Yes: tenant projects, sites, deploys, domains | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:22-29`). Required: `db`, `storage` (`blobs.writable()`, `server/storage.js:98-102`). Optional: jwks, events_relay, domain_checks. | **Yes** (database and blob directory) | Drill passed 05:23 (`host.db`, counts only). `README.md` and `STATUS.json` say Stage B is **not deployed**, while the drill counted a production `host.db`. Whether `openvibe-host.service` is running is unknown: check `systemctl status openvibe-host`. |
+
+## Deployed-state notes
+
+- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,
+ Codes, AI and OpenRe's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:
+ Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whether
+ they are public is unknown: check nginx `sites-enabled`. Sites still has placeholders for
+ openvibe.deals, .coupons and .trade.
+- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRe
+ and AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,
+ Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.
+
+## Re-checking
+
+```
+sudo ovhost status <service> # unit, port, readiness as ovhost sees it
+curl -s 127.0.0.1:<port>/api/ready | jq .checks # the named checks and their state
+sudo ovhost backup <service> && sudo ovhost drill <service>
+```
+
+To re-verify a row, read the handler cited in the row. A readiness change is a code change in that
+service, and this page is updated in the same week.