openvibers/openvibe.fun AGENTS.md
AGENTS.md
AGENTS.md — OpenVibe.Fun
Project Overview
OpenVibe.Fun is the Fun room: IRL quest boards attached to a public OpenVibe.Live channel, a deterministic
"what's fun near me" guide from OpenStreetMap (Ticketmaster only when a key is configured), and group plans with one
vote each and a winner the viewer can add to their own OpenVibe.Today.
- Domain:
openvibe.fun· Port: 5140 · Service id:fun· Env prefix:FUN - Runtime: Node 22, Express 4, PostgreSQL 18 through
openvibe-sdk/db(embedded PGlite in development/tests),
Valkey 9 for shared limit counters. - Read first: [README.md](README.md) (ownership, API and safety rules) and [STATUS.json](STATUS.json).
Network context: identity, roles and the OpenCoins wallet are OpenVibe.Network (OAuth client fun, PKCE S256;
staff means Network role global_mod or above). Fun reads Live's public channel state and Chat's public history
through server/upstreams.js; it never posts to Live or Chat. Today is written only with the signed-in person's own
Network token (server/today.js). Account export/deletion arrives from OpenVibe.Events at /internal/events. Search
documents wait in fun_outbox and are delivered by openvibe-publishing/search-relay (server/search.js). Fun owns
no other service's state.
Commands
npm ci # install (the repo pins contracts/sdk/shared by tag)
npm run dev # http://localhost:5140, NODE_ENV=development
npm start # node server/index.js
npm test # every test/*.test.js in its own process, temp PGlite, mocked Network/upstreams
npm run test:pg # the same through PostgreSQL + PgBouncer (openvibe-sdk scripts/test-services.sh up)
npm test -- quests # only test files whose name contains the word
npm test -- --strict # a skipped test fails the run
node --check <file.js> # syntax check (no linter/formatter in this repo)
test/run.js uses openvibe-shared/test-runner; tests never reach the internet — upstream fixtures are local. There
is no build step: pages are server-rendered and assets are served from public/ with content-hash cache headers.
Architecture at a Glance
- Entry: [server/index.js](server/index.js) — starts the listener, telemetry tick/retention, the read-only chat
sync, the OpenCoin retry timer and the Events subscriptions;FUN_DRILLkeeps boot-time work off. Graceful stop is
createLifecycle()in the same file. - App factory: [server/app.js](server/app.js) — helmet/CSP, SSO middleware,
/auth,/internal/events,
/shared+ static,/api/v1, the agent layer (Markdown twins and/.well-known/agent.json), pages, 404 and error
handler. Tests build this app withcreateApp({ store, fetchImpl, ... }). - Config: [server/config.js](server/config.js) —
load(env)is pure; production reads/etc/openvibe/fun.env.
Only variable names appear in code and docs. - Database: [server/db.js](server/db.js) — migrations in [migrations/](migrations/) run at boot with
DATABASE_DIRECT_URL(owner);DATABASE_URLserves through PgBouncer. No table is created at runtime. - Store/domain: [server/store.js](server/store.js), [server/quests/](server/quests/) (
ideas.js,safety.js),
[server/guide/](server/guide/) (index.js,places.js,upstream.js,events.js),
[server/chat-sync.js](server/chat-sync.js), [server/coins.js](server/coins.js),
[server/sharing.js](server/sharing.js), [server/today.js](server/today.js). - HTTP: [server/http/router.js](server/http/router.js) is the async router; [server/http/api.js](server/http/api.js)
mounts/api/v1, [server/http/pages.js](server/http/pages.js) the pages, [server/http/principal.js](server/http/principal.js)
decides person vs service token and capabilities. Views are under [server/views/](server/views/). - Auth: [server/auth/sso.js](server/auth/sso.js) + [server/auth/keys.js](server/auth/keys.js) — Network OAuth2
PKCE and offline JWKS verification; staff from the Network role claim ([server/identity/staff.js](server/identity/staff.js)).
Account export/deletion: [server/identity/account-data.js](server/identity/account-data.js). - Frontend: server-rendered HTML through [server/render/layout.js](server/render/layout.js) and
[server/render/html.js](server/render/html.js), which pull the OpenVibe Frame/kit fromopenvibe-shared. Assets:
[public/css/fun.css](public/css/fun.css), [public/js/fun.js](public/js/fun.js). Pages work without JavaScript; JS
only adds convenience. Markdown twins come from [server/agent-pages.js](server/agent-pages.js) and
[server/agent-descriptor.js](server/agent-descriptor.js). - Deploy: [deploy/systemd/openvibe-fun.service](deploy/systemd/openvibe-fun.service) (127.0.0.1:5140,
/etc/openvibe/fun.env) and [deploy/nginx/openvibe.fun.conf](deploy/nginx/openvibe.fun.conf) (TLS, www → apex,
/internaland/metricsclosed).sudo ovhost deploy funon the host; databaseov_fun; /opt/openvibe.fun.
Ownership and Boundaries
Fun is the authority for the tables its migrations create: quest boards/quests/votes/checks/badges/rewards, plans and
their options/votes, fun_reports, fun_chat_seen/fun_chat_cursors, fun_geo_cache, fun_outbox,
account_data_events, sharing tables and telemetry_samples. It is not the authority for:
- identity, accounts, roles, sessions or the wallet — OpenVibe.Network;
- Live channels, Chat rooms/history, Today calendars — read-only upstreams or the person's own token;
- OSM/Ticketmaster data — cached, attributed public sources, never paid providers;
- event delivery (OpenVibe.Events), the host/database/Valkey (OpenVibe.Host), or the search index (OpenVibe.Search).
Public quests/plans are anonymized on account deletion; shared plans stay for their other members. Never silently keep
a deleted person's name on a row.
Auth and Capabilities
- A person is the
fun_atsession cookie orAuthorization: Bearer <Network user token>, verified offline. A
cookie-authenticated write must come fromhttps://openvibe.fun(403fun.cross_site). - Per-actor limits:
FUN_LIMITS_MINUTE,FUN_LIMITS_HOUR; shared counters use Valkey. - Inbound v1: no service capability guards a route. Do not invent one.
- Outbound:
events.subscription.manageat boot;network.account.export.contributeand
network.account.deletion.confirmthroughopenvibe-sdk/account-data;search.document.writefor the Search
relay;network.coins.credit+identity.subject.resolveonly whenFUN_COINS=on. - The OpenCoin credit is an outbox (
fun_quest_rewards) with retries; the local badge is always paid.
API (JSON under /api/v1)
GET /guide, GET /quests, GET /quests/ideas, GET /quests/boards/:id, GET /quests/:slug,
POST /quests/boards, POST /quests/boards/:id/suggest|idea, POST /quests/:slug/vote|accept|live|complete|verify|hide|report;
GET /plans, GET /plans/:slug, POST /plans, POST /plans/:slug/options|vote|decide|today|hide|report;
GET /reports (staff). Discovery/health: /api/v1/ping, /openapi.json, /llms.txt, /sitemap.xml, /robots.txt,
/api/health, /api/ready, /release.json, /metrics (loopback only). Errors are RFC 9457
application/problem+json with a stable code (fun.invalid, fun.quest.*, fun.plan.*, fun.rate_limited, …).
Conventions
- CommonJS (
require/module.exports), 4-space indent, single quotes, semicolons; no linter/formatter. - Every route handler goes through [server/http/router.js](server/http/router.js) so async rejections reach the error
handler. Never log request bodies; never return raw rows — page/view code renders explicit fields. - IDs are
prefix_+ ULID via the store; timestamps are ISO strings from the injected clock. - New schema = a new
migrations/NNNN_name.sql; never edit a migration that has run. Keep table prefixesfun_. - CSP: add every external origin to the
helmetdirectives in [server/app.js](server/app.js); images/frames default
to self plus the named OpenVibe hosts. Do not widenframeAncestorsorobjectSrc. - Phone first: pages must work at 390 px with no horizontal overflow; the navbar/footer come from
openvibe-shared. - "Calm reduces, only Off removes": a user preference may de-emphasize content, never delete it; an Off switch is the
only removal. - Public pages must have real content in the first paint — no placeholder text or fake counts (
home-real-content,
home-real-firsttests); link-outs must be real (remind-links). - Do not add model calls or a new AI capability here: v1 is deterministic.
Key Pitfalls
- No build step. Editing
public/is live on deploy; do not add?v=by hand — the layout's asset version does
that. - Upstreams are public and optional. Ticketmaster is off without
TICKETMASTER_API_KEY; OSM must be paced,
cached, attributed (ODbL) and has a stale fallback. Never fetch an arbitrary user URL. - Read-only chat.
server/chat-sync.jsingests!quest/!plancommands idempotently per chat message id; it
must never write to Chat or Live. - Drill mode. Anything started at boot (timers, loops, outbound calls) must be off under
FUN_DRILL. - Agent layer.
.mdpaths and?format=markdownare handled before page routes so/q/x.mdis a twin, not a
quest slug. New top-level page routes must also be visible to [server/http/pages.js](server/http/pages.js) and the
sitemap/llms artifacts. - Safety filter.
server/quests/safety.jsis explainable and must keep rejecting dangerous, illegal,
trespassing and harassing suggestions before storage. - Account deletion runs through
/internal/eventsonly (loopback, signature-verified); a redelivered event must
change nothing. - No Font Awesome in new UI unless the shared kit/glyph test for the page allows it; use the shared glyph set.
Testing
npm test boots every test/*.test.js in its own process on a temporary PGlite database with a mock Network and
fixture upstreams (test/run.js). The suite covers migration, quest safety/state machine, chat idempotency, plan
voting/tie/Today, Search documents, SSO, cache/pages, coins opt-in off/on, sharing, account export/deletion, agent
layer, real-content-first and broken-link regressions. npm run test:pg runs the same suite through PostgreSQL +
PgBouncer. Run the narrowest file first (npm test -- quests), then the suite. Always node --check changed files.
Documentation
- [README.md](README.md) — purpose, owns/does-not-own, capabilities, API, quest safety, guide, plans, pages, Search,
run/test, deploy. - [STATUS.json](STATUS.json) — current stage, deployed flag, features and not-yet list.
- [.env.example](.env.example) — every variable (names only; never commit values).
- [CHANGELOG.md](CHANGELOG.md) — released changes.
- [SECURITY.md](SECURITY.md) — vulnerability reporting.