openvibers/openvibe.events AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Events |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | The durable event backbone of the network: durable events, subscriptions, signed delivery, retry, dead |
| 6 | letters and replay, plus the Realtime SSE plane (ADR-005 put Realtime inside Events). Every authoritative |
| 7 | service writes its domain change and its outbox row in one transaction; a relay publishes here; consumers |
| 8 | use inbox/idempotency receipts; browsers get authorized, resumable projections. Node.js/Express, |
| 9 | PostgreSQL (`ov_events`), CommonJS, **no build step**. |
| 10 | |
| 11 | - Service id `events`, port `4300`, origin `https://openvibe.events` (API and product origin), unit |
| 12 | `openvibe-events.service`, release root `/opt/openvibe.events`. The old `events.openvibe.network` was |
| 13 | retired on 2026-10-10. |
| 14 | - `package.json` `main` is [lib/client.js](lib/client.js) — the client library services import — while |
| 15 | `npm start` runs [server/index.js](server/index.js). |
| 16 | |
| 17 | ## What it owns |
| 18 | |
| 19 | - Tables: `events`, `events_archive` (the replay tier), `subscriptions`, `deliveries`, |
| 20 | `consumer_checkpoints`, `idempotency_receipts`, `app_revocations`, `app_usage`, `billing_readings`, |
| 21 | `billing_periods`, `telemetry_samples`; the realtime fan-out is in-process (one Events process). |
| 22 | - The canonical event envelope (event_id, trace_id, type, version, source, actor, subject + revision, |
| 23 | payload), visibility (`public|internal|subject`), priority classes `critical|important|low`, loop |
| 24 | guards, backpressure, the DLQ and replay. |
| 25 | - Developer-app event scope, sandbox separation and per-project quotas (ADR-014). |
| 26 | - Realtime: SSE topics, `Last-Event-ID`/cursor resume and gap detection (WebSocket and presence are not |
| 27 | built). |
| 28 | - The resource index (`/api/v1/resources`), the fabric (carriers, planner, explain), the home page and its |
| 29 | stats, and the developer limits (`/limits.json`). |
| 30 | - Retention: three tiers (hot window, archive/replay, then deletion), checked at boot and in the config. |
| 31 | |
| 32 | Not owned: the domain events themselves (each service), identity (Network), the consumers' inboxes, the |
| 33 | service that decides what is private (Events enforces visibility, it does not invent it). |
| 34 | |
| 35 | ## Who calls it, and whom it calls |
| 36 | |
| 37 | - Callers: every service with a Network client-credentials token audience `openvibe.events`; developer |
| 38 | apps; browsers on the realtime stream with a ticket/cookie/Bearer; services reading the resource index. |
| 39 | - Depends on: **OpenVibe.Contracts** (`events.event-envelope@1`, service tokens, problem details, ids) |
| 40 | and **OpenVibe.Network** (the JWKS signing key and user subject ids for topic authorization). No |
| 41 | product service is required for delivery. |
| 42 | |
| 43 | ## Repository map |
| 44 | |
| 45 | - **Entry:** [server/index.js](server/index.js) — store, keys, config, worker and realtime; graceful stop. |
| 46 | - **App:** [server/app.js](server/app.js) — `/auth`, `/shared`, the `/api` routers, realtime, health/ |
| 47 | readiness, metrics, home, discovery, legal and release files. |
| 48 | - **API:** [server/api/](server/api/) — `publish.js`, `subscriptions.js`, `read.js`, `policies.js`, |
| 49 | `placement.js`, `resources.js`, `resource-control.js`. |
| 50 | - **Fabric:** [server/fabric/](server/fabric/) — `planner.js`, `signals.js`, `policy.js`, `nats-core.js`, |
| 51 | `rate-cards.json`. |
| 52 | - **Delivery/store:** [server/egress.js](server/egress.js), the store/worker modules, `server/realtime.js`, |
| 53 | [server/home.js](server/home.js), [server/home-stats.js](server/home-stats.js), |
| 54 | [server/discovery.js](server/discovery.js), [server/openapi.js](server/openapi.js). |
| 55 | - **Client library:** [lib/client.js](lib/client.js) (also shipped to services through the SDK's events |
| 56 | client); [server/endpoints.js](server/endpoints.js), [server/limits.js](server/limits.js). |
| 57 | - **Migrations:** [migrations/](migrations/) (events, subscriptions, deliveries, checkpoints, receipts, |
| 58 | app usage/billing, telemetry). |
| 59 | - **Deploy:** [deploy/systemd/openvibe-events.service](deploy/systemd/openvibe-events.service), |
| 60 | [deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf). |
| 61 | |
| 62 | ## Commands |
| 63 | |
| 64 | ```bash |
| 65 | npm ci |
| 66 | cp .env.example .env |
| 67 | npm run dev # http://127.0.0.1:4300 |
| 68 | npm start # node server/index.js |
| 69 | npm test # test/run.js: every file in its own process, temp databases |
| 70 | npm run test:pg # same on PostgreSQL (EVENTS_TEST_STORE=pg) |
| 71 | npm test -- delivery # filter by file name; --strict makes a skip fail |
| 72 | ov test # the harness's sharded run of what the change reaches |
| 73 | ``` |
| 74 | |
| 75 | Node 22 in production (`fnm exec --using=22.22.1 npm test`). |
| 76 | |
| 77 | ## Database and migrations |
| 78 | |
| 79 | `ov_events` on the host's data role (ADR-035); temp/PGlite databases in tests. Async through |
| 80 | `openvibe-sdk/db`; migrations `NNNN_*.sql` applied at boot; never edit an applied one, add a new number. |
| 81 | The event and archive tables are append-only; a delivery is claimed/updated, an event is not rewritten |
| 82 | except for a redaction tombstone. The delivery worker is the only writer of `deliveries` state; a replay |
| 83 | never mutates the original event. |
| 84 | |
| 85 | ## Auth and capabilities |
| 86 | |
| 87 | - Services use a Network client-credentials token (audience `openvibe.events`), verified offline against |
| 88 | the JWKS with `openvibe-sdk/auth` `verifyServiceToken` and the pinned contracts rules. People use |
| 89 | `verifyUserToken`, which refuses service principals and typed tokens. |
| 90 | - Capabilities: `events.event.publish`, `events.subscription.manage` (own subscriptions only), |
| 91 | `events.event.read` (+ checkpoints/realtime), `events.delivery.admin`, `events.resource.read` |
| 92 | (`first-party`). Developer apps get the public app capabilities on the same routes; a sandbox token is |
| 93 | accepted **only** on developer-app routes (`401 token.sandbox_refused` elsewhere); an app token is never |
| 94 | judged on a first-party capability. |
| 95 | - Realtime accepts a **single-use Network ticket**, the `ov_token` cookie, a Bearer user JWT or a service |
| 96 | token with `events.event.read`; a ticket is never a session and is never logged. |
| 97 | - `/api/v1/deliveries` stays host-local; `/metrics` answers direct loopback callers only; nginx refuses |
| 98 | both paths publicly. |
| 99 | |
| 100 | ## Frontend loading model |
| 101 | |
| 102 | The only page is the server-rendered home (`server/home.js`): the bus at work from this service's own |
| 103 | tables (counts and names only — never a payload, actor, subject id or principal), with a clear empty state |
| 104 | and a 1.5 s bound; `/` is HTML by default, and the text API index answers `Accept: text/plain` or |
| 105 | `?format=text`. Discovery (`robots.txt`, `sitemap.xml`, `llms.txt`) and the JSON-LD `ItemList`/`FAQPage` |
| 106 | come from the same data. There is no SPA, bundler or transpiler; `openvibe-shared` supplies the Frame and |
| 107 | `/shared/*` from the pinned node_modules. The realtime stream is SSE at `/realtime/stream`. |
| 108 | |
| 109 | ## Deploy |
| 110 | |
| 111 | `sudo ovhost deploy events` (git-checkout: fetch, fast-forward `/opt/openvibe.events`, install on a |
| 112 | lockfile change, restart, wait for `/api/ready`). Unit `openvibe-events.service` on |
| 113 | `127.0.0.1:4300`, env `/etc/openvibe/events.env`; nginx |
| 114 | [deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf) exposes the home/discovery, |
| 115 | `/realtime/stream`, health and the token-guarded developer API; `/api/v1/deliveries` stays host-local. |
| 116 | ovhost treats open realtime connections as a report-only drain (`--wait-idle` waits for them). Rollback |
| 117 | `sudo ovhost rollback events --to <sha>`; migrations only add tables and columns. `EVENTS_LIMITS=off` |
| 118 | turns the per-actor limits off without a deploy. |
| 119 | |
| 120 | ## Tests |
| 121 | |
| 122 | `npm test` runs each `test/*.test.js` in its own process with temp databases and in-process Network keys; |
| 123 | `npm run test:pg` runs the same on PostgreSQL. The suite covers publish/envelope/validation, visibility and |
| 124 | redaction, subscriptions and endpoint egress rules, signed v2 delivery and the replay window, retries/ |
| 125 | backoff, DLQ and replay, checkpoints/cursors, developer apps and sandbox separation, realtime resume/gap, |
| 126 | the resource index and the home stats bounds. `ov test` runs the same. |
| 127 | |
| 128 | ## Conventions |
| 129 | |
| 130 | - CommonJS, 4-space indent, single quotes, semicolons; factories with injectable config/store/clock/fetch/ |
| 131 | log; `openvibe-contracts` shapes for the envelope, errors and ids. |
| 132 | - Errors are RFC 9457 problem+json with stable codes. |
| 133 | - Priority is data (`critical|important|low`), not a new queue; retries use exponential backoff; a |
| 134 | delivery attempt is idempotent by delivery id. |
| 135 | - Sign with **v2 only** (`X-OpenVibe-Signature-V2`); a subscription secret is shown once. |
| 136 | |
| 137 | ## Pitfalls |
| 138 | |
| 139 | - **`internal` events never reach a browser; `subject` events reach only that person.** A guessed topic |
| 140 | returns nothing; do not "helpfully" widen a topic. |
| 141 | - **Redacted events replay as tombstones**, never as the original payload. |
| 142 | - **Endpoints are allow-listed:** first-party subscriptions must be `http(s)` on `127.0.0.1` or an |
| 143 | OpenVibe host (`EVENTS_ENDPOINT_HOSTS`); app webhooks must be public https, checked at creation and at |
| 144 | every delivery's DNS lookup; **redirects are never followed**. |
| 145 | - **A sandbox token is refused everywhere but the developer-app routes**; an app token is never judged on |
| 146 | a first-party capability. |
| 147 | - **The delivery worker is the only mutator of delivery state**; a replay is a new delivery, not an edit. |
| 148 | - **`/api/v1/deliveries` is host-local and `/metrics` is loopback-only.** Do not add them to the vhost. |
| 149 | - **The home page leaves only counts and names.** No payload, actor, subject id or principal in the stats. |
| 150 | - **Don't wait on the database for the shell:** the stats read has a 1.5 s bound and a clear empty state; |
| 151 | a slow read must not take the page down. |
| 152 | - **One Events process owns the realtime fan-out.** A second process needs a shared fan-out design first. |
| 153 | |
| 154 | ## Documentation |
| 155 | |
| 156 | - [README.md](README.md) — purpose, run, auth/capabilities, publishing/redaction, subscriptions/delivery, |
| 157 | retention, resource index, fabric, developer apps, client library, realtime, security, deploy. |
| 158 | - [docs/](docs/) — delivery, subscriptions, realtime and operations notes. |
| 159 | - `lib/client.js` is the reference client; `openvibe-sdk/events` is the supported one. |
| 160 |