openvibers/openvibe.contracts Commits
v0.34.0: ADR amendments dated 2026-09-24 (roadmap §28.3, W22): ADR-003 delegated authorization (who may name the acting person, intersection of app grants, approved scopes and the person's own rights, staff power and money never delegated, 5-minute unrefreshable tokens, audit names app and person) and the approval UX in force (PKCE, no prompt=none for apps, registered name and redirect host, narrow-only exchange) plus what Network still owes (capability list on the chooser, stored authorizations, connected apps with revoke); ADR-006 four placement classes (canonical, hot cache, asset origin, local scratch) mapped to today's providers, local as the first-class dev provider, infrequent-access tier deferred with the measured 2,839 objects / 481 GB; ADR-007 PgBouncer transaction pooling, read replicas only for lag-tolerant reads, SERIALIZABLE plus ordered row locks and 40001 retries on money paths, Redis never authoritative; ADR-012 roadmap ledger names mapped to Billing types and legs (platform fee, creator earning, payout hold/release, reversals), OpenCoins stays in Network (supersedes W8 "OpenCoins wallet becomes a Billing client"), payouts/refunds/plans substituted by cashouts, reversal transactions and VIP plans; ADR-013 package signing and review named open owner decisions, mod monetization only through Billing and VIP primitives, manifest 1.1.0 fields; ADR-021 extraction decided from measured volume (Live 1.78M raw events / 30 days, 58k a day, 502 MB): no analytics service, analytics stays per service through openvibe-shared/analytics, the schema stays in Shared, revisit triggers; ADR-022 Wave 12 revisit: the decision stands, next trigger named
Changes
@@@ -1,6 +1,6 @@# ADR-003: Service/app principal authentication and capability grants--**Status:** Accepted, implemented 2026-09-22++**Status:** Accepted, implemented 2026-09-22. Amended 2026-09-24: delegated authorization semantics and approval UX (roadmap §28.3).## Context and current evidence@@@ -29,3 +29,78 @@ Remove a grant (new tokens stop carrying it) or stop sending tokens (callers fal## Acceptance tests`OpenVibe.Network/test/principals.test.js`, `modules.test.js`, Contracts service-auth tests (forged, expired, wrong audience, alg none, tampered, ungranted, namespace, owner), Media `service-token.test.js`. Production smoke: tokens issued, cross-app and cross-namespace calls refused.++++## Amendment 2026-09-24: delegated authorization++++Roadmap §28.3 carries "delegated authorization semantics and approval UX" into this ADR. Delegation++already runs in production: Network issues app tokens with `on_behalf_of` (ADR-014,++`server/developer/tokens.js`), and Wiki, Blog, News, Reviews and Trade honour it++(`server/auth/viewer.js` in each, deployed 2026-09-23). This section records the rules they follow,++so every other receiver implements the same ones.++++### Who can act for whom++++| Principal | May name the acting person | How |++|---|---|---|++| First-party service (`svc:<id>`, `actor_type: service`) | any person | `X-OV-Subject: usr_…` on the request. The service vouches for its own signed-in user. |++| Developer app (`app:app_…`) | only the person who authorized it | `on_behalf_of` in the token. An `X-OV-Subject` naming anyone else gets 403 `subject.not_delegated`. |++| Mod (`mod:mod_…`, ADR-013) | only the person who installed or authorized it | the same rule as an app |++| Any principal, `client_credentials` token | nobody | no `on_behalf_of`. The call is the principal's own, never a person's. |++++### What a delegated call may do++++- **Intersection, never union.** The effective permission of a delegated call is the capabilities++ in the token (the app's approved grants ∩ its project allowance ∩ what is still grantable, for that++ audience), narrowed to the scopes the person approved, and then limited again by what that person++ may do on the receiver. A delegated call can never do more than the person could do, nor more than++ the app was granted.++- **Staff power is never delegated.** A receiver treats a delegated caller as a plain user: staff++ roles and staff capabilities (`manifests/policy/staff-roles.json`, v0.34.0) are never granted++ through an app or mod token. Moderation needs a first-party staff surface.++- **Money is not delegable by default.** A developer app can be granted only `active`++ capabilities whose manifest says `visibility: public` or `partner` (partner only when staff put it++ in one project's allowance by hand; Network `server/developer/policy.js`). The only public++ money-adjacent ones are the read-only++ `vip.plan.list` and `vip.perk.list`; no capability that moves money, credit or payouts is public,++ and making one public needs an ADR-012 amendment.++- **Short and unrefreshable.** App tokens last 5 minutes and have no refresh token. Revoking the++ app, its credential or a grant stops new tokens at once, and issued tokens end within 5 minutes.++- **Sandbox stays sandbox.** A receiver refuses `env: sandbox` tokens (401 `token.sandbox_refused`)++ unless it opted in. Only members of the app's project may authorize a sandbox app.++- **Audit names both.** A receiver's audit rows and events record the principal (`app:…`) as the++ actor and the person as `on_behalf_of`. They never record it as the person acting alone.++++### Approval UX (Network)++++In force today (Network `server/auth/oauth-routes.js`, `public/login.html`):++++1. `/oauth/authorize` for an app requires an exact registered `redirect_uri` and PKCE S256.++ `prompt=none` is refused with `interaction_required`, so a third-party app never gets a code++ without the person choosing to continue.++2. The account chooser shows the app's registered name, marked "(third-party app)", and the host++ the code will be sent to (`/oauth/client-info`). The name is looked up by client id, never taken++ from the URL.++3. The code is single use, lasts 5 minutes, and records the approved scope. The token exchange may++ narrow that scope but never widen it (400 `invalid_scope`).++++Required next, owned by Network (not built yet):++++4. **The chooser lists the requested capabilities** in plain words (each capability manifest's++ `description`), before the person continues. A capability the app was not granted is not shown++ and not issued.++5. **A stored authorization** per (person, app), holding the approved scopes and a date. A++ repeated request within those scopes may skip the chooser. A request for more always shows it.++6. **"Connected apps" on my.openvibe.network**, listing each authorization with its scopes and last++ use, and a revoke action. Revoking stops new tokens for that person and app at once.++7. Revoking a person's authorization, and every Network ban, also ends `on_behalf_of` issuance for++ them (the ban check at exchange exists today).++++### Tests++++- Receivers: an app token naming another person in `X-OV-Subject` gets 403++ `subject.not_delegated`. A sandbox token gets 401. A first-party service may name the person.++ (Wiki, Blog, News, Reviews and Trade have these tests.)++- Network: `prompt=none` for an app returns `interaction_required`. A widened scope at exchange is++ `invalid_scope`. A reused or expired code is `invalid_grant`.++- Still to add, with items 4 to 6: the chooser lists exactly the issued capabilities, and a revoked++ authorization gets no new token.@@@ -1,6 +1,6 @@# ADR-006: Media object, location and namespace model--**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4)++**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4). Amended 2026-09-24: four placement classes, the local/dev provider, the infrequent-access tier deferred (roadmap §28.3 m1).## Context and current evidence@@@ -30,3 +30,32 @@ The object layer is additive; old routes keep working throughout.## Acceptance testsNon-Live app uploads/reads with only a capability grant; deliberate replica loss is detected; private objects cannot be fetched without a grant; invariant violations are reported.++++## Amendment 2026-09-24: placement classes, local provider, infrequent access++++Roadmap §28.3 m1 and §28.4 item 6. A copy's **placement class** says why it exists and what may++remove it. The provider says where it is. There are four classes:++++| Class | Purpose | Where today | Rules |++|---|---|---|---|++| **canonical** | The durable copy of record | B2 (`provider b2`, `storage_class cold`); the local disk for an object not yet offloaded | Never removed while the object exists or is held. Demotion never touches it. Deletion only after holds clear. |++| **hot cache** | A promoted copy for heavy playback | R2 (`provider r2`, `storage_class cache`) | Only analytics-gated promotion creates it (the Decision above; roadmap §28.2 M2). Demotion removes only this copy. It is never the only copy. |++| **asset origin** | Small public assets served through the CDN: avatars, thumbnails, previews, sprites, paste screenshots and similar | Media's own host disk behind Cloudflare (`provider local`, `storage_class hot`) | May be the canonical copy for small assets. It is served with long-lived, content-hashed or revisioned URLs. A separate cheap origin VPS is used only when disk or egress measurements call for one. |++| **local scratch** | Development and active processing: recordings in progress, remux and cut work, job temporaries | the host's local disk | Never a durable copy and never a location of record. A job cleans up after itself, and a restart may discard scratch. |++++`media_locations.storage_class` keeps its values (`hot`, `cold`, `cache`). The class is derived from++the provider and the object kind until Media adds a `placement` column. That column is additive,++and Media records it with its lifecycle work.++++**Local/dev provider.** `local` is a first-class provider. With no B2 or R2 credentials, every++Media path degrades to it: uploads, playback, thumbnails, jobs and the objects API++(`providerConfigured('b2')` and `('r2')` gates in `server/vod/vod-storage.js`). Development and tests++therefore need no cloud account. Still to do in Media: one provider interface (`put`, `get`,++`head`, `delete`, `presign`, `list`) with `local`, `b2` and `r2` implementations behind it, instead of++branches inside `vod-storage.js`. The S3 facade (Alternatives above) would sit on that interface.++++**Infrequent-access tier: deferred.** No infrequent-access or archive storage class (B2 has none; S3++IA and Glacier-style tiers are not used) is adopted at current scale. On 2026-09-24, production held 2,839++ready objects totalling 481 GB, which fits B2 at its single price. Revisit when retrieval-priced storage would save more than it costs++in retrieval fees and restore latency, measured from `media_locations` sizes and access. That++change is a new amendment here.@@@ -1,6 +1,6 @@# ADR-007: Logical data ownership and database technology--**Status:** Accepted 2026-09-22++**Status:** Accepted 2026-09-22. Amended 2026-09-24: PostgreSQL runtime posture (pooling, replicas, money-path isolation, Redis non-authoritative; roadmap §28.3 m10).## Context and current evidence@@@ -28,3 +28,46 @@ n/a## Acceptance testsWave 0 baseline shows no service reading another's database; `/ready` truthfulness checked per service in Wave 22.++++## Amendment 2026-09-24: PostgreSQL runtime posture++++Roadmap §28.3 m10 and §28.4 item 15. No service runs on PostgreSQL today. Every service is SQLite on++one host (the Decision above still holds). These rules bind the first service that moves, and every++one after it.++++1. **Canonical truth, pooled.** A service's PostgreSQL database is its canonical relational truth.++ Application processes connect through **PgBouncer in transaction-pooling mode**, never with one++ long-lived connection per process. Code that moves must therefore work without session state:++ no session-level `SET`, advisory locks held across transactions, `LISTEN`/`NOTIFY` or++ session-scoped prepared statements through the pooler. Migrations and those few session++ features use a direct connection with its own role.++2. **Read replicas serve reads.** Replicas may serve reads that tolerate lag: public pages, lists,++ search feeds and read models. A read that decides a write, checks a balance, an entitlement, a++ grant or a ban, or answers read-your-own-write goes to the primary. A service that routes reads++ to a replica says in `/ready` which reads may lag and by how much.++3. **Money paths are SERIALIZABLE with row locks.** Every transaction that moves money, credit or a++ payout (the ADR-012 MONEY and CREDIT classes: Billing's ledger, and anything Tips or VIP writes++ that changes a balance) runs at `SERIALIZABLE` isolation. It locks the account rows it debits++ (`SELECT … FOR UPDATE`, in a fixed order by account id so two transfers cannot deadlock), and it++ retries a serialization failure (SQLSTATE 40001) a bounded number of times with the same++ idempotency key. A balance check and the entry it guards are always in one transaction.++ Loyalty ledgers (OpenCoins, channel points) follow the same rule when they move.++4. **Redis is never authoritative** for money, credit, entitlements, account or identity state,++ grants or bans. It may hold caches, rate-limit counters, presence and fan-out that can be lost and++ rebuilt from the owner's database. Losing Redis must never lose a write or change an answer about++ money or accounts. No service uses Redis today; Chat's and Live's history stores keep a note that++ a ring buffer or Redis may replace the in-process cache later, under this rule.++5. **One write role per service**, as the Decision says. A shared cluster isolates ownership by++ database and role (roadmap §17.1). Cross-domain joins happen in read models or analytics.++++While on SQLite, the equivalent money-path rule is already in force: Billing posts every journal++transaction inside one `better-sqlite3` transaction together with its funds check++(`server/ledger.js`). SQLite's single writer serializes those transactions.++++### Tests (when the first service moves)++++- The service's suite passes against PostgreSQL behind PgBouncer in transaction mode.++- A concurrent-debit test: two transfers race for the same balance, exactly one succeeds, and the++ journal still sums to zero.++- A lag test: a replica read is never used for a balance, entitlement or grant decision.++- Redis flushed mid-run changes no money or account answer.@@@ -1,6 +1,6 @@# ADR-012: Billing vs loyalty, credits and currency classification--**Status:** Accepted 2026-09-22. Gates Wave 8 (OpenVibe.Billing), Wave 9 (Tips) and Wave 10 (VIP).++**Status:** Accepted 2026-09-22. Gates Wave 8 (OpenVibe.Billing), Wave 9 (Tips) and Wave 10 (VIP). Amended 2026-09-24: ledger transaction types mapped to the roadmap's names; OpenCoins stays in Network; the payouts/refunds/plans table substitution.## Context and current evidence@@@ -70,3 +70,52 @@ Until cutover, Live remains authoritative and Billing only shadows (imports and- Creator payables reconcile exactly to the ledger; imported opening balances equal the Live columns at cutover, with every adjustment listed.- The freeze switch refuses writes and serves reads.- Entitlements are queryable with Live offline.++++## Amendment 2026-09-24: transaction types, OpenCoins, table substitution++++### Ledger transaction types (roadmap §28.3, §28.4 item 23)++++The roadmap asks for named types: platform fee, creator earning, payout hold and release, and the++compensating reversals. Billing does not add them as new `transactions.type` values. Each of them++is a **leg** (an entry on an account kind) of an existing, named type. The mapping is binding, and++reports and events use these names:++++| Roadmap name | Billing type(s) | Leg (account kind) | Where |++|---|---|---|---|++| **Platform fee** (purchase spread, retained subscription share, site-route fee) | `purchase`, `subscription`, site-routed tips from receipts | credit to `platform_revenue` (cents or bits). The provider's own fee is kept as `fee_cents` in metadata, never as revenue. | `server/ops/common.js` receiptEntries, `ops/subscriptions.js` |++| **Creator earning** | `donation` (tip, donation, paid interaction), `subscription` (the creator's share), `import` / `subscription_share` for imported Live history | credit to `creator_payable:<subject>` | `ops/transfers.js`, `ops/subscriptions.js`, `importer/live.js` |++| **Payout hold** | `cashout_request` | `creator_payable` → `payouts_pending`, with `escrow_until`. Billing refuses approval during escrow (rule 10). | `ops/cashouts.js` request |++| **Payout release** (paid out) | `cashout_paid` | `payouts_pending` → `provider_clearing:<provider>_payouts` (bits to cents through `fx_conversion`), with the provider's payout reference required | `ops/cashouts.js` approve |++| **Payout release** (returned) | `cashout_denied` | `payouts_pending` → `creator_payable`, and `reverses_txn` → the request | `ops/cashouts.js` deny |++| **Compensating reversals** | `refund`, `chargeback` (with `reverses_txn` → the original); `refund` of a transfer (a media request that never played); `adjustment` (staff, with a reason) | back to `user_credit` or out through `refunds` / `chargeback_loss`. A creator's payable is never clawed back for credit already given away: the loss is booked and flagged for review. | `ops/reversals.js`, `ops/transfers.js` refund, `ops/admin.js` |++| (MONEY → CREDIT) | `recycle` | `creator_payable` → `user_credit` (rule 4) | `ops/cashouts.js` recycle |++++A new money movement gets a new `type` value only when no existing type and leg describes it.++Adding one is additive in Billing's CHECK and in `billing.transaction.settled@1`.++++### OpenCoins stays in Network (supersession)++++Roadmap Wave 8 (line 692) says "Network's OpenCoins wallet becomes a Billing client". **This ADR++supersedes that line.** OpenCoins are LOYALTY. Network owns them, with their own ledger, and++Billing does not hold, mirror or proxy them (see Decision, and Migration consequences: "OpenCoins++and channel points are **not** migrated into Billing in Wave 8"). A Billing loyalty ledger, if it++ever comes, is a separate step with its own ADR amendment, and it never shares accounts with MONEY++or CREDIT. The Network requirement ledger (D28) and the roadmap crosswalk should cite this++paragraph instead of listing the line as remaining.++++### Table substitution (roadmap §11.1.1)++++The roadmap's minimal entity list names twelve tables. Billing has nine of them under those names:++`accounts`, `ledger_entries`, `transactions`, `provider_events`, `payment_intents`, `subscriptions`,++`entitlements`, `reconciliation_runs` and `idempotency_keys`. It deliberately substitutes the other++three:++++| Roadmap entity | Billing's equivalent | Why |++|---|---|---|++| `payouts` | `cashouts` (`server/db.js`), with the `cashout_request` / `cashout_paid` / `cashout_denied` journal transactions | The product calls them cashouts. The row holds the payout method, escrow and provider payout reference. |++| `refunds` | reversal transactions (`type refund` or `chargeback`, `reverses_txn` → the original) plus the `refunds` and `chargeback_loss` accounts (`server/ops/reversals.js`) | A refund is a journal fact, not a separate mutable record. Partial and cumulative reversals are checked against the original. |++| `plans` | VIP's `vip_plans` (OpenVibe.VIP) | Plans and perks are VIP's (see the ENTITLEMENT row in the Decision). Billing holds only the entitlement truth. |++++Billing also has `external_receipts` (EXTERNAL-class PowerChat tips, recorded without liability)++and `account_balances` (a projection of the journal, checked by reconciliation). Thin `payouts` or++`refunds` views may be added for reporting. They are not required.@@@ -1,6 +1,6 @@# ADR-013: Mod manifest, trust and sandbox/resource model--**Status:** Accepted 2026-09-23. Gates Wave 12 (mods) and Wave 21 Stage C (sandboxed user code).++**Status:** Accepted 2026-09-23. Gates Wave 12 (mods) and Wave 21 Stage C (sandboxed user code). Amended 2026-09-24: signing and review are open owner decisions; mod monetization only through Billing; manifest 1.1.0 (read/write grants, billing hooks, dependencies).## Context and current evidence@@@ -32,3 +32,59 @@ Revoke the grants; the manifest schema stays.- A capability the mod was denied cannot be reached from its scripted runtime.- A revoked mod stops affecting the world.- Install, grant, use and revoke all appear in the audit log.++++## Amendment 2026-09-24: open decisions, monetization and manifest 1.1.0++++Roadmap §28.3 (m6 and "Mod signing and review") asks for three things this ADR left implicit.++++### Open owner decisions: package signing and review++++Neither is decided. Both stay **open decisions for the owner**, and nothing may fill them with a++silent default.++++1. **Package signing.** Who signs a mod release and with what key: the publisher, the platform at++ review time, or both. Which artifact the signature covers: the manifest, the pack or bundle, and++ the Media asset hashes. Where a runtime checks the signature. What an unsigned release may do.++2. **Review process.** Who reviews, against which checklist, and what a review changes. It may raise++ the trust tier. It never grants anything, because tiers stay metadata. Also open: how a published++ review is withdrawn, and whether the ADR's tier names (`unreviewed`, `reviewed`, `first-party`) or++ the roadmap §15 vocabulary are final (requirement ledger D32).++++Until the owner decides, installs stay as they are. Only staff install mods: Games++`POST /api/v1/mods` is staff-only, with the approved capability subset given at install. No++self-service publishing or install exists. Executable mods wait for Host Stage C in any case. The++decision will be recorded as a new amendment here, before any non-staff install path ships.++++### Monetization only through Billing++++A mod earns money only through Billing and marketplace primitives (ADR-012, ADR-025), never around++them:++++- A mod never takes payment, never sees payment details and never holds a balance. Prices, plans and++ entitlements live in Billing and VIP, not in the mod or its manifest.++- A mod may **check an entitlement** that the person holds (`billing.entitlement.check`,++ `vip.entitlement.check`) and may **send the person to a platform checkout**++ (`vip.membership.checkout`, Billing intents). Both need the capability in the install's approved++ grants, like any other call.++- Every paid unlock a mod gates is an ENTITLEMENT (ADR-012). Game state bought with money needs its++ own ADR (ADR-012 rule 7), and so does any creator share for a mod publisher.++- A marketplace for mods is out of scope until ADR-025 is revisited.++++### Manifest 1.1.0 (openvibe-contracts v0.34.0)++++`mods.mod-manifest@1` gains three optional fields, so every 1.0.0 manifest stays valid:++++- `permissions.readGrants` and `permissions.writeGrants` split module and Media namespace access into++ read-only and read-write, each as `{ modules?, mediaNamespaces? }`. A namespace that is only in++ `readGrants` is read-only. The older `permissions.modules` and `permissions.mediaNamespaces` still++ mean read and write.++- `billingHooks`: the entitlement checks and checkouts the mod uses, as `{ kind, key, description? }`.++ `kind` is `entitlement` or `checkout`, and `key` is an entitlement or plan key (`vip.plan:pln_…`).++ These declare intent for review and for the install screen. The capability still has to be++ granted.++- `dependencies`: other mods this release needs, as `{ id: mod_…, version: <semver range>,++ optional? }`. A runtime refuses to enable a mod whose required dependency is not installed, not++ enabled, or outside the range.++++Games validates against a local copy of the 1.0.0 schema (`apps/server/src/mods/manifestSchema.ts`).++It adopts 1.1.0 when it bumps its contracts pin.@@@ -1,6 +1,6 @@# ADR-021: Analytics ownership and privacy bounds--**Status:** Accepted 2026-09-23. Contract now; extraction decision at Wave 22.++**Status:** Accepted 2026-09-23. Contract now; extraction decision at Wave 22. Amended 2026-09-24: extraction decided (no analytics service; analytics stays per service through `openvibe-shared/analytics`).## Context and current evidence@@@ -31,3 +31,42 @@ Pruning is the only destructive step. Export the rollups before enabling it.- No raw analytics row older than 30 days.- No IP address or subject id in any analytics table.++++## Amendment 2026-09-24: the extraction decision++++Wave 22 asked whether to create an analytics service, "from measured volume". **Decision: no++analytics service.** Analytics stays per service, through the shared module++`openvibe-shared/analytics` (tracker, privacy, retention, schema, prune CLI; Shared ≥ 1.4.0).++The ADR-021 privacy rules above apply unchanged: no IP address, precise location or subject id in++raw analytics, and at most 30 days of raw events.++++**Measured volume** (production, read-only, 2026-09-24):++++- Live `analytics.db`, the largest: 1,776,181 raw events in its 30-day window (2026-08-25 to++ 2026-09-24), 58,247 in the last 24 hours, a 502 MB SQLite file. It was 1.7 GB before the++ ADR-021 prune and scrub.++- Network: raw analytics in its own database, pruned to 30 days (network.db 731 → 276 MB after the++ ADR-021 scrub and VACUUM).++- Tools: one analytics database per satellite, each smaller than Live's.++++One process per service writes this volume into SQLite without strain. No product needs raw events++joined across services: the cross-site rollup already lives in Tools and Network. An extra++service would add a deploy unit, a network hop on every page view and a second copy of the privacy++rules, and would buy nothing measured.++++**Where the contract lives.** The event schema is `openvibe-shared/analytics/event.v1.json`++(`docs/schemas/analytics-event.v1.json` in OpenVibe.Shared), versioned with the module that writes++it. It is not copied into this repository's catalog, because a second copy would drift. The Decision++above said "in OpenVibe.Contracts"; this amendment corrects that.++++**Revisit** (a new amendment here) when any of these is measured:++++- one service's raw events pass 1,000,000 a day, or its 30-day raw table passes 5 GB;++- a product needs raw events joined across services (funnels or attribution across sites), which++ rollups cannot answer;++- analytics writes show up in a service's latency or `/ready`;++- the platform moves to more than one host for a service that records analytics.++++**Consumers:** Live (`server/index.js`, `server/paths.js`), Network (`server/analytics/network.js`)++and the Tools satellites (`apps/*/server/index.js`) use the module. A new product that wants++analytics uses it too, with its own database and the nightly prune.@@@ -1,6 +1,6 @@# ADR-022: Moderation and policy console--**Status:** Accepted 2026-09-23. Revisit at Wave 12.++**Status:** Accepted 2026-09-23. Revisited at Wave 12 (2026-09-24): the decision stands, with no moderation service; next revisit at Wave 22 or when the trigger below is met.## Context and current evidence@@@ -33,3 +33,38 @@ The events are additive.- Every staff action in Live, Community and Chat appears in the audit log with actor and reason.- A service without the moderate capability cannot vouch for staff.++++## Revisit at Wave 12 (2026-09-24): outcome++++Wave 12 (Games and mods) was the named revisit point, because mods could have made moderation a++multi-product workflow. **Outcome: the decision stands.** Network stays the staff directory and++the moderation audit log, enforcement stays with each owner, and no moderation service is created.++++Evidence:++++- **No cross-product workflow exists.** Each moderated thing has one owner:++ - Chat moderates chat (channel-scoped bans, deletions; it emits `chat.moderation.action`).++ - Community moderates pastes, threads and comments (`community.*.moderate`).++ - Tips moderates interactions (`tips.interaction.moderated`).++ - Billing logs staff money actions (`billing.staff.action`).++ - Games installs, grants and revokes mods itself: installing is staff-only, audited in its own++ `mod_audit`.++++ No queue, appeal or case spans two of them.++- **Mods did not change that.** Games' mods are declarative packs installed by staff. There is no++ self-service publishing, so there is no review queue. Mod signing and review are open owner++ decisions (ADR-013 amendment of 2026-09-24).++- **What is still missing is inside this decision, not a reason to change it.**++ - Network does not consume `*.moderation.action` yet (requirement D05-R2), so the audit log has++ no entries.++ - Services still check raw roles. v0.34.0 adds the staff capability map++ (`manifests/policy/staff-roles.json`): `user < streamer < global_mod < admin < owner` and the++ `staff.*` capabilities each role holds. Network issues it as claims, and Live, Chat and++ Community replace their raw checks with it.++++**Trigger for the next revisit** (at Wave 22 at the latest): the first workflow that no single++owner can host, for example:++++- an appeal that crosses products;++- one report queue for Chat, Community and Media;++- a public mod marketplace with review.@@@ -6,17 +6,17 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each|---|---|---|| [ADR-001](ADR-001-canonical-subjects.md) | Canonical subject IDs and legacy identity mapping | Accepted, implemented 2026-09-22 || [ADR-002](ADR-002-contract-repository.md) | Contract repository and compatibility policy | Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+) |--| [ADR-003](ADR-003-service-principals.md) | Service/app principal authentication and capability grants | Accepted, implemented 2026-09-22 |++| [ADR-003](ADR-003-service-principals.md) | Service/app principal authentication and capability grants | Accepted, implemented 2026-09-22; amended 2026-09-24 (delegated authorization and approval UX) || [ADR-004](ADR-004-durable-events.md) | Durable event store, outbox/inbox and delivery semantics | Accepted 2026-09-22; implementation in progress (OpenVibe.Events) || [ADR-005](ADR-005-realtime.md) | Realtime topics, cursor/resume and whether OpenVibe.Realtime exists | Accepted 2026-09-22: realtime runs inside OpenVibe.Events |--| [ADR-006](ADR-006-media-objects.md) | Media object, location and namespace model | Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4) |--| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22 |++| [ADR-006](ADR-006-media-objects.md) | Media object, location and namespace model | Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4); amended 2026-09-24 (placement classes, local provider, infrequent access deferred) |++| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22; amended 2026-09-24 (PgBouncer, replicas, SERIALIZABLE money paths, Redis non-authoritative) || [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) || [ADR-009](ADR-009-live-openre-boundary.md) | Live versus OpenRe boundary | Accepted 2026-09-23; OpenRe deployed, no slot moved yet || [ADR-010](ADR-010-chat-boundary.md) | Chat versus Live/Community boundary | Accepted and executed 2026-09-23 (Chat serves Live's chat) || [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |--| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10 |--| [ADR-013](ADR-013-mod-manifest.md) | Mod manifest, trust and sandbox/resource model | Accepted 2026-09-23; gates Waves 12 and 21C |++| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10; amended 2026-09-24 (transaction-type mapping, OpenCoins stays in Network, table substitution) |++| [ADR-013](ADR-013-mod-manifest.md) | Mod manifest, trust and sandbox/resource model | Accepted 2026-09-23; gates Waves 12 and 21C; amended 2026-09-24 (signing and review open, monetization through Billing, manifest 1.1.0) || [ADR-014](ADR-014-developer-projects.md) | Developer project, tenant and quota model | Accepted 2026-09-23; gates Waves 20-21 || [ADR-015](ADR-015-ai-workflows.md) | AI workflow and publication ownership boundary | Accepted 2026-09-23; OpenVibe.AI deployed || [ADR-016](ADR-016-active-client-updates.md) | Active client update safety and mixed-version window | Accepted 2026-09-23; Track R |@@@ -24,8 +24,8 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each| [ADR-018](ADR-018-search-home.md) | Home of the search index and query contract | Accepted 2026-09-23: OpenVibe.Search, FTS first || [ADR-019](ADR-019-publishing-packages.md) | Home of the shared publishing packages | Accepted 2026-09-23: OpenVibe.Publishing, packages only || [ADR-020](ADR-020-notification-delivery.md) | Notification delivery ownership | Accepted 2026-09-23: Network authority, Events transport |--| [ADR-021](ADR-021-analytics.md) | Analytics ownership and privacy bounds | Accepted 2026-09-23: contract now, 30-day raw retention |--| [ADR-022](ADR-022-moderation-console.md) | Moderation and policy console | Accepted 2026-09-23: Network module + audit events |++| [ADR-021](ADR-021-analytics.md) | Analytics ownership and privacy bounds | Accepted 2026-09-23: contract now, 30-day raw retention; extraction decided 2026-09-24: no analytics service |++| [ADR-022](ADR-022-moderation-console.md) | Moderation and policy console | Accepted 2026-09-23: Network module + audit events; Wave 12 revisit 2026-09-24: stands || [ADR-023](ADR-023-client-surfaces.md) | First-party client surfaces | Accepted 2026-09-23: OpenVibe.Extensions at Wave 18 || [ADR-024](ADR-024-theme-authority.md) | Theme authority between Network and Shared | Accepted 2026-09-23 || [ADR-025](ADR-025-marketplace-scope.md) | Marketplace and commerce scope | Accepted 2026-09-23: Trade stays informational |
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-003: Service/app principal authentication and capability grants
# ADR-003: Service/app principal authentication and capability grants
-**Status:** Accepted, implemented 2026-09-22
+**Status:** Accepted, implemented 2026-09-22. Amended 2026-09-24: delegated authorization semantics and approval UX (roadmap §28.3).
## Context and current evidence
## Context and current evidence
@@ -29,3 +29,78 @@ Remove a grant (new tokens stop carrying it) or stop sending tokens (callers fal
@@ -29,3 +29,78 @@ Remove a grant (new tokens stop carrying it) or stop sending tokens (callers fal
## Acceptance tests
## Acceptance tests
`OpenVibe.Network/test/principals.test.js`, `modules.test.js`, Contracts service-auth tests (forged, expired, wrong audience, alg none, tampered, ungranted, namespace, owner), Media `service-token.test.js`. Production smoke: tokens issued, cross-app and cross-namespace calls refused.
`OpenVibe.Network/test/principals.test.js`, `modules.test.js`, Contracts service-auth tests (forged, expired, wrong audience, alg none, tampered, ungranted, namespace, owner), Media `service-token.test.js`. Production smoke: tokens issued, cross-app and cross-namespace calls refused.
+
+## Amendment 2026-09-24: delegated authorization
+
+Roadmap §28.3 carries "delegated authorization semantics and approval UX" into this ADR. Delegation
+already runs in production: Network issues app tokens with `on_behalf_of` (ADR-014,
+`server/developer/tokens.js`), and Wiki, Blog, News, Reviews and Trade honour it
+(`server/auth/viewer.js` in each, deployed 2026-09-23). This section records the rules they follow,
+so every other receiver implements the same ones.
+
+### Who can act for whom
+
+| Principal | May name the acting person | How |
+|---|---|---|
+| First-party service (`svc:<id>`, `actor_type: service`) | any person | `X-OV-Subject: usr_…` on the request. The service vouches for its own signed-in user. |
+| Developer app (`app:app_…`) | only the person who authorized it | `on_behalf_of` in the token. An `X-OV-Subject` naming anyone else gets 403 `subject.not_delegated`. |
+| Mod (`mod:mod_…`, ADR-013) | only the person who installed or authorized it | the same rule as an app |
+| Any principal, `client_credentials` token | nobody | no `on_behalf_of`. The call is the principal's own, never a person's. |
+
+### What a delegated call may do
+
+- **Intersection, never union.** The effective permission of a delegated call is the capabilities
+ in the token (the app's approved grants ∩ its project allowance ∩ what is still grantable, for that
+ audience), narrowed to the scopes the person approved, and then limited again by what that person
+ may do on the receiver. A delegated call can never do more than the person could do, nor more than
+ the app was granted.
+- **Staff power is never delegated.** A receiver treats a delegated caller as a plain user: staff
+ roles and staff capabilities (`manifests/policy/staff-roles.json`, v0.34.0) are never granted
+ through an app or mod token. Moderation needs a first-party staff surface.
+- **Money is not delegable by default.** A developer app can be granted only `active`
+ capabilities whose manifest says `visibility: public` or `partner` (partner only when staff put it
+ in one project's allowance by hand; Network `server/developer/policy.js`). The only public
+ money-adjacent ones are the read-only
+ `vip.plan.list` and `vip.perk.list`; no capability that moves money, credit or payouts is public,
+ and making one public needs an ADR-012 amendment.
+- **Short and unrefreshable.** App tokens last 5 minutes and have no refresh token. Revoking the
+ app, its credential or a grant stops new tokens at once, and issued tokens end within 5 minutes.
+- **Sandbox stays sandbox.** A receiver refuses `env: sandbox` tokens (401 `token.sandbox_refused`)
+ unless it opted in. Only members of the app's project may authorize a sandbox app.
+- **Audit names both.** A receiver's audit rows and events record the principal (`app:…`) as the
+ actor and the person as `on_behalf_of`. They never record it as the person acting alone.
+
+### Approval UX (Network)
+
+In force today (Network `server/auth/oauth-routes.js`, `public/login.html`):
+
+1. `/oauth/authorize` for an app requires an exact registered `redirect_uri` and PKCE S256.
+ `prompt=none` is refused with `interaction_required`, so a third-party app never gets a code
+ without the person choosing to continue.
+2. The account chooser shows the app's registered name, marked "(third-party app)", and the host
+ the code will be sent to (`/oauth/client-info`). The name is looked up by client id, never taken
+ from the URL.
+3. The code is single use, lasts 5 minutes, and records the approved scope. The token exchange may
+ narrow that scope but never widen it (400 `invalid_scope`).
+
+Required next, owned by Network (not built yet):
+
+4. **The chooser lists the requested capabilities** in plain words (each capability manifest's
+ `description`), before the person continues. A capability the app was not granted is not shown
+ and not issued.
+5. **A stored authorization** per (person, app), holding the approved scopes and a date. A
+ repeated request within those scopes may skip the chooser. A request for more always shows it.
+6. **"Connected apps" on my.openvibe.network**, listing each authorization with its scopes and last
+ use, and a revoke action. Revoking stops new tokens for that person and app at once.
+7. Revoking a person's authorization, and every Network ban, also ends `on_behalf_of` issuance for
+ them (the ban check at exchange exists today).
+
+### Tests
+
+- Receivers: an app token naming another person in `X-OV-Subject` gets 403
+ `subject.not_delegated`. A sandbox token gets 401. A first-party service may name the person.
+ (Wiki, Blog, News, Reviews and Trade have these tests.)
+- Network: `prompt=none` for an app returns `interaction_required`. A widened scope at exchange is
+ `invalid_scope`. A reused or expired code is `invalid_grant`.
+- Still to add, with items 4 to 6: the chooser lists exactly the issued capabilities, and a revoked
+ authorization gets no new token.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-006: Media object, location and namespace model
# ADR-006: Media object, location and namespace model
-**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4)
+**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4). Amended 2026-09-24: four placement classes, the local/dev provider, the infrequent-access tier deferred (roadmap §28.3 m1).
## Context and current evidence
## Context and current evidence
@@ -30,3 +30,32 @@ The object layer is additive; old routes keep working throughout.
@@ -30,3 +30,32 @@ The object layer is additive; old routes keep working throughout.
## Acceptance tests
## Acceptance tests
Non-Live app uploads/reads with only a capability grant; deliberate replica loss is detected; private objects cannot be fetched without a grant; invariant violations are reported.
Non-Live app uploads/reads with only a capability grant; deliberate replica loss is detected; private objects cannot be fetched without a grant; invariant violations are reported.
+
+## Amendment 2026-09-24: placement classes, local provider, infrequent access
+
+Roadmap §28.3 m1 and §28.4 item 6. A copy's **placement class** says why it exists and what may
+remove it. The provider says where it is. There are four classes:
+
+| Class | Purpose | Where today | Rules |
+|---|---|---|---|
+| **canonical** | The durable copy of record | B2 (`provider b2`, `storage_class cold`); the local disk for an object not yet offloaded | Never removed while the object exists or is held. Demotion never touches it. Deletion only after holds clear. |
+| **hot cache** | A promoted copy for heavy playback | R2 (`provider r2`, `storage_class cache`) | Only analytics-gated promotion creates it (the Decision above; roadmap §28.2 M2). Demotion removes only this copy. It is never the only copy. |
+| **asset origin** | Small public assets served through the CDN: avatars, thumbnails, previews, sprites, paste screenshots and similar | Media's own host disk behind Cloudflare (`provider local`, `storage_class hot`) | May be the canonical copy for small assets. It is served with long-lived, content-hashed or revisioned URLs. A separate cheap origin VPS is used only when disk or egress measurements call for one. |
+| **local scratch** | Development and active processing: recordings in progress, remux and cut work, job temporaries | the host's local disk | Never a durable copy and never a location of record. A job cleans up after itself, and a restart may discard scratch. |
+
+`media_locations.storage_class` keeps its values (`hot`, `cold`, `cache`). The class is derived from
+the provider and the object kind until Media adds a `placement` column. That column is additive,
+and Media records it with its lifecycle work.
+
+**Local/dev provider.** `local` is a first-class provider. With no B2 or R2 credentials, every
+Media path degrades to it: uploads, playback, thumbnails, jobs and the objects API
+(`providerConfigured('b2')` and `('r2')` gates in `server/vod/vod-storage.js`). Development and tests
+therefore need no cloud account. Still to do in Media: one provider interface (`put`, `get`,
+`head`, `delete`, `presign`, `list`) with `local`, `b2` and `r2` implementations behind it, instead of
+branches inside `vod-storage.js`. The S3 facade (Alternatives above) would sit on that interface.
+
+**Infrequent-access tier: deferred.** No infrequent-access or archive storage class (B2 has none; S3
+IA and Glacier-style tiers are not used) is adopted at current scale. On 2026-09-24, production held 2,839
+ready objects totalling 481 GB, which fits B2 at its single price. Revisit when retrieval-priced storage would save more than it costs
+in retrieval fees and restore latency, measured from `media_locations` sizes and access. That
+change is a new amendment here.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-007: Logical data ownership and database technology
# ADR-007: Logical data ownership and database technology
-**Status:** Accepted 2026-09-22
+**Status:** Accepted 2026-09-22. Amended 2026-09-24: PostgreSQL runtime posture (pooling, replicas, money-path isolation, Redis non-authoritative; roadmap §28.3 m10).
## Context and current evidence
## Context and current evidence
@@ -28,3 +28,46 @@ n/a
@@ -28,3 +28,46 @@ n/a
## Acceptance tests
## Acceptance tests
Wave 0 baseline shows no service reading another's database; `/ready` truthfulness checked per service in Wave 22.
Wave 0 baseline shows no service reading another's database; `/ready` truthfulness checked per service in Wave 22.
+
+## Amendment 2026-09-24: PostgreSQL runtime posture
+
+Roadmap §28.3 m10 and §28.4 item 15. No service runs on PostgreSQL today. Every service is SQLite on
+one host (the Decision above still holds). These rules bind the first service that moves, and every
+one after it.
+
+1. **Canonical truth, pooled.** A service's PostgreSQL database is its canonical relational truth.
+ Application processes connect through **PgBouncer in transaction-pooling mode**, never with one
+ long-lived connection per process. Code that moves must therefore work without session state:
+ no session-level `SET`, advisory locks held across transactions, `LISTEN`/`NOTIFY` or
+ session-scoped prepared statements through the pooler. Migrations and those few session
+ features use a direct connection with its own role.
+2. **Read replicas serve reads.** Replicas may serve reads that tolerate lag: public pages, lists,
+ search feeds and read models. A read that decides a write, checks a balance, an entitlement, a
+ grant or a ban, or answers read-your-own-write goes to the primary. A service that routes reads
+ to a replica says in `/ready` which reads may lag and by how much.
+3. **Money paths are SERIALIZABLE with row locks.** Every transaction that moves money, credit or a
+ payout (the ADR-012 MONEY and CREDIT classes: Billing's ledger, and anything Tips or VIP writes
+ that changes a balance) runs at `SERIALIZABLE` isolation. It locks the account rows it debits
+ (`SELECT … FOR UPDATE`, in a fixed order by account id so two transfers cannot deadlock), and it
+ retries a serialization failure (SQLSTATE 40001) a bounded number of times with the same
+ idempotency key. A balance check and the entry it guards are always in one transaction.
+ Loyalty ledgers (OpenCoins, channel points) follow the same rule when they move.
+4. **Redis is never authoritative** for money, credit, entitlements, account or identity state,
+ grants or bans. It may hold caches, rate-limit counters, presence and fan-out that can be lost and
+ rebuilt from the owner's database. Losing Redis must never lose a write or change an answer about
+ money or accounts. No service uses Redis today; Chat's and Live's history stores keep a note that
+ a ring buffer or Redis may replace the in-process cache later, under this rule.
+5. **One write role per service**, as the Decision says. A shared cluster isolates ownership by
+ database and role (roadmap §17.1). Cross-domain joins happen in read models or analytics.
+
+While on SQLite, the equivalent money-path rule is already in force: Billing posts every journal
+transaction inside one `better-sqlite3` transaction together with its funds check
+(`server/ledger.js`). SQLite's single writer serializes those transactions.
+
+### Tests (when the first service moves)
+
+- The service's suite passes against PostgreSQL behind PgBouncer in transaction mode.
+- A concurrent-debit test: two transfers race for the same balance, exactly one succeeds, and the
+ journal still sums to zero.
+- A lag test: a replica read is never used for a balance, entitlement or grant decision.
+- Redis flushed mid-run changes no money or account answer.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-012: Billing vs loyalty, credits and currency classification
# ADR-012: Billing vs loyalty, credits and currency classification
-**Status:** Accepted 2026-09-22. Gates Wave 8 (OpenVibe.Billing), Wave 9 (Tips) and Wave 10 (VIP).
+**Status:** Accepted 2026-09-22. Gates Wave 8 (OpenVibe.Billing), Wave 9 (Tips) and Wave 10 (VIP). Amended 2026-09-24: ledger transaction types mapped to the roadmap's names; OpenCoins stays in Network; the payouts/refunds/plans table substitution.
## Context and current evidence
## Context and current evidence
@@ -70,3 +70,52 @@ Until cutover, Live remains authoritative and Billing only shadows (imports and
@@ -70,3 +70,52 @@ Until cutover, Live remains authoritative and Billing only shadows (imports and
- Creator payables reconcile exactly to the ledger; imported opening balances equal the Live columns at cutover, with every adjustment listed.
- Creator payables reconcile exactly to the ledger; imported opening balances equal the Live columns at cutover, with every adjustment listed.
- The freeze switch refuses writes and serves reads.
- The freeze switch refuses writes and serves reads.
- Entitlements are queryable with Live offline.
- Entitlements are queryable with Live offline.
+
+## Amendment 2026-09-24: transaction types, OpenCoins, table substitution
+
+### Ledger transaction types (roadmap §28.3, §28.4 item 23)
+
+The roadmap asks for named types: platform fee, creator earning, payout hold and release, and the
+compensating reversals. Billing does not add them as new `transactions.type` values. Each of them
+is a **leg** (an entry on an account kind) of an existing, named type. The mapping is binding, and
+reports and events use these names:
+
+| Roadmap name | Billing type(s) | Leg (account kind) | Where |
+|---|---|---|---|
+| **Platform fee** (purchase spread, retained subscription share, site-route fee) | `purchase`, `subscription`, site-routed tips from receipts | credit to `platform_revenue` (cents or bits). The provider's own fee is kept as `fee_cents` in metadata, never as revenue. | `server/ops/common.js` receiptEntries, `ops/subscriptions.js` |
+| **Creator earning** | `donation` (tip, donation, paid interaction), `subscription` (the creator's share), `import` / `subscription_share` for imported Live history | credit to `creator_payable:<subject>` | `ops/transfers.js`, `ops/subscriptions.js`, `importer/live.js` |
+| **Payout hold** | `cashout_request` | `creator_payable` → `payouts_pending`, with `escrow_until`. Billing refuses approval during escrow (rule 10). | `ops/cashouts.js` request |
+| **Payout release** (paid out) | `cashout_paid` | `payouts_pending` → `provider_clearing:<provider>_payouts` (bits to cents through `fx_conversion`), with the provider's payout reference required | `ops/cashouts.js` approve |
+| **Payout release** (returned) | `cashout_denied` | `payouts_pending` → `creator_payable`, and `reverses_txn` → the request | `ops/cashouts.js` deny |
+| **Compensating reversals** | `refund`, `chargeback` (with `reverses_txn` → the original); `refund` of a transfer (a media request that never played); `adjustment` (staff, with a reason) | back to `user_credit` or out through `refunds` / `chargeback_loss`. A creator's payable is never clawed back for credit already given away: the loss is booked and flagged for review. | `ops/reversals.js`, `ops/transfers.js` refund, `ops/admin.js` |
+| (MONEY → CREDIT) | `recycle` | `creator_payable` → `user_credit` (rule 4) | `ops/cashouts.js` recycle |
+
+A new money movement gets a new `type` value only when no existing type and leg describes it.
+Adding one is additive in Billing's CHECK and in `billing.transaction.settled@1`.
+
+### OpenCoins stays in Network (supersession)
+
+Roadmap Wave 8 (line 692) says "Network's OpenCoins wallet becomes a Billing client". **This ADR
+supersedes that line.** OpenCoins are LOYALTY. Network owns them, with their own ledger, and
+Billing does not hold, mirror or proxy them (see Decision, and Migration consequences: "OpenCoins
+and channel points are **not** migrated into Billing in Wave 8"). A Billing loyalty ledger, if it
+ever comes, is a separate step with its own ADR amendment, and it never shares accounts with MONEY
+or CREDIT. The Network requirement ledger (D28) and the roadmap crosswalk should cite this
+paragraph instead of listing the line as remaining.
+
+### Table substitution (roadmap §11.1.1)
+
+The roadmap's minimal entity list names twelve tables. Billing has nine of them under those names:
+`accounts`, `ledger_entries`, `transactions`, `provider_events`, `payment_intents`, `subscriptions`,
+`entitlements`, `reconciliation_runs` and `idempotency_keys`. It deliberately substitutes the other
+three:
+
+| Roadmap entity | Billing's equivalent | Why |
+|---|---|---|
+| `payouts` | `cashouts` (`server/db.js`), with the `cashout_request` / `cashout_paid` / `cashout_denied` journal transactions | The product calls them cashouts. The row holds the payout method, escrow and provider payout reference. |
+| `refunds` | reversal transactions (`type refund` or `chargeback`, `reverses_txn` → the original) plus the `refunds` and `chargeback_loss` accounts (`server/ops/reversals.js`) | A refund is a journal fact, not a separate mutable record. Partial and cumulative reversals are checked against the original. |
+| `plans` | VIP's `vip_plans` (OpenVibe.VIP) | Plans and perks are VIP's (see the ENTITLEMENT row in the Decision). Billing holds only the entitlement truth. |
+
+Billing also has `external_receipts` (EXTERNAL-class PowerChat tips, recorded without liability)
+and `account_balances` (a projection of the journal, checked by reconciliation). Thin `payouts` or
+`refunds` views may be added for reporting. They are not required.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-013: Mod manifest, trust and sandbox/resource model
# ADR-013: Mod manifest, trust and sandbox/resource model
-**Status:** Accepted 2026-09-23. Gates Wave 12 (mods) and Wave 21 Stage C (sandboxed user code).
+**Status:** Accepted 2026-09-23. Gates Wave 12 (mods) and Wave 21 Stage C (sandboxed user code). Amended 2026-09-24: signing and review are open owner decisions; mod monetization only through Billing; manifest 1.1.0 (read/write grants, billing hooks, dependencies).
## Context and current evidence
## Context and current evidence
@@ -32,3 +32,59 @@ Revoke the grants; the manifest schema stays.
@@ -32,3 +32,59 @@ Revoke the grants; the manifest schema stays.
- A capability the mod was denied cannot be reached from its scripted runtime.
- A capability the mod was denied cannot be reached from its scripted runtime.
- A revoked mod stops affecting the world.
- A revoked mod stops affecting the world.
- Install, grant, use and revoke all appear in the audit log.
- Install, grant, use and revoke all appear in the audit log.
+
+## Amendment 2026-09-24: open decisions, monetization and manifest 1.1.0
+
+Roadmap §28.3 (m6 and "Mod signing and review") asks for three things this ADR left implicit.
+
+### Open owner decisions: package signing and review
+
+Neither is decided. Both stay **open decisions for the owner**, and nothing may fill them with a
+silent default.
+
+1. **Package signing.** Who signs a mod release and with what key: the publisher, the platform at
+ review time, or both. Which artifact the signature covers: the manifest, the pack or bundle, and
+ the Media asset hashes. Where a runtime checks the signature. What an unsigned release may do.
+2. **Review process.** Who reviews, against which checklist, and what a review changes. It may raise
+ the trust tier. It never grants anything, because tiers stay metadata. Also open: how a published
+ review is withdrawn, and whether the ADR's tier names (`unreviewed`, `reviewed`, `first-party`) or
+ the roadmap §15 vocabulary are final (requirement ledger D32).
+
+Until the owner decides, installs stay as they are. Only staff install mods: Games
+`POST /api/v1/mods` is staff-only, with the approved capability subset given at install. No
+self-service publishing or install exists. Executable mods wait for Host Stage C in any case. The
+decision will be recorded as a new amendment here, before any non-staff install path ships.
+
+### Monetization only through Billing
+
+A mod earns money only through Billing and marketplace primitives (ADR-012, ADR-025), never around
+them:
+
+- A mod never takes payment, never sees payment details and never holds a balance. Prices, plans and
+ entitlements live in Billing and VIP, not in the mod or its manifest.
+- A mod may **check an entitlement** that the person holds (`billing.entitlement.check`,
+ `vip.entitlement.check`) and may **send the person to a platform checkout**
+ (`vip.membership.checkout`, Billing intents). Both need the capability in the install's approved
+ grants, like any other call.
+- Every paid unlock a mod gates is an ENTITLEMENT (ADR-012). Game state bought with money needs its
+ own ADR (ADR-012 rule 7), and so does any creator share for a mod publisher.
+- A marketplace for mods is out of scope until ADR-025 is revisited.
+
+### Manifest 1.1.0 (openvibe-contracts v0.34.0)
+
+`mods.mod-manifest@1` gains three optional fields, so every 1.0.0 manifest stays valid:
+
+- `permissions.readGrants` and `permissions.writeGrants` split module and Media namespace access into
+ read-only and read-write, each as `{ modules?, mediaNamespaces? }`. A namespace that is only in
+ `readGrants` is read-only. The older `permissions.modules` and `permissions.mediaNamespaces` still
+ mean read and write.
+- `billingHooks`: the entitlement checks and checkouts the mod uses, as `{ kind, key, description? }`.
+ `kind` is `entitlement` or `checkout`, and `key` is an entitlement or plan key (`vip.plan:pln_…`).
+ These declare intent for review and for the install screen. The capability still has to be
+ granted.
+- `dependencies`: other mods this release needs, as `{ id: mod_…, version: <semver range>,
+ optional? }`. A runtime refuses to enable a mod whose required dependency is not installed, not
+ enabled, or outside the range.
+
+Games validates against a local copy of the 1.0.0 schema (`apps/server/src/mods/manifestSchema.ts`).
+It adopts 1.1.0 when it bumps its contracts pin.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-021: Analytics ownership and privacy bounds
# ADR-021: Analytics ownership and privacy bounds
-**Status:** Accepted 2026-09-23. Contract now; extraction decision at Wave 22.
+**Status:** Accepted 2026-09-23. Contract now; extraction decision at Wave 22. Amended 2026-09-24: extraction decided (no analytics service; analytics stays per service through `openvibe-shared/analytics`).
## Context and current evidence
## Context and current evidence
@@ -31,3 +31,42 @@ Pruning is the only destructive step. Export the rollups before enabling it.
@@ -31,3 +31,42 @@ Pruning is the only destructive step. Export the rollups before enabling it.
- No raw analytics row older than 30 days.
- No raw analytics row older than 30 days.
- No IP address or subject id in any analytics table.
- No IP address or subject id in any analytics table.
+
+## Amendment 2026-09-24: the extraction decision
+
+Wave 22 asked whether to create an analytics service, "from measured volume". **Decision: no
+analytics service.** Analytics stays per service, through the shared module
+`openvibe-shared/analytics` (tracker, privacy, retention, schema, prune CLI; Shared ≥ 1.4.0).
+The ADR-021 privacy rules above apply unchanged: no IP address, precise location or subject id in
+raw analytics, and at most 30 days of raw events.
+
+**Measured volume** (production, read-only, 2026-09-24):
+
+- Live `analytics.db`, the largest: 1,776,181 raw events in its 30-day window (2026-08-25 to
+ 2026-09-24), 58,247 in the last 24 hours, a 502 MB SQLite file. It was 1.7 GB before the
+ ADR-021 prune and scrub.
+- Network: raw analytics in its own database, pruned to 30 days (network.db 731 → 276 MB after the
+ ADR-021 scrub and VACUUM).
+- Tools: one analytics database per satellite, each smaller than Live's.
+
+One process per service writes this volume into SQLite without strain. No product needs raw events
+joined across services: the cross-site rollup already lives in Tools and Network. An extra
+service would add a deploy unit, a network hop on every page view and a second copy of the privacy
+rules, and would buy nothing measured.
+
+**Where the contract lives.** The event schema is `openvibe-shared/analytics/event.v1.json`
+(`docs/schemas/analytics-event.v1.json` in OpenVibe.Shared), versioned with the module that writes
+it. It is not copied into this repository's catalog, because a second copy would drift. The Decision
+above said "in OpenVibe.Contracts"; this amendment corrects that.
+
+**Revisit** (a new amendment here) when any of these is measured:
+
+- one service's raw events pass 1,000,000 a day, or its 30-day raw table passes 5 GB;
+- a product needs raw events joined across services (funnels or attribution across sites), which
+ rollups cannot answer;
+- analytics writes show up in a service's latency or `/ready`;
+- the platform moves to more than one host for a service that records analytics.
+
+**Consumers:** Live (`server/index.js`, `server/paths.js`), Network (`server/analytics/network.js`)
+and the Tools satellites (`apps/*/server/index.js`) use the module. A new product that wants
+analytics uses it too, with its own database and the nightly prune.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ADR-022: Moderation and policy console
# ADR-022: Moderation and policy console
-**Status:** Accepted 2026-09-23. Revisit at Wave 12.
+**Status:** Accepted 2026-09-23. Revisited at Wave 12 (2026-09-24): the decision stands, with no moderation service; next revisit at Wave 22 or when the trigger below is met.
## Context and current evidence
## Context and current evidence
@@ -33,3 +33,38 @@ The events are additive.
@@ -33,3 +33,38 @@ The events are additive.
- Every staff action in Live, Community and Chat appears in the audit log with actor and reason.
- Every staff action in Live, Community and Chat appears in the audit log with actor and reason.
- A service without the moderate capability cannot vouch for staff.
- A service without the moderate capability cannot vouch for staff.
+
+## Revisit at Wave 12 (2026-09-24): outcome
+
+Wave 12 (Games and mods) was the named revisit point, because mods could have made moderation a
+multi-product workflow. **Outcome: the decision stands.** Network stays the staff directory and
+the moderation audit log, enforcement stays with each owner, and no moderation service is created.
+
+Evidence:
+
+- **No cross-product workflow exists.** Each moderated thing has one owner:
+ - Chat moderates chat (channel-scoped bans, deletions; it emits `chat.moderation.action`).
+ - Community moderates pastes, threads and comments (`community.*.moderate`).
+ - Tips moderates interactions (`tips.interaction.moderated`).
+ - Billing logs staff money actions (`billing.staff.action`).
+ - Games installs, grants and revokes mods itself: installing is staff-only, audited in its own
+ `mod_audit`.
+
+ No queue, appeal or case spans two of them.
+- **Mods did not change that.** Games' mods are declarative packs installed by staff. There is no
+ self-service publishing, so there is no review queue. Mod signing and review are open owner
+ decisions (ADR-013 amendment of 2026-09-24).
+- **What is still missing is inside this decision, not a reason to change it.**
+ - Network does not consume `*.moderation.action` yet (requirement D05-R2), so the audit log has
+ no entries.
+ - Services still check raw roles. v0.34.0 adds the staff capability map
+ (`manifests/policy/staff-roles.json`): `user < streamer < global_mod < admin < owner` and the
+ `staff.*` capabilities each role holds. Network issues it as claims, and Live, Chat and
+ Community replace their raw checks with it.
+
+**Trigger for the next revisit** (at Wave 22 at the latest): the first workflow that no single
+owner can host, for example:
+
+- an appeal that crosses products;
+- one report queue for Chat, Community and Media;
+- a public mod marketplace with review.
@@ -6,17 +6,17 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
@@ -6,17 +6,17 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
|---|---|---|
|---|---|---|
| [ADR-001](ADR-001-canonical-subjects.md) | Canonical subject IDs and legacy identity mapping | Accepted, implemented 2026-09-22 |
| [ADR-001](ADR-001-canonical-subjects.md) | Canonical subject IDs and legacy identity mapping | Accepted, implemented 2026-09-22 |
| [ADR-002](ADR-002-contract-repository.md) | Contract repository and compatibility policy | Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+) |
| [ADR-002](ADR-002-contract-repository.md) | Contract repository and compatibility policy | Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+) |
-| [ADR-003](ADR-003-service-principals.md) | Service/app principal authentication and capability grants | Accepted, implemented 2026-09-22 |
+| [ADR-003](ADR-003-service-principals.md) | Service/app principal authentication and capability grants | Accepted, implemented 2026-09-22; amended 2026-09-24 (delegated authorization and approval UX) |
| [ADR-004](ADR-004-durable-events.md) | Durable event store, outbox/inbox and delivery semantics | Accepted 2026-09-22; implementation in progress (OpenVibe.Events) |
| [ADR-004](ADR-004-durable-events.md) | Durable event store, outbox/inbox and delivery semantics | Accepted 2026-09-22; implementation in progress (OpenVibe.Events) |
| [ADR-005](ADR-005-realtime.md) | Realtime topics, cursor/resume and whether OpenVibe.Realtime exists | Accepted 2026-09-22: realtime runs inside OpenVibe.Events |
| [ADR-005](ADR-005-realtime.md) | Realtime topics, cursor/resume and whether OpenVibe.Realtime exists | Accepted 2026-09-22: realtime runs inside OpenVibe.Events |
-| [ADR-006](ADR-006-media-objects.md) | Media object, location and namespace model | Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4) |
-| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22 |
+| [ADR-006](ADR-006-media-objects.md) | Media object, location and namespace model | Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4); amended 2026-09-24 (placement classes, local provider, infrequent access deferred) |
+| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22; amended 2026-09-24 (PgBouncer, replicas, SERIALIZABLE money paths, Redis non-authoritative) |
| [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
| [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
| [ADR-009](ADR-009-live-openre-boundary.md) | Live versus OpenRe boundary | Accepted 2026-09-23; OpenRe deployed, no slot moved yet |
| [ADR-009](ADR-009-live-openre-boundary.md) | Live versus OpenRe boundary | Accepted 2026-09-23; OpenRe deployed, no slot moved yet |
| [ADR-010](ADR-010-chat-boundary.md) | Chat versus Live/Community boundary | Accepted and executed 2026-09-23 (Chat serves Live's chat) |
| [ADR-010](ADR-010-chat-boundary.md) | Chat versus Live/Community boundary | Accepted and executed 2026-09-23 (Chat serves Live's chat) |
| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |
| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |
-| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10 |
-| [ADR-013](ADR-013-mod-manifest.md) | Mod manifest, trust and sandbox/resource model | Accepted 2026-09-23; gates Waves 12 and 21C |
+| [ADR-012](ADR-012-economic-classification.md) | Billing vs loyalty, credits and currency classification | Accepted 2026-09-22; gates Waves 8-10; amended 2026-09-24 (transaction-type mapping, OpenCoins stays in Network, table substitution) |
+| [ADR-013](ADR-013-mod-manifest.md) | Mod manifest, trust and sandbox/resource model | Accepted 2026-09-23; gates Waves 12 and 21C; amended 2026-09-24 (signing and review open, monetization through Billing, manifest 1.1.0) |
| [ADR-014](ADR-014-developer-projects.md) | Developer project, tenant and quota model | Accepted 2026-09-23; gates Waves 20-21 |
| [ADR-014](ADR-014-developer-projects.md) | Developer project, tenant and quota model | Accepted 2026-09-23; gates Waves 20-21 |
| [ADR-015](ADR-015-ai-workflows.md) | AI workflow and publication ownership boundary | Accepted 2026-09-23; OpenVibe.AI deployed |
| [ADR-015](ADR-015-ai-workflows.md) | AI workflow and publication ownership boundary | Accepted 2026-09-23; OpenVibe.AI deployed |
| [ADR-016](ADR-016-active-client-updates.md) | Active client update safety and mixed-version window | Accepted 2026-09-23; Track R |
| [ADR-016](ADR-016-active-client-updates.md) | Active client update safety and mixed-version window | Accepted 2026-09-23; Track R |
@@ -24,8 +24,8 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
@@ -24,8 +24,8 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-018](ADR-018-search-home.md) | Home of the search index and query contract | Accepted 2026-09-23: OpenVibe.Search, FTS first |
| [ADR-018](ADR-018-search-home.md) | Home of the search index and query contract | Accepted 2026-09-23: OpenVibe.Search, FTS first |
| [ADR-019](ADR-019-publishing-packages.md) | Home of the shared publishing packages | Accepted 2026-09-23: OpenVibe.Publishing, packages only |
| [ADR-019](ADR-019-publishing-packages.md) | Home of the shared publishing packages | Accepted 2026-09-23: OpenVibe.Publishing, packages only |
| [ADR-020](ADR-020-notification-delivery.md) | Notification delivery ownership | Accepted 2026-09-23: Network authority, Events transport |
| [ADR-020](ADR-020-notification-delivery.md) | Notification delivery ownership | Accepted 2026-09-23: Network authority, Events transport |
-| [ADR-021](ADR-021-analytics.md) | Analytics ownership and privacy bounds | Accepted 2026-09-23: contract now, 30-day raw retention |
-| [ADR-022](ADR-022-moderation-console.md) | Moderation and policy console | Accepted 2026-09-23: Network module + audit events |
+| [ADR-021](ADR-021-analytics.md) | Analytics ownership and privacy bounds | Accepted 2026-09-23: contract now, 30-day raw retention; extraction decided 2026-09-24: no analytics service |
+| [ADR-022](ADR-022-moderation-console.md) | Moderation and policy console | Accepted 2026-09-23: Network module + audit events; Wave 12 revisit 2026-09-24: stands |
| [ADR-023](ADR-023-client-surfaces.md) | First-party client surfaces | Accepted 2026-09-23: OpenVibe.Extensions at Wave 18 |
| [ADR-023](ADR-023-client-surfaces.md) | First-party client surfaces | Accepted 2026-09-23: OpenVibe.Extensions at Wave 18 |
| [ADR-024](ADR-024-theme-authority.md) | Theme authority between Network and Shared | Accepted 2026-09-23 |
| [ADR-024](ADR-024-theme-authority.md) | Theme authority between Network and Shared | Accepted 2026-09-23 |
| [ADR-025](ADR-025-marketplace-scope.md) | Marketplace and commerce scope | Accepted 2026-09-23: Trade stays informational |
| [ADR-025](ADR-025-marketplace-scope.md) | Marketplace and commerce scope | Accepted 2026-09-23: Trade stays informational |