openvibers/openvibe.contracts Commits
ADRs 001-008 and 011: canonical subjects, contract repository, service principals, durable events, realtime inside Events, media objects, data ownership, shared packages, Community pastes
Changes
nnew file mode 100644@@@ -0,0 +1,32 @@++# ADR-001: Canonical subject IDs and legacy identity mapping++++**Status:** Accepted, implemented 2026-09-22++++## Context and current evidence++++Live, Media and Games each kept their own integer user ids; the same integer named different people in different services (Network #57 and Live #57). Community existed partly to translate identities through Live. The Wave 0 baseline counted 391 Network accounts, 27,848 guest sessions, and 326 Live↔Network links known only to Live.++++## Decision++++- Every actor is a `SubjectRef` `{ type: user|guest|service|app|mod|system, id }` (contract `identity.subject-ref@1`). Users and guests get ULID ids (`usr_`, `gst_`), services and system actors slugs.++- OpenVibe.Network issues subjects and keeps `identity_legacy_map (source_system, source_type, source_id) -> subject_id`. A mapping is never repointed; a conflicting write is reported.++- Subject ids are added **alongside** integer ids. Tokens keep the integer `sub` and gain `subject_id`.++- Services resolve through `/internal/identity/resolve` and `resolve-batch` with a service token (`identity.subject.resolve`).++++## Alternatives considered++++- Replace integer ids with subjects everywhere at once (big-bang): rejected, every consumer breaks together.++- Let each service keep translating through the service that knows (Community → Live): rejected, it is the coupling this removes.++- UUIDv4 ids: rejected in favour of ULIDs, which sort by creation time and keep indexes append-mostly.++++## Migration consequences++++Backfilled at Network boot (idempotent, time part from `created_at`). Live reports its links daily (`identity-legacy-sync`); `/internal/link-account` writes the map too. Rows created in the 2026-08-17..20 window may carry Network ids where Live ids were expected (see ADR-011 import).++++## Rollback++++Additive: removing `subject_id` columns and the map returns to the previous state; nothing reads subjects in place of integer ids where a legacy caller still needs the integer.++++## Acceptance tests++++`OpenVibe.Network/test/identity-subjects.test.js`: backfill validity and stability, legacy-map seeding, no repointing, problem+json errors, `subject_id` in tokens with `sub` unchanged. Production: resolve by Live id returns the Network account; unknown ids return 404 problem.nnew file mode 100644@@@ -0,0 +1,31 @@++# ADR-002: Contract repository and compatibility policy++++**Status:** Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+)++++## Context and current evidence++++Cross-service contracts lived as prose in CONTRACTS.md and as copied code; nothing validated them.++++## Decision++++- `OpenVibe.Contracts` is the machine-readable authority: JSON Schema 2020-12 contracts with a catalog (id, semver, owner, visibility, compatibility, status), fixtures for every contract, generated TypeScript types, service/capability/namespace manifests.++- A contract id is permanent; minor versions only add optional fields; a breaking change is a new major file plus a deprecation record with a replacement and window.++- `scripts/compat.js` fails CI on breaking changes against the previous tag; services run `openvibe-contracts-check` in their CI.++- Consumers pin a release tarball (`codeload.github.com/.../tar.gz/refs/tags/vX`), so production installs need no GitHub credentials.++++## Alternatives considered++++- Publish to the npm registry: deferred; tarball pins are immutable enough and need no registry account.++- OpenAPI-first: rejected as the source of truth; OpenAPI fragments can be generated later from the schemas.++++## Migration consequences++++Contracts grow additively; each service moves to a new tag by bumping its pin.++++## Rollback++++A bad release is fixed forward (new patch tag); consumers can pin the previous tag.++++## Acceptance tests++++Contracts `npm test` (fixtures, catalog integrity, manifests cross-checked, compat gate), CI green on every tag; the compat gate was shown to fail on a deliberate breaking edit.nnew file mode 100644@@@ -0,0 +1,31 @@++# ADR-003: Service/app principal authentication and capability grants++++**Status:** Accepted, implemented 2026-09-22++++## Context and current evidence++++First-party services authenticated to each other with one shared `X-Internal-Key` per deployment, with unrestricted reach.++++## Decision++++- Services obtain 5-minute RS256 tokens from Network's `/oauth/token` with `grant_type=client_credentials` and their existing OAuth client credentials. Claims follow `identity.service-token-claims@1`: `sub svc:<id>`, `aud`, `cap` (exact grants for that audience, optionally narrowed by `scope`), `ns`, `jti`.++- Grants live in Network's `principal_grants` (revocable). Receivers check one capability per route (`requireCapability` / `principals.guard` / `tenantAuth({ capability })`), plus namespace and ownership (`app_id` must be the caller's own).++- A request with a Bearer token is judged only on that token; a bad token is never rescued by a legacy key. New routes refuse the legacy key outright.++- `principal_usage` counts every decision by caller, route and auth method; the key is retired per route once legacy use reaches zero.++++## Alternatives considered++++- mTLS between services: deferred; one host today and tokens give per-action scope that mTLS alone would not.++- Long-lived per-service API keys: rejected, not scoped to actions and hard to rotate.++++## Migration consequences++++Guarded routes accept both during migration; callers fall back to the key only when no token can be had.++++## Rollback++++Remove a grant (new tokens stop carrying it) or stop sending tokens (callers fall back to the key).++++## 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.nnew file mode 100644@@@ -0,0 +1,31 @@++# ADR-004: Durable event store, outbox/inbox and delivery semantics++++**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Events)++++## Context and current evidence++++Cross-service side effects are best-effort POSTs and HMAC webhooks; a consumer that is down misses them.++++## Decision++++- OpenVibe.Events persists every event (`events.event-envelope@1`) before any consumer sees it, then delivers to subscriptions with retries (exponential backoff), a dead-letter state, and replay by cursor.++- Producers write events to a transactional outbox in the same database transaction as the domain change; a relay publishes them (at-least-once, idempotent by `event_id`).++- Consumers get exactly-once *effects* through an inbox of idempotency receipts in their own database.++- Storage starts on SQLite (single host, see ADR-007); Redis is not durable truth and is not used.++++## Alternatives considered++++- Kafka/NATS now: rejected, operational weight far beyond current volume.++- Webhooks only: rejected, no durability or replay.++++## Migration consequences++++Migrate one existing family at a time (notifications, Media ready/failed, stream lifecycle), keeping the old direct call until the event path is proven, then deleting it.++++## Rollback++++Producers can switch back to their direct calls; events already stored stay replayable.++++## Acceptance tests++++Events tests: persistence before delivery, retry/backoff, DLQ, replay, crash-and-replay yields one effect, outbox commit/rollback, SSE visibility and resume.nnew file mode 100644@@@ -0,0 +1,30 @@++# ADR-005: Realtime topics, cursor/resume and whether OpenVibe.Realtime exists++++**Status:** Accepted 2026-09-22: realtime runs inside OpenVibe.Events++++## Context and current evidence++++The charter for OpenVibe.Realtime was frozen pending this decision. Browser realtime today is five product-local WebSocket servers in Live.++++## Decision++++- Browser realtime is a delivery plane inside OpenVibe.Events (SSE `/realtime/stream`), authorised per topic and per event visibility (`public`, `subject`, `internal` never to browsers), with `Last-Event-ID` resume and an explicit `gap` message when history is gone.++- `OpenVibe.Realtime` is **not** created now. Revisit only when measured connection counts or fan-out latency need an independently scaled process; the move is mechanical because the protocol is fixed here.++- Media and game traffic never go through this plane.++++## Alternatives considered++++- Separate Realtime service now: rejected, adds a deploy unit with no measured need.++- WebSockets instead of SSE: SSE suffices for server→browser fan-out and resumes natively; chat keeps its own WebSocket until Wave 6.++++## Migration consequences++++Live's product WebSockets stay until their domains move (Chat in Wave 6).++++## Rollback++++SSE is additive; disabling it leaves durable delivery untouched.++++## Acceptance tests++++Guessed private topic yields nothing; reconnect from a cursor gets the complete authorised sequence or a gap marker.nnew file mode 100644@@@ -0,0 +1,32 @@++# ADR-006: Media object, location and namespace model++++**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4)++++## Context and current evidence++++Media stored VODs, clips, files and paste screenshots in separate predecessor-shaped tables with a single mutable provider field; copies in local/B2/R2 were implicit.++++## Decision++++- `media_objects` (`med_<ULID>`) is the canonical record; VOD/clip/file rows become typed projections linked by `object_id`.++- `media_locations` records every copy (local/B2/R2) with state and checksum; B2 stays canonical archive, R2 a promoted hot copy; promotion is analytics-gated policy in configuration, demotion only removes the promoted copy.++- Namespaces are the `:app` tenants plus developer projects later; access by app key or service token with a namespace-scoped capability.++- Retention holds (moderation, DMCA, creator pin, admin, evidence) block deletion and demotion.++- The public object-size invariant is configuration (default max 500 MB, target 256, warn 384), validated and reported, not a copied constant.++++## Alternatives considered++++- Keep per-kind tables only: rejected, every new kind re-implements storage.++- Full S3 wire compatibility first: deferred; a small native object API first, an S3 facade later if needed.++++## Migration consequences++++Backfill creates an object per existing row without moving bytes; reconciliation verifies locations before any state is trusted.++++## Rollback++++The object layer is additive; old routes keep working throughout.++++## 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.nnew file mode 100644@@@ -0,0 +1,30 @@++# ADR-007: Logical data ownership and database technology++++**Status:** Accepted 2026-09-22++++## Context and current evidence++++Every service uses its own SQLite database on one host. The roadmap targets PostgreSQL as canonical relational truth, but forbids a big-bang migration.++++## Decision++++- **Ownership is the rule, the engine is not:** one authoritative writer per dataset; no service reads or writes another service's database; cross-domain joins happen in read models or analytics.++- 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; the migration follows the rehearsal pipeline (snapshot, idempotent import, parity, runtime verification) and money paths use SERIALIZABLE transactions and row locks when they move.++++## Alternatives considered++++- Migrate everything to PostgreSQL now: rejected, no measured need and highest risk.++- Shared database with schemas per service: rejected until PostgreSQL arrives; even then write roles are per service.++++## Migration consequences++++None immediately; each future migration carries its own plan.++++## Rollback++++n/a++++## Acceptance tests++++Wave 0 baseline shows no service reading another's database; `/ready` truthfulness checked per service in Wave 22.nnew file mode 100644@@@ -0,0 +1,31 @@++# ADR-008: Shared package publication and release manifests++++**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Shared)++++## Context and current evidence++++`openvibe-shared` lived in Network and was hand-rsynced into four repositories; copies had diverged (Live user-card.js; ov-icons.js in three consumers).++++## Decision++++- `OpenVibe.Shared` is the canonical package, released by semver tags and consumed by tarball pin, like openvibe-contracts.++- Browser files are served by Network at `/shared/*` from its pinned package (and may be served locally by a site from `node_modules`), so a Network outage cannot blank another site's first paint beyond what the inline critical styles already cover.++- No site edits a copy; vendored copies are removed.++- Release manifests and the active-client update coordinator follow in Track R.++++## Alternatives considered++++- Keep vendoring with a sync script: rejected, drift already happened.++- A CDN-only global script: rejected by anti-goal 7 (mutable unversioned runtime dependency).++++## Migration consequences++++Consumers move one at a time, smallest first (Community, Media, Tools, Sites, Live).++++## Rollback++++A consumer can pin the previous tag.++++## Acceptance tests++++No repository contains a vendored copy (CI drift check); navbar stays under its size budget.nnew file mode 100644@@@ -0,0 +1,30 @@++# ADR-011: Community paste and comment ownership migration++++**Status:** Accepted and executed 2026-09-22++++## Context and current evidence++++Pastes were stored in Media (bytes and semantics) and written through Live, which translated identities; Community proxied everything through Live.++++## Decision++++- OpenVibe.Community owns pastes, versions, likes and comments in its own database; owners are Network subjects.++- Media keeps screenshot bytes; new screenshots go to Media's token-only `community` tenant.++- AI-derived pastes are origin `ai` and ownerless, linked to their stream (roadmap §33).++- Live and the Tools gateway forward to Community; Media answers 410 to paste writes and 301s paste pages.++++## Alternatives considered++++- Keep pastes in Media and teach it subjects (option A): rejected, the semantics move to Community in Wave 5 anyway.++++## Migration consequences++++Export bundle (read-only, sha256, no IPs) → idempotent import with legacy maps and import-hold; owners in the 2026-08-17..20 window where both id spaces resolve differently are held (1 paste, 4 comments). Shadow parity: 878/878 public pastes identical before the flip. Incremental import after the flip caught 2 late pastes.++++## Rollback++++Set PASTES_AUTHORITY=live in Community and Live, remove Media's PASTES_FROZEN_APPS/PASTES_MOVED_TO; export Community-side writes first.++++## Acceptance tests++++Community test suite (store, API, importer, SSR), Live `pastes-community.test.js`, Media `pastes-moved.test.js`; production: all three entry points serve the same 880 pastes.nnew file mode 100644@@@ -0,0 +1,15 @@++# Architecture decision records++++Platform-wide decisions from the OpenVibe development roadmap (section 10). Each record states the evidence, the decision, the alternatives, migration consequences, rollback and the tests that prove it (roadmap 23.4). A record is written before the code it governs; records for decisions taken earlier on 2026-09-22 were written the same day the code shipped.++++| ADR | Decision | Status |++|---|---|---|++| [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-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-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |++| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
+# ADR-001: Canonical subject IDs and legacy identity mapping
+
+**Status:** Accepted, implemented 2026-09-22
+
+## Context and current evidence
+
+Live, Media and Games each kept their own integer user ids; the same integer named different people in different services (Network #57 and Live #57). Community existed partly to translate identities through Live. The Wave 0 baseline counted 391 Network accounts, 27,848 guest sessions, and 326 Live↔Network links known only to Live.
+
+## Decision
+
+- Every actor is a `SubjectRef` `{ type: user|guest|service|app|mod|system, id }` (contract `identity.subject-ref@1`). Users and guests get ULID ids (`usr_`, `gst_`), services and system actors slugs.
+- OpenVibe.Network issues subjects and keeps `identity_legacy_map (source_system, source_type, source_id) -> subject_id`. A mapping is never repointed; a conflicting write is reported.
+- Subject ids are added **alongside** integer ids. Tokens keep the integer `sub` and gain `subject_id`.
+- Services resolve through `/internal/identity/resolve` and `resolve-batch` with a service token (`identity.subject.resolve`).
+
+## Alternatives considered
+
+- Replace integer ids with subjects everywhere at once (big-bang): rejected, every consumer breaks together.
+- Let each service keep translating through the service that knows (Community → Live): rejected, it is the coupling this removes.
+- UUIDv4 ids: rejected in favour of ULIDs, which sort by creation time and keep indexes append-mostly.
+
+## Migration consequences
+
+Backfilled at Network boot (idempotent, time part from `created_at`). Live reports its links daily (`identity-legacy-sync`); `/internal/link-account` writes the map too. Rows created in the 2026-08-17..20 window may carry Network ids where Live ids were expected (see ADR-011 import).
+
+## Rollback
+
+Additive: removing `subject_id` columns and the map returns to the previous state; nothing reads subjects in place of integer ids where a legacy caller still needs the integer.
+
+## Acceptance tests
+
+`OpenVibe.Network/test/identity-subjects.test.js`: backfill validity and stability, legacy-map seeding, no repointing, problem+json errors, `subject_id` in tokens with `sub` unchanged. Production: resolve by Live id returns the Network account; unknown ids return 404 problem.
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+# ADR-002: Contract repository and compatibility policy
+
+**Status:** Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+)
+
+## Context and current evidence
+
+Cross-service contracts lived as prose in CONTRACTS.md and as copied code; nothing validated them.
+
+## Decision
+
+- `OpenVibe.Contracts` is the machine-readable authority: JSON Schema 2020-12 contracts with a catalog (id, semver, owner, visibility, compatibility, status), fixtures for every contract, generated TypeScript types, service/capability/namespace manifests.
+- A contract id is permanent; minor versions only add optional fields; a breaking change is a new major file plus a deprecation record with a replacement and window.
+- `scripts/compat.js` fails CI on breaking changes against the previous tag; services run `openvibe-contracts-check` in their CI.
+- Consumers pin a release tarball (`codeload.github.com/.../tar.gz/refs/tags/vX`), so production installs need no GitHub credentials.
+
+## Alternatives considered
+
+- Publish to the npm registry: deferred; tarball pins are immutable enough and need no registry account.
+- OpenAPI-first: rejected as the source of truth; OpenAPI fragments can be generated later from the schemas.
+
+## Migration consequences
+
+Contracts grow additively; each service moves to a new tag by bumping its pin.
+
+## Rollback
+
+A bad release is fixed forward (new patch tag); consumers can pin the previous tag.
+
+## Acceptance tests
+
+Contracts `npm test` (fixtures, catalog integrity, manifests cross-checked, compat gate), CI green on every tag; the compat gate was shown to fail on a deliberate breaking edit.
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+# ADR-003: Service/app principal authentication and capability grants
+
+**Status:** Accepted, implemented 2026-09-22
+
+## Context and current evidence
+
+First-party services authenticated to each other with one shared `X-Internal-Key` per deployment, with unrestricted reach.
+
+## Decision
+
+- Services obtain 5-minute RS256 tokens from Network's `/oauth/token` with `grant_type=client_credentials` and their existing OAuth client credentials. Claims follow `identity.service-token-claims@1`: `sub svc:<id>`, `aud`, `cap` (exact grants for that audience, optionally narrowed by `scope`), `ns`, `jti`.
+- Grants live in Network's `principal_grants` (revocable). Receivers check one capability per route (`requireCapability` / `principals.guard` / `tenantAuth({ capability })`), plus namespace and ownership (`app_id` must be the caller's own).
+- A request with a Bearer token is judged only on that token; a bad token is never rescued by a legacy key. New routes refuse the legacy key outright.
+- `principal_usage` counts every decision by caller, route and auth method; the key is retired per route once legacy use reaches zero.
+
+## Alternatives considered
+
+- mTLS between services: deferred; one host today and tokens give per-action scope that mTLS alone would not.
+- Long-lived per-service API keys: rejected, not scoped to actions and hard to rotate.
+
+## Migration consequences
+
+Guarded routes accept both during migration; callers fall back to the key only when no token can be had.
+
+## Rollback
+
+Remove a grant (new tokens stop carrying it) or stop sending tokens (callers fall back to the key).
+
+## 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.
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+# ADR-004: Durable event store, outbox/inbox and delivery semantics
+
+**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Events)
+
+## Context and current evidence
+
+Cross-service side effects are best-effort POSTs and HMAC webhooks; a consumer that is down misses them.
+
+## Decision
+
+- OpenVibe.Events persists every event (`events.event-envelope@1`) before any consumer sees it, then delivers to subscriptions with retries (exponential backoff), a dead-letter state, and replay by cursor.
+- Producers write events to a transactional outbox in the same database transaction as the domain change; a relay publishes them (at-least-once, idempotent by `event_id`).
+- Consumers get exactly-once *effects* through an inbox of idempotency receipts in their own database.
+- Storage starts on SQLite (single host, see ADR-007); Redis is not durable truth and is not used.
+
+## Alternatives considered
+
+- Kafka/NATS now: rejected, operational weight far beyond current volume.
+- Webhooks only: rejected, no durability or replay.
+
+## Migration consequences
+
+Migrate one existing family at a time (notifications, Media ready/failed, stream lifecycle), keeping the old direct call until the event path is proven, then deleting it.
+
+## Rollback
+
+Producers can switch back to their direct calls; events already stored stay replayable.
+
+## Acceptance tests
+
+Events tests: persistence before delivery, retry/backoff, DLQ, replay, crash-and-replay yields one effect, outbox commit/rollback, SSE visibility and resume.
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+# ADR-005: Realtime topics, cursor/resume and whether OpenVibe.Realtime exists
+
+**Status:** Accepted 2026-09-22: realtime runs inside OpenVibe.Events
+
+## Context and current evidence
+
+The charter for OpenVibe.Realtime was frozen pending this decision. Browser realtime today is five product-local WebSocket servers in Live.
+
+## Decision
+
+- Browser realtime is a delivery plane inside OpenVibe.Events (SSE `/realtime/stream`), authorised per topic and per event visibility (`public`, `subject`, `internal` never to browsers), with `Last-Event-ID` resume and an explicit `gap` message when history is gone.
+- `OpenVibe.Realtime` is **not** created now. Revisit only when measured connection counts or fan-out latency need an independently scaled process; the move is mechanical because the protocol is fixed here.
+- Media and game traffic never go through this plane.
+
+## Alternatives considered
+
+- Separate Realtime service now: rejected, adds a deploy unit with no measured need.
+- WebSockets instead of SSE: SSE suffices for server→browser fan-out and resumes natively; chat keeps its own WebSocket until Wave 6.
+
+## Migration consequences
+
+Live's product WebSockets stay until their domains move (Chat in Wave 6).
+
+## Rollback
+
+SSE is additive; disabling it leaves durable delivery untouched.
+
+## Acceptance tests
+
+Guessed private topic yields nothing; reconnect from a cursor gets the complete authorised sequence or a gap marker.
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
+# ADR-006: Media object, location and namespace model
+
+**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4)
+
+## Context and current evidence
+
+Media stored VODs, clips, files and paste screenshots in separate predecessor-shaped tables with a single mutable provider field; copies in local/B2/R2 were implicit.
+
+## Decision
+
+- `media_objects` (`med_<ULID>`) is the canonical record; VOD/clip/file rows become typed projections linked by `object_id`.
+- `media_locations` records every copy (local/B2/R2) with state and checksum; B2 stays canonical archive, R2 a promoted hot copy; promotion is analytics-gated policy in configuration, demotion only removes the promoted copy.
+- Namespaces are the `:app` tenants plus developer projects later; access by app key or service token with a namespace-scoped capability.
+- Retention holds (moderation, DMCA, creator pin, admin, evidence) block deletion and demotion.
+- The public object-size invariant is configuration (default max 500 MB, target 256, warn 384), validated and reported, not a copied constant.
+
+## Alternatives considered
+
+- Keep per-kind tables only: rejected, every new kind re-implements storage.
+- Full S3 wire compatibility first: deferred; a small native object API first, an S3 facade later if needed.
+
+## Migration consequences
+
+Backfill creates an object per existing row without moving bytes; reconciliation verifies locations before any state is trusted.
+
+## Rollback
+
+The object layer is additive; old routes keep working throughout.
+
+## 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.
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+# ADR-007: Logical data ownership and database technology
+
+**Status:** Accepted 2026-09-22
+
+## Context and current evidence
+
+Every service uses its own SQLite database on one host. The roadmap targets PostgreSQL as canonical relational truth, but forbids a big-bang migration.
+
+## Decision
+
+- **Ownership is the rule, the engine is not:** one authoritative writer per dataset; no service reads or writes another service's database; cross-domain joins happen in read models or analytics.
+- 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; the migration follows the rehearsal pipeline (snapshot, idempotent import, parity, runtime verification) and money paths use SERIALIZABLE transactions and row locks when they move.
+
+## Alternatives considered
+
+- Migrate everything to PostgreSQL now: rejected, no measured need and highest risk.
+- Shared database with schemas per service: rejected until PostgreSQL arrives; even then write roles are per service.
+
+## Migration consequences
+
+None immediately; each future migration carries its own plan.
+
+## Rollback
+
+n/a
+
+## Acceptance tests
+
+Wave 0 baseline shows no service reading another's database; `/ready` truthfulness checked per service in Wave 22.
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+# ADR-008: Shared package publication and release manifests
+
+**Status:** Accepted 2026-09-22; implementation in progress (OpenVibe.Shared)
+
+## Context and current evidence
+
+`openvibe-shared` lived in Network and was hand-rsynced into four repositories; copies had diverged (Live user-card.js; ov-icons.js in three consumers).
+
+## Decision
+
+- `OpenVibe.Shared` is the canonical package, released by semver tags and consumed by tarball pin, like openvibe-contracts.
+- Browser files are served by Network at `/shared/*` from its pinned package (and may be served locally by a site from `node_modules`), so a Network outage cannot blank another site's first paint beyond what the inline critical styles already cover.
+- No site edits a copy; vendored copies are removed.
+- Release manifests and the active-client update coordinator follow in Track R.
+
+## Alternatives considered
+
+- Keep vendoring with a sync script: rejected, drift already happened.
+- A CDN-only global script: rejected by anti-goal 7 (mutable unversioned runtime dependency).
+
+## Migration consequences
+
+Consumers move one at a time, smallest first (Community, Media, Tools, Sites, Live).
+
+## Rollback
+
+A consumer can pin the previous tag.
+
+## Acceptance tests
+
+No repository contains a vendored copy (CI drift check); navbar stays under its size budget.
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+# ADR-011: Community paste and comment ownership migration
+
+**Status:** Accepted and executed 2026-09-22
+
+## Context and current evidence
+
+Pastes were stored in Media (bytes and semantics) and written through Live, which translated identities; Community proxied everything through Live.
+
+## Decision
+
+- OpenVibe.Community owns pastes, versions, likes and comments in its own database; owners are Network subjects.
+- Media keeps screenshot bytes; new screenshots go to Media's token-only `community` tenant.
+- AI-derived pastes are origin `ai` and ownerless, linked to their stream (roadmap §33).
+- Live and the Tools gateway forward to Community; Media answers 410 to paste writes and 301s paste pages.
+
+## Alternatives considered
+
+- Keep pastes in Media and teach it subjects (option A): rejected, the semantics move to Community in Wave 5 anyway.
+
+## Migration consequences
+
+Export bundle (read-only, sha256, no IPs) → idempotent import with legacy maps and import-hold; owners in the 2026-08-17..20 window where both id spaces resolve differently are held (1 paste, 4 comments). Shadow parity: 878/878 public pastes identical before the flip. Incremental import after the flip caught 2 late pastes.
+
+## Rollback
+
+Set PASTES_AUTHORITY=live in Community and Live, remove Media's PASTES_FROZEN_APPS/PASTES_MOVED_TO; export Community-side writes first.
+
+## Acceptance tests
+
+Community test suite (store, API, importer, SSR), Live `pastes-community.test.js`, Media `pastes-moved.test.js`; production: all three entry points serve the same 880 pastes.
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+# Architecture decision records
+
+Platform-wide decisions from the OpenVibe development roadmap (section 10). Each record states the evidence, the decision, the alternatives, migration consequences, rollback and the tests that prove it (roadmap 23.4). A record is written before the code it governs; records for decisions taken earlier on 2026-09-22 were written the same day the code shipped.
+
+| ADR | Decision | Status |
+|---|---|---|
+| [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-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-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
+| [ADR-011](ADR-011-community-pastes.md) | Community paste and comment ownership migration | Accepted and executed 2026-09-22 |