- JavaScript 95.1%
- PLpgSQL 2.9%
- CSS 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| deploy | ||
| docs | ||
| migrations | ||
| public/css | ||
| scripts | ||
| server | ||
| test | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| STATUS.json | ||
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.
Every review site, one search
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 principalsvc: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 thesources.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 throughopenvibe-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@1and every index document assearch.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 forX-OV-Subject. Editorial writes need an editor who is a person (staff orREVIEWS_EDITORS). A cookie-authenticated write started by another site is 403request.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_SECRETandREVIEWS_EVENTS_SECRET(Events delivery signatures) live in/etc/openvibe/reviews.env(0600). nginx answers/metricsand/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):
- an owning runtime with health/readiness endpoints and observability — built (
/api/health,/api/ready,/metrics); - canonical identity/auth integration (Network subjects, a scoped service principal) — built and provisioned in production;
- server-rendered public routes useful without JavaScript — built;
- real persistence and end-to-end workflows — built and deployed on the host (empty database); no Sources review source is enabled yet;
- capability and event registration against OpenVibe.Contracts — done (openvibe-contracts v0.107.0);
- 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;
- 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