Mirror of github.com/OpenVibers/OpenVibe.Reviews
  • JavaScript 95.1%
  • PLpgSQL 2.9%
  • CSS 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
OpenVibers 68bc6df4ce
openvibe-contracts v0.215.0 (was v0.214.1) (#109)
Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.215.0 release.
2026-10-27 03:58:55 -07:00
.github/workflows Reviews moves to PostgreSQL and Valkey (ADR-035, roadmap WS-X2, repository-pass group G1), async from end to end. 2026-09-28 08:03:01 -07:00
deploy nginx: GET /auth/me on the API zone (it runs on every page load; the sign-in flow's 10/min showed a visitor opening a dozen pages as signed out, the browser check's 429s) (#71) 2026-10-17 00:43:42 -07:00
docs OpenVibe.Reviews docs: public on its own domain since 2026-10-09 (#24) 2026-10-09 17:26:59 -07:00
migrations Landlords: NYC HPD's open housing-code violations per building (by class, with HPD Online's page) on a landlord page named by its address; README covers ratings from other sites and official records (#82) 2026-10-17 18:15:46 -07:00
public/css Phones: list grids size their columns with minmax(0, 1fr) (or a min(…, 100%) minimum), so one long line can never widen a page past the screen (#100) 2026-10-25 03:36:25 -07:00
scripts The SQLite era is gone (on PostgreSQL since 2026-09-28; no compatibility kept): the one-time migrate-to-postgres script, REVIEWS_DB_PATH/dbPath (config, .env.example, systemd unit, test helpers), the unused better-sqlite3 dependency and its lockfile entries, and docs, docstrings and STATUS lines about SQLite or its rollback window. Reviewed independently (VERDICT: SHIP). npm test 15/15 test files passed. 2026-09-28 20:14:16 -07:00
server The sitemaps' entity list is built once for concurrent readers, 8 entities at a time, and reused for 10 minutes in production: each /sitemap.xml fetch fired ~30,000 queries at once (#93) 2026-10-23 14:52:56 -07:00
test openvibe-shared v4.15.0 (was v4.14.0) (#105) 2026-10-26 19:47:45 -07:00
.env.example The sitemaps' entity list is built once for concurrent readers, 8 entities at a time, and reused for 10 minutes in production: each /sitemap.xml fetch fired ~30,000 queries at once (#93) 2026-10-23 14:52:56 -07:00
.gitignore Pin openvibe-contracts v0.96.0 and drop the proposed-capability fallback (#10) 2026-10-04 18:19:05 -07:00
CHANGELOG.md Reviews: pins current (contracts 0.107.0, shared 2.11.0, sdk 0.34.0). (#13) 2026-10-07 11:02:46 -07:00
LICENSE Charter: OpenVibe.Reviews as defined in the OpenVibe realignment plan (placeholder, no code yet) 2026-09-21 09:19:32 -07:00
package-lock.json openvibe-contracts v0.215.0 (was v0.214.1) (#109) 2026-10-27 03:58:55 -07:00
package.json openvibe-contracts v0.215.0 (was v0.214.1) (#109) 2026-10-27 03:58:55 -07:00
README.md openvibe-contracts v0.215.0 (was v0.214.1) (#109) 2026-10-27 03:58:55 -07:00
SECURITY.md SECURITY.md: how to report a vulnerability ([email protected], 7-day reply, scope, supported versions) 2026-09-23 19:27:46 -07:00
STATUS.json openvibe-contracts v0.215.0 (was v0.214.1) (#109) 2026-10-27 03:58:55 -07:00

OpenVibe.Reviews

Review signals gathered across sources with provenance, entity resolution and honest aggregates.

Status: alpha (roadmap Wave 17, Reviews half). Public at openvibe.reviews since 2026-10-09, after an independent pre-launch security review whose fixes shipped first. On 2026-10-18 it holds about 5,700 entities:

  • 19 Steam games with review signals from Sources;
  • 550 companies with CFPB complaint counts;
  • 1,155 NYC restaurants with their health inspections;
  • 4,000 NYC apartment buildings with HPD's open violations.

Members write their own reviews and save the ratings other sites show. Domain: openvibe.reviews · Port: 4830 · Service id: reviews Plan: OpenVibe End-to-End Realignment & Implementation Plan, revision 3 — roadmap Wave 17, §15.13, §29, §32. License: AGPL-3.0 (same as every OpenVibe service).

Purpose

Entity resolution plus the interpretation and publication layer for review signals. Reviews reads what sources stated (through OpenVibe.Sources), attributes each observation to the thing it is about, shows every number with where it came from and when it was read, and computes an aggregate only from the signals that exist. It never fabricates a rating and never presents AI output as user reviews: summaries are text written or approved by a person, every point cites the signals it rests on, and nothing a summary says can become a number.

The owner's April ask was review aggregation across Glassdoor, Yelp, BBB, Reddit and the rest, "subject to legal and technical feasibility". Their terms forbid copying their reviews, so /everywhere (server/reviews/everywhere.js) does what they allow: one name, its kind (a local business, an employer, a landlord, a company or online shop, a hosting provider, a product, software) and, for local places, where it is, open each site's own search in new tabs: Google Maps, Yelp, Tripadvisor, BBB, Trustpilot, Glassdoor, Indeed, Blind, LowEndTalk, RTINGS, YouTube reviews, Amazon, G2, Capterra, Product Hunt and Reddit. The URL forms were checked in a real browser on 2026-10-15 (WebHostingTalk, ApartmentRatings and Rate My Landlord have no search address that works). Reviews fetches and stores nothing from them; a result page is noindex.

Members' reviews

Any signed-in OpenVibe member reviews anything of the kinds Reviews lists, adding it first when it is not there yet. The kinds are employers, landlords, restaurants, local businesses, hosting providers, products, apps, games, vehicles, communities, platforms and more. A local kind needs where it is.

A review has 1 to 5 stars, a title, 30 to 5,000 characters, and how the member knows it (a current or former employee or tenant, a customer…). There is one review per member per entity, which they can rewrite. A member can add 20 things and write 30 reviews a day.

The entity page shows members' reviews as theirs: "Reviews from OpenVibe members", with their own average. They are never mixed into the sources' aggregate. The member or an editor removes a review (review_member_reviews, migrations/0004).

What other sites show, brought by members

A member can save the rating another site shows for something: Glassdoor, Indeed, Yelp, the BBB, Trustpilot, Google Maps or Tripadvisor. They do it at /save (the page's address, the rating, its scale, how many ratings) or with the OpenVibe Companion extension's "Save its rating to OpenVibe Reviews" button (0.5.0). That button reads the page's own structured data or star labels, only when pressed. The entity page shows the latest look per site, with the date and how many members saved it, beside that site's link (review_seen_ratings, migrations/0006). These are members' notes of what a page said, never signals and never in the aggregate. Capped at 60 a day per person; the person or an editor removes one.

Official records

What a public body published about an entity, read through OpenVibe.Sources (review_records, migrations/0007, 0008). Each register has its own section on the entity page, never as signals or in the aggregate:

Source Record Entity
cfpb-complaints-by-company (weekly, CC0) the CFPB complaint database's count for a company an organization; added at 500 complaints or more
nyc-restaurant-inspections (every 6 h) NYC Health's inspection results: grade, score, violations (critical marked) a restaurant per CAMIS id, named with its address
nyc-hpd-open-violations (weekly) NYC HPD's open housing-code violations by class (C immediately hazardous, B hazardous, A non-hazardous) a landlord page per building, named by its address; added at 10 or more open class C

A record finds its entity by its external alias (cfpb:, nyc-camis:, nyc-hpd:). Otherwise it uses a same-named entity of that kind and place that holds no record of the same register, otherwise it adds one. So two restaurants of a chain, or two buildings, are never one page. Search gets one line per register.

Owns

The nine authority tables of §15.13, in Reviews' own PostgreSQL database (ov_reviews on the host's data role, ADR-035; schema in migrations/):

Table What it holds
review_entities the things reviewed (kind, slug, state active/merged/deleted, merged_into)
review_entity_aliases names, URLs, GTIN/SKU/MPN, source bindings and external ids used for resolution (strong identifiers are unique)
review_sources the OpenVibe.Sources sources Reviews has seen: name, homepage, licence and terms notes, health
review_source_items Sources items Reviews read — signal fields only, never review text — with provenance and how each was resolved
review_signals typed observations (recommendation, recommendation_tally, rating, rating_aggregate) from one item revision, with provenance; values immutable (database triggers), status active/superseded/withdrawn
review_summaries one editorial summary per entity, its publication state and review flag
review_summary_revisions immutable summary revisions (openvibe-publishing/revisions) with authorship
review_trust_metadata editor trust decisions per source / signal / entity (exclude from the aggregate with a visible reason, limitation and verification notes), history kept
review_entity_links typed links between entities or to another service's EntityRef; a merge is a merged_into link (who, when, why), a split ends it

Helper tables: review_aggregates (every aggregate revision, immutable), review_member_reviews, review_seen_ratings, review_records (official records), review_summary_citations, review_corrections, review_audit (append-only editorial log), review_import_queue, review_sync_state, plus the Publishing packages' review_summary_reviews, review_summary_drafts, review_entity_redirects, review_discussion_refs, review_index_revisions and the SDK's review_event_outbox / review_event_inbox.

Does not own

  • discussion — OpenVibe.Community holds the threads and comments; Reviews stores the thread id and shows the comments as Community serves them
  • source adapter policy and ingestion — OpenVibe.Sources decides what is fetched, under which terms, and keeps the items
  • AI generation — OpenVibe.AI runs reviews.summarize_entity; Reviews only accepts its output as a draft
  • search — OpenVibe.Search indexes the documents Reviews sends

What works

Signals from Sources. Two paths feed one applyItem(): a pull of GET /api/v1/items?category=reviews&include_removed=1 in change order (cursor stored per page, a restart resumes) and the sources.item.* events through a signed webhook + inbox (created/updated items are queued and fetched by id; removals apply at once). An item becomes at most one signal: Steam appreviews items (review_signal, the Sources seed) give a recommendation from voted_up, with the reviewer facts Steam states (bought on Steam, received for free, playtime) as trust metadata; total_positive/total_reviews give a recommendation_tally; JSON-LD products give a rating_aggregate only when the page states a count; JSON-LD reviews give a rating. A rating whose source did not state the scale is recorded but left out of the aggregate with the reason shown. Each signal keeps the Sources item id and revision, the retrieval time, the source's publication time, the canonical URL and the licence note; a later revision of the item supersedes it, a removal withdraws it. A Sources outage changes nothing and invents nothing.

Resolution. Deterministic rules, strongest first: a source binding (every item of that source is about one entity), the item's URL (normalised), GTIN (normalised to 14 digits), SKU, MPN. One entity → resolved. Identifiers naming different entities → ambiguous. A name alone never resolves: it yields candidates for an editor. Nothing → unmatched, listed on the editor desk. Editors confirm (optionally remembering the identifier, which settles other waiting items), re-attribute or ignore; every decision is in the audit log.

Merge and split. A merge rewrites nothing: the merged entity keeps its aliases and its signals keep their attribution; it becomes merged with an active merged_into link and its page answers 301 to the target, whose aggregate spans both. A split ends the link and restores the pre-merge attribution exactly (tested as a round trip), recomputes both aggregates and flags the former target's summary if it cited signals that went back. Both are audited (link row, audit log, reviews.entity.merged|split).

Aggregates (method reviews-aggregate@1, a pure function in server/reviews/aggregate.js): recommendation share = positives ÷ total (a source's own tally replaces its single recommendations); rating = count-weighted mean of value ÷ best, as a percentage and on the shared scale when there is one. Editors can exclude a source or signal, with a reason shown on the page. The aggregate lists its inputs, exclusions and computation. With no qualifying signal it is absent — null, not zero, not a placeholder — and every change of inputs records a new aggregate revision (including the one where nothing is left).

Summaries. Editors write overview + pros + cons; every pro and con cites signals of the entity (uncited points, foreign or inactive signals, and any field that looks like a rating are refused). OpenVibe.AI's reviews.summarize_entity is a seam: POST …/summary/proposals stores a draft revision with ai authorship (workflow and run id, stub flag); it is never published by that call, stays off the page, feeds and Search, cannot be published until a person approves it, and AI text that states a rating ("4.5/5", "9 out of 10", star glyphs) is refused. When a cited signal is withdrawn or replaced, the summary is flagged on the page, the gate marks it unsupported_claims (noindex), and a pending revision (citations moved to replacements, unsupported points dropped) waits for an editor's approval.

Corrections. Signed-in people send corrections (entity, alias, signal, summary, aggregate, optional evidence URL) from the entity page or reviews.correction.submit; editors accept or reject them on the desk. What a request says and who sent it are never published; one person sends at most 20 a day and the same open request once (plus a per-address limit). A correction of a published summary is a new immutable summary revision carrying a public correction note (who: the editor who wrote it; when: its time), approved by that editor and published at once: accepting a request about an entity whose summary is published needs that note (the text is carried forward unless the editor changes it, from the desk or the edit page), and an editor can correct on their own with correction_note on a new revision. The entity page shows the corrected text, a "Corrected" notice and the summary's revision history with every correction note; earlier revisions stay readable at /e/:slug/summary/:n. Rejecting creates nothing. AI-drafted text a person corrected becomes AI-assisted (hybrid), never "written by a person". JSON-LD (dateModified), the feeds (a new entry that starts with the note) and the Search document follow the corrected revision.

Pages (SSR, useful without JavaScript). /, /about, /search, /e/:slug (aggregate with inputs and computation, summary with citations, signal table with provenance and timestamps, sources and limitations, discussion), /e/:slug.json, /e/:slug/history (aggregate revisions, summary revisions, merges and splits, every signal including withdrawn ones, editorial log), /e/:slug/summary/:n, /e/:slug/correct, and the editor desk (/editor, new entity, edit, summary form with signal pickers, merge/split, item resolution, trust decisions, corrections) — all plain form posts behind Network SSO. Old slugs 301, merged entities 301, deleted ones 410.

Structured data and discovery. JSON-LD Product / VideoGame / SoftwareApplication / … with AggregateRating, and a Review for a summary, are emitted only when the aggregate exists (i.e. real signals back it); without signals the page carries breadcrumbs only (tested). A recommendation share is expressed as ratingValue 0–100 with bestRating: 100 and the real count; a summary's Review never has a reviewRating. The openvibe-publishing v1.3.0 gate decides robots per entity (no live signal → unsourced, summary under the word minimum → thin, unsupported points → unsupported_claims, unreviewed AI → hidden); sitemaps list indexable entities only, Atom/JSON feeds list published summaries, robots.txt and llms.txt are served, and Search receives reviews.index_document.upserted|deleted with Sources items as provenance. With INDEXNOW_KEY set, publishing, editing, unpublishing or deleting an indexable entity page tells IndexNow (the key file is served at /<key>.txt; unset, nothing is sent, and drafts and noindex pages never ping).

Operations. /api/health, /api/ready (DB required; Network key, Sources, Events relay, webhook secret, Community and editors reported), /release.json, /metrics (outbox and backlog gauges).

Telemetry. Each HTTP request is aggregated by route template, method and status class, never by raw URL; /shared files, static assets, health, readiness and /metrics are not counted. Once a minute one http.request sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this service's own telemetry_samples table (openvibe-sdk 0.46 telemetry, the platform.telemetry-sample@1 shape); rows older than 14 days are deleted daily. start() in server/index.js builds the collector and, with the workers on (production), flushes it every minute and runs the 14-day retention; server/app.js mounts its middleware; the table is created by migration migrations/0003_telemetry.sql.

Routes

Path What
GET /, /about, /search?q= home (entities with source data), method, search
GET /e/:slug, /e/:slug.json, /e/:slug/history, /e/:slug/summary/:n entity page, JSON, history, one summary revision
`GET POST /e/:slug/correct, POST /e/:slug/discuss`
/editor, /editor/entities/new, /e/:slug/edit, /editor/items/:id, … editor desk (editors only)
/robots.txt, /sitemap.xml, /sitemaps/*.xml, /feed.atom, /feed.json, /llms.txt discovery
POST /internal/events OpenVibe.Events deliveries (sources.item.*), HMAC-signed; not proxied publicly
/auth/* Network SSO (client reviews)

Sitemap entries carry lastmod from entity updates, the newest entity on the home page, or the release commit for /about.

API /api/v1

People use their Network JWT; services use a client-credentials token for audience openvibe.reviews and act for the person in X-OV-Subject. One capability per route; editorial writes need an editor who is a person (staff or REVIEWS_EDITORS). Errors are RFC 9457 problem+json. A write carried by the ov_token cookie that another site started is 403 request.cross_site (Bearer tokens are not affected).

Capability Routes
reviews.entity.resolve POST /resolve, GET /entities, GET /entities/:ref, /history, /aggregate, /summary/revisions/:n, GET /items, GET /items/:id, POST /items/:id/resolution
reviews.entity.manage `POST
reviews.entity.merge / reviews.entity.split POST /entities/:ref/merge { into, note }, POST /entities/:ref/split { note }
reviews.signal.import POST /signals/import { source_item_id }, POST /sources/sync
reviews.summary.propose POST /entities/:ref/summary/proposals (OpenVibe.AI)
reviews.summary.publish POST /entities/:ref/summary/revisions (correction_note, correction_id: a correction, published at once), …/revisions/:n/review, …/summary/publish, …/summary/unpublish
reviews.correction.submit POST /entities/:ref/corrections

The ids and the service manifest are released in openvibe-contracts (pinned at v0.107.0; the source proposals stay in docs/capabilities-proposal/ and docs/service-manifest-proposal.json); server/auth/capabilities.js decides them with the contracts grant rule. reviews.entity.manage and reviews.summary.propose are additions to the §15.13 minimum list.

Per-actor limits

/api/v1 and the forms also limit who calls them, once req.actor is resolved and a route's capability guard passed, before any work (for a form, before its body is read): server/http/actor-limits.js, openvibe-sdk/limits, roadmap WS-R task 4. Counted: a person as user:usr_… (their own token or cookie, named by a first-party service in X-OV-Subject, or an app's on_behalf_of); a service or app acting as itself (OpenVibe.AI's proposals, an import service) by its principal; a signed-out caller by address, on writes only. Signed-out reads keep only the per-address limit (many readers share a carrier or campus address), and a first-party service reading for itself is not counted on reads. Past a limit: 429 problem+json rate_limited with Retry-After, one [Limits] log line and reviews_rate_limited_total{limit,window}. A form and the API route that do the same thing share one budget; a form counts only a signed-in person.

Routes (API and form) Per caller, a minute / an hour
Reads, and POST /resolve (a lookup) REVIEWS_LIMITS_MINUTE / REVIEWS_LIMITS_HOUR (120 / 3000)
Entity create, edit, delete, aliases, links, trust (/editor/entities/new, /e/:slug/edit) 30 / 300
Merge and split 10 / 100
Item resolution (POST /items/:id/resolution, /editor/items/:id) 60 / 1200
POST /signals/import 60 / 1200
Sync now (POST /sources/sync, /editor/sync) 2 / 20
Summary write, review, publish, unpublish 30 / 300
Summary proposals (OpenVibe.AI) 60 / 1200
Correction submit (POST …/corrections, /e/:slug/correct) 5 / 20
Correction resolve (PATCH /corrections/:id, /editor/corrections/:id) 30 / 300
Discussion comment (/e/:slug/discuss, sent to Community) 20 / 300

The per-address limits (API 300 a minute, corrections 20 an hour, forms, sign-in) stay. Never limited per actor: /api/health, /api/ready, /release.json, /metrics, sign-in, the pages people read, and the signed Events deliveries at /internal/events. test/actor-limits.test.js; the other tests boot with rateLimits: false, which turns off both kinds.

Events

Produced (transactional outbox → OpenVibe.Events when EVENTS_URL is set): reviews.entity.merged, reviews.entity.split, reviews.signal.added (with replaces), reviews.signal.removed (status, reason, replaced_by), reviews.summary.published|updated|unpublished (a correction is reviews.summary.updated with correction: { note, corrects }; a dedicated reviews.summary.corrected would first need registering in OpenVibe.Contracts), and the Search index events reviews.index_document.upserted|deleted. Consumed: sources.item.created|updated|removed (subscription created with scripts/subscribe.js).

Running it

fnm exec --using=22.22.1 npm install
cp .env.example .env
fnm exec --using=22.22.1 npm run dev      # http://127.0.0.1:4830
fnm exec --using=22.22.1 npm test         # every test/*.test.js on temp databases with stub upstreams

Production: /opt/openvibe.reviews, env /etc/openvibe/reviews.env, unit deploy/systemd/openvibe-reviews.service, database ov_reviews on PostgreSQL (ADR-035), nginx deploy/nginx/openvibe.reviews.conf. There is no seed: a fresh database holds no entity, signal, summary or rating. Entities are created by editors; signals arrive only from Sources.

Depends on

  • OpenVibe.Network — SSO (OAuth client reviews), signing key, service principal svc:reviews
  • OpenVibe.Sources — review items (sources.item.read, sources.source.read); its reviews seed (Steam appreviews for Portal 2) is disabled until a person verifies its terms, so until then Reviews has nothing to show
  • OpenVibe.Events — outbound events (events.event.publish) and the sources.item.* subscription
  • OpenVibe.Community — discussion threads (community.comment.write)
  • OpenVibe.AI — optional, proposes summaries through reviews.summary.propose
  • OpenVibe.Search — consumes the index events
  • PostgreSQL 18 and Valkey 9 (OpenVibe.Host roles/data/, ADR-035): every read and write is async through openvibe-sdk/db; Valkey holds the per-actor limit counters (optional)
  • packages: openvibe-publishing v1.3.0, openvibe-contracts v0.107.0, openvibe-shared v4.15.2, openvibe-sdk v0.65.0

Capabilities

Implemented here (the service manifest's capabilities, audience openvibe.reviews, one per route; routes under API): reviews.entity.resolve, reviews.entity.manage, reviews.entity.merge, reviews.entity.split, reviews.signal.import, reviews.summary.propose, reviews.summary.publish and reviews.correction.submit.

Called elsewhere, as the service principal svc:reviews (client credentials, one token per audience):

Service Grant Why
OpenVibe.Sources sources.item.read, sources.source.read review items in change order, source names and terms
OpenVibe.Events events.event.publish; events.subscription.manage once (scripts/subscribe.js) outbound events; the sources.item.* subscription
OpenVibe.Community community.comment.write discussion threads on entity pages

Other services call Reviews with reviews.summary.propose (OpenVibe.AI) or reviews.signal.import.

Acceptance (tested in test/)

  • no aggregate without signals: null, no aggregate row, and it disappears again (as a recorded revision) when the last signal is withdrawn (signals.test.js, aggregate.test.js)
  • AggregateRating JSON-LD absent without signals, present with the real value and count while signals back it, gone after removal (nojs.test.js)
  • merge → split round trip exact: attribution, aliases and aggregates restored; merge/split audited and evented (merge.test.js)
  • source removal changes the next aggregate (webhook and pull paths); an item update supersedes its signal (signals.test.js)
  • AI text never yields a rating: rating fields and rating text refused; an AI draft is never published, shown, fed or indexed before a person approves it (summaries.test.js)
  • provenance on every signal, enforced by the schema, immutable by trigger; review text never stored (signals.test.js)
  • no-JS: every public page and every editor workflow works with plain HTML and form posts; Community comments are shown, never copied (nojs.test.js)
  • a withdrawn cited signal flags the summary and prepares a pending revision that only a person can publish (summaries.test.js)
  • a correction yields a public revision: accepting a request (or an editor correcting on their own) creates a new immutable revision with the correction note, the page shows the corrected text and the revision history with the note without JavaScript, the old revision stays readable, Search/JSON-LD/feeds/events follow it, the request's text and sender never reach a public surface, a rejection creates nothing, and only an editor who is a person decides (corrections.test.js)
  • deterministic resolution; a name alone never resolves; permissions and capabilities enforced (resolution.test.js)
  • the threat review's fixes: cross-site writes refused on pages and on the cookie-authenticated API, the correction queue's per-person allowance, bounded audit text, editor ids hidden from readers, ratings in words refused in AI text, out-of-scale source ratings not counted (threat.test.js; see docs/threat-review.md)
  • every event validates as events.event-envelope@1 and every index document as search.index-document@1; proposals validate against the contracts schemas; nothing seeded (signals.test.js, proposals.test.js)

Demonstrated on production (2026-10-15): the deployed Sources runs 19 enabled review sources, and their items reach Reviews through the deployed Events sources.item.* subscription (1,187 signals stored).

Not yet demonstrated: a real OpenVibe.AI reviews.summarize_entity run, because entities wait on an editor and no summary exists yet.

Security

Reporting a vulnerability: SECURITY.md. The authors' threat review, with file:line references and the gaps that remain: docs/threat-review.md.

  • Auth. Network JWTs (RS256, verified offline) for people; client-credentials service tokens for audience openvibe.reviews, one capability per route, acting for X-OV-Subject. Editorial writes need an editor who is a person (staff or REVIEWS_EDITORS). A cookie-authenticated write started by another site is 403 request.cross_site.
  • Private data. Review text is never stored; a correction's text and sender never reach a public surface; editor ids are hidden from readers.
  • Honesty. No aggregate without signals; AI text never yields a rating and is never shown before a person approves it.
  • Egress. Reviews calls only its configured Network, Sources, Events and Community hosts; it never fetches a URL a user chose.
  • Secrets. OV_OAUTH_CLIENT_SECRET and REVIEWS_EVENTS_SECRET (Events delivery signatures) live in /etc/openvibe/reviews.env (0600). nginx answers /metrics and /internal/ with 404.

Deploy

Production deploys with sudo ovhost deploy reviews on the host (strategy git-checkout: fetch, fast-forward /opt/openvibe.reviews, install on a lockfile change, restart, wait for /api/ready). The unit is openvibe-reviews.service on 127.0.0.1:4830, the env file /etc/openvibe/reviews.env. The database is ov_reviews on the host's data role (sudo /opt/openvibe.host/roles/data/add-service.sh reviews writes its settings); the release migrates it at boot (schema in migrations/). The nginx reference is deploy/nginx/openvibe.reviews.conf (the domain has served this service since 2026-10-09). Rollback: ovhost puts the previous sha back by itself when /api/ready does not answer 2xx after the restart; afterwards sudo ovhost rollback reviews --to <sha>. Migrations only add tables and columns.

Launch rule

This repository never made the product real on its own: openvibe.reviews kept its placeholder page on OpenVibers/OpenVibe.Sites until all of the following held (plan §12.12):

  1. an owning runtime with health/readiness endpoints and observability — built (/api/health, /api/ready, /metrics);
  2. canonical identity/auth integration (Network subjects, a scoped service principal) — built and provisioned in production;
  3. server-rendered public routes useful without JavaScript — built;
  4. real persistence and end-to-end workflows — built and deployed on the host (empty database); no Sources review source is enabled yet;
  5. capability and event registration against OpenVibe.Contracts — done (openvibe-contracts v0.107.0);
  6. a migration/seed strategy (schema migrations in migrations/; nothing seeded), a security/threat review, sitemap/robots/feed behaviour — discovery built; threat review written: docs/threat-review.md (the service authors' own, from the code: controls with file:line references, the gaps fixed in that pass and the ones that remain); an independent pre-launch security review followed, its fixes shipped first;
  7. acceptance tests proving the advertised functionality — built (npm test).

The launch release removed openvibe.reviews from OpenVibe.Sites/sites.json, switched routing to this service and registered its maturity in the ecosystem registry in the same release it went live: the domain has served this service since 2026-10-09, when plan T11 deleted OpenVibe.Sites. A placeholder is never counted as an implemented service.


Part of the OpenVibe network. Built in the open by OpenVibers.

  • openvibe-contracts: v0.215.0
  • openvibe-sdk: v0.65.0
  • openvibe-shared: v4.15.2
  • openvibe-publishing: v1.3.0