openvibers/openvibe.community AGENTS.md
AGENTS.md
AGENTS.md — OpenVibe.Community
Project overview
The OpenVibe community home: pastes (the only authority for them since 2026-09-22), the forum
(spaces, threads, posts, votes), the typed comment threads every other product embeds, Pulse
(the network's public activity), submissions, the Discord relay, and members-only gates through
VIP. A small Node.js/Express app, CommonJS, no frontend framework, one PostgreSQL database
(ov_community), server-rendered pages with a little progressive JavaScript.
- Origin
https://openvibe.community, port4200, unitopenvibe-community.service, release root
/opt/openvibe.community. - The forum moved back here from Space on 2026-10-08; Space redirects its old forum URLs here.
Space remains the code-hosting / "spaces" product.
What it owns
- Pastes, their versions, likes, visibility (public/unlisted/private/burn), locked pastes, spam and
image review, and the/api/pastes/*native API. Pages, raw text, screenshots and downloads read
from the paste store — never from Media (Media redirects its old paste URLs here). - The typed comment threads every product embeds (
commentswith a type + ref, not a copy of the
product's content; the product stores only the thread id). - The forum: spaces, groups, threads, posts, votes, categories, pinned starters, the Roadmap space
(docs/roadmap/public.json), a space's Chat room, members-only spaces/threads (VIP). - Pulse, the network's public activity; submissions and their review queue; the Discord relay.
community.*events, Search documents for public threads, thecommunity.profileuser module.ov_community(host data role, ADR-035), Valkey for per-actor limits.
Not owned: identity/SSO (Network), file bytes (Media), memberships (VIP), chat rooms (Chat), the
contents of products whose threads it hosts.
Who calls it, and whom it calls
- Callers: people in the browser; Live (old VOD/clip comment imports), every product embedding a
comment thread, Chat/Space linking a forum thread, and services with a Network service token
audienceopenvibe.communityand the route's capability. - Depends on: Network (SSO, JWKS, service tokens,
identity.subject.resolve, the profile module);
Media (screenshots/attachments; metadata stripped before upload); VIP (members-only gates,
fails closed); Chat (a space's room); Events (outbox and the Pulse/account subscriptions);
Vision (OV_VISION_URL, unsafe-image review; unset means no image review); Search for public
thread documents;openvibe-contracts,openvibe-sdkandopenvibe-sharedpins.
Repository map
- Entry: [server/index.js](server/index.js) — database/migrations first, app, Discord relay, Events,
Roadmap and starter sync; graceful stop. - App: [server/app.js](server/app.js) — helmet/CSP,
/auth,/api/pastes,/api/v1/*(comments,
spaces, posts, pulse, feed, submissions, profiles, relay, resources),/shared,/internal/events,
server-rendered pages. - Config/db: [server/config.js](server/config.js), [server/db.js](server/db.js).
- Pastes: [server/pastes/](server/pastes/) —
api.js,service.js,store.js,catalog.js,
review.js,content-hash.js,spam.js. - Comments: [server/comments/](server/comments/) —
service.js,api.js,store.js,routes.js. - Forum: [server/forum/](server/forum/) —
service.js,api.js,routes.js,roadmap.js,
starters.js. - Pulse/submissions/relay: [server/pulse/](server/pulse/), [server/submissions/](server/submissions/),
[server/relay/](server/relay/). - Identity/auth: [server/identity/](server/identity/), [server/auth/](server/auth/),
[server/vip/](server/vip/). - Render: [server/render/](server/render/) — pages, comments, forum, feed, pulse, markdown,
highlight, JSON-LD. - Browser: [public/js/community.js](public/js/community.js),
inline-comments.js,composer.js;
CSS in [public/css/](public/css/); assets in [public/assets/](public/assets/). - Deploy: [deploy/systemd/openvibe-community.service](deploy/systemd/openvibe-community.service),
[deploy/nginx/openvibe.community.conf](deploy/nginx/openvibe.community.conf).
Commands
npm ci
npm run dev # NODE_ENV=development node server/index.js
npm start # node server/index.js, http://127.0.0.1:4200
npm test # test/run.js, PGlite, mock Live/Network
npm run test:pg # same suite on PostgreSQL (COMMUNITY_TEST_STORE=pg)
npm test -- seo proxy # filter by file name; --strict makes a skip fail
npm run n-1:record # record the N-1 fixtures after a deploy
ov test # the harness's sharded run of what the change reaches
Database and migrations
ov_community on the host's data role (ADR-035); embedded PGlite in development/tests. Async through
openvibe-sdk/db. Migrations are migrations/NNNN_*.sql, applied at boot; the schema is created
idempotently in every mode (comments, forum, Pulse and the relay live here whichever service owns
pastes), and three seed spaces are inserted. Never edit a migration that has run; add a new number.
A rollback is safe because migrations only add tables and columns.
Auth, identity and comments
- People are Network subjects (
usr_…,gst_…), never service-local integers. A browser's owner
is the JWT'ssubject_id(older tokens resolved once through Network and cached inlegacy_id_map);
names/avatars come fromsubject_projection, refreshed through the Network's resolve-batch. - Identity is never read from bodies or queries; a token issued before
token_valid_afteris
refused.openvibe-sdk/ssoowns the cookie flow. - Service callers need a service token audience
openvibe.communityand the route's capability
(community.paste.create|write|moderate, the comment/forum equivalents, …). AnX-OV-Subjectscopes
a first-party service call. - Comments are typed: a product passes its own ref and gets a thread id; it never stores comment text.
- Members-only spaces/threads go through VIP and fail closed; locked pastes are sealed and never
readable from the text path ([docs/locked-pastes.md](docs/locked-pastes.md)).
Frontend loading model
Every page is server-rendered — crawlers and no-JS readers get the whole thing — with small no-build
scripts for pagination, copy buttons, the upload path and the inline composer. There is no SPA,
bundler or transpiler. openvibe-shared supplies the Frame, the share sheet and /shared/* from the
pinned node_modules (including /shared/ov-glyphs.js). Icons are OpenVibe glyphs and brand logos,
never Font Awesome (see the pitfall). Markdown is rendered through the local renderer, which escapes
first and only links http(s).
Deploy
sudo ovhost deploy community (git-checkout: fetch, fast-forward /opt/openvibe.community, install
on a lockfile change, restart, wait for /api/ready). Unit openvibe-community.service on
127.0.0.1:4200, env /etc/openvibe/community.env (0600), vhost
[deploy/nginx/openvibe.community.conf](deploy/nginx/openvibe.community.conf). Database ov_community
on the data role (add-service.sh community). Record the N-1 fixtures after a deploy
(npm run n-1:record). Rollback sudo ovhost rollback community --to <sha>.
Tests
npm test runs each test/*.test.js in its own process through openvibe-shared/test-runner
(60 s/file) with in-process Live/Network mocks. Notable guards: the no-FA guard (no FA link, font or unknown
fa- name; every icon page loads the glyph drop-in),
test/empty-states.test.js, test/markdown.test.js (an unsafe
scheme never becomes a link), test/members-only.test.js, test/privacy.test.js, the Pulse source
rules, test/nginx-auth-limit.test.js, test/nginx-internal-paths.test.js and the N-1 fixtures.
ov test runs the same.
Conventions
- CommonJS, 4-space indent, single quotes, semicolons; domain logic in
server/pastes|comments|forum,
rendering inserver/render/, no SQL in routes. - Escape every interpolated value (
esc), embed JSON with the<-escaping helper; never build HTML
from unsanitised Markdown. - Errors are small JSON/problem bodies with stable codes; never leak a private paste, a member-only
body or another person's data through an error or a cache header. - Works with JavaScript off; a feature that requires a script must not be the only path.
Pitfalls
- No Font Awesome. The no-FA guard fails on a page/stylesheet that links it, or on
anfa-name no glyph/brand draws. Useopenvibe-shared/glyphsandopenvibe-shared/brands. - Community is the only paste authority. Do not add a second store or read pastes from Media/Live.
- Identity comes from the verified token only — never a body or a query.
- Locked pastes are sealed at rest, readable only through their gate; a share row must not make an
unpublished/private paste readable. - Pulse excludes unlisted, private, burn, NSFW and AI pastes; do not "fix" a missing item by
widening that filter. - Strip image metadata before upload; a 1×1 PNG's tEXt chunk must not reach Media.
- Members-only content fails closed when VIP is unavailable: teaser, not body.
- Phone first is a standing product rule. There is no automated overflow test here: check a
390 px viewport by hand (and the forum/commenter grids) before shipping a layout change. - Empty states are part of the product; a blank page for no posts/pastes/comments is a bug.
/internal/*and/metricsstay off the public vhost (nginx tests).- N-1 fixtures are recorded after a deploy; keep them current.
Documentation
- [README.md](README.md) — every surface, the endpoint list, principles, acceptance and deploy.
- [docs/locked-pastes.md](docs/locked-pastes.md), [docs/sharing.md](docs/sharing.md),
[docs/discord-relay.md](docs/discord-relay.md). - [docs/forum/](docs/forum/), [docs/roadmap/](docs/roadmap/), [docs/screenshots/](docs/screenshots/).
openvibe-contractsholdscommunity.*shapes/capabilities; VIP and Media own their own.