openvibers/openvibe.coupons AGENTS.md
AGENTS.md
AGENTS.md — OpenVibe.Coupons
Project overview
Coupon codes with merchant matching, restrictions, expiry and real-people validity reports, plus the
public API the OpenVibe browser helper uses. Distinct from Deals even where they cross-link. Node.js/
Express, PostgreSQL (ov_coupons), server-rendered, CommonJS, no build step.
- Service id
coupons, port4850, originhttps://openvibe.coupons, unitopenvibe-coupons.service,
release root/opt/openvibe.coupons. - It holds no codes today: nothing is seeded and no coupon source is enabled.
The four rules
- No code is "working" because a model guessed it. Status comes only from recent reports by
people; a submission, a source, staff or AI output cannot set it. - Unknown stays unknown. An unstated expiry is "Expiry unknown" (
nullin JSON); no recent reports
means no confidence number; "no restrictions stated" is never "no restrictions". - Expired codes leave active results predictably — at the instant their known expiry passes, in
the API, pages, feeds, sitemaps and Search. - A merchant page (or any page) cannot obtain anyone's data. Lookups return the same bytes to
every caller; the API ignores cookies; the extension holds no OpenVibe session.
What it owns
The nine charter tables in [migrations/](migrations/):
| Table | What |
|---|---|
| coupon_merchants | pending (member proposal, invisible until staff approve), active, disabled |
| coupon_merchant_domains | normalized host, registrable domain (eTLD+1), subdomain rule, optional path prefix |
| coupons | code; status exactly unknown\|reported_working\|reported_failed\|expired\|disabled; confidence NULL when no recent reports; expires_at NULL with precision (instant/date) and basis (evidence/submitter/source/staff); review state pending\|published; origin member\|staff\|source\|ai |
| coupon_sources | evidence: member/staff submissions (URL, own-domain flag), Sources items (id, revision, retrieval time), AI runs |
| coupon_restrictions | min spend (minor units + ISO 4217), categories, new customers only, ISO 3166-1 regions, other |
| coupon_validation_reports | worked/failed; one per (reporter, code, UTC day); reporter is an HMAC of the subject |
| coupon_status_history | every status/confidence change with reason and actor |
| coupon_application_hints | how to apply a code (one code or all of a merchant's) |
| coupon_watches | private member watches |
Plus extension_installs (browser-helper tokens, hashes only), the Sources importer state/holds, the
SDK outbox and the publishing index sequencer.
Not owned: the merchant's real terms, the browser extension itself
(OpenVibe.Extensions), identity, the Search index.
Who calls it, and whom it calls
- Callers: people in the browser; the browser helper with an extension token; OpenVibe.Sources'
importer; services with a Network service token audienceopenvibe.couponsand the route's
capability; staff moderation. - Depends on: Network (SSO client
coupons, JWKS, account export/deletion); Sources
(sources.item.read, categorycoupons; the seededstaff-coupon-codessource is disabled until a
person enables it); Events (SDK outbox); Search (consumescoupons.index_document.*);
openvibe-publishing(seo gate, ssr, index hooks); PostgreSQL and Valkey;openvibe-contracts/
openvibe-sdk/openvibe-sharedpins.
Repository map
- Entry: [server/index.js](server/index.js) — app, worker (expiry sweep, importer), outbox;
graceful stop. - App: [server/app.js](server/app.js) — helmet/CSP,
/auth,/shared,/api/v1(API),
/api, pages, discovery, health/ready. - Config/db: [server/config.js](server/config.js), [server/db.js](server/db.js).
- Domain: [server/domain/](server/domain/) —
publication.js,merchants.js,coupons.js,
reports.js,installs.js,watches.js,facts.js,moderation.js. - Clients: [server/clients/sources.js](server/clients/sources.js).
- HTTP/render: [server/http/api.js](server/http/api.js),
public.js,discovery.js,
actor-limits.js; [server/render/](server/render/) — pages, home, offer, store, short, layout. - Auth: [server/auth/viewer.js](server/auth/viewer.js); [server/sharing.js](server/sharing.js).
- Worker/importer: [server/worker.js](server/worker.js),
server/events/consumer.js. - Deploy: [deploy/systemd/openvibe-coupons.service](deploy/systemd/openvibe-coupons.service),
[deploy/nginx/openvibe.coupons.conf](deploy/nginx/openvibe.coupons.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:4850
npm test # test/run.js, PGlite, mock Network/Sources/Events
npm run test:pg # same on PostgreSQL (COUPONS_TEST_STORE=pg)
npm test -- lifecycle # filter by file name; --strict makes a skip fail
ov test # the harness's sharded run of what the change reaches
Database and migrations
ov_coupons on the host's data role (ADR-035); PGlite in development/tests. Async through
openvibe-sdk/db; migrations migrations/NNNN_*.sql applied at boot; never edit an applied migration,
add a new number. The status/confidence history is append-only in spirit: a change writes
coupon_status_history and the outbox in the same transaction, and the expiry sweep records the event
and tombstone once.
Auth and privacy
- People sign in through Network SSO for submissions, watches and reports. The public API takes an
Authorizationheader only; it ignores cookies and returns the same bytes to every caller, so a
merchant page cannot read a visitor's data. - The browser helper authenticates with an extension token: stored as a hash, scoped, revocable,
and refused immediately on revocation; no OpenVibe session lives in the extension. - Reports are never anonymous, deduplicated per (reporter, code, UTC day), only the latest per person
counts, and rate-limited per hour/day; the reporter is an HMAC, not a subject id. - Service callers need the route's capability (
coupons.*); staff actions are audited.
Frontend loading model
Every page is server-rendered and useful without JavaScript; there is no SPA, bundler or
transpiler. openvibe-shared supplies the Frame, legal/SEO and /shared/* from the pinned
node_modules; openvibe-publishing supplies the SSR/index helpers. Discovery (robots, sitemap, feeds,
llms.txt, IndexNow) is generated from the same data. CSP and cache policy are in
[server/app.js](server/app.js) and openvibe-shared/cache-policy.
Deploy
sudo ovhost deploy coupons (git-checkout: fetch, fast-forward /opt/openvibe.coupons, install on a
lockfile change, restart, wait for /api/ready). Unit openvibe-coupons.service on
127.0.0.1:4850, env /etc/openvibe/coupons.env; the release migrates ov_coupons at boot; vhost
[deploy/nginx/openvibe.coupons.conf](deploy/nginx/openvibe.coupons.conf). Rollback
sudo ovhost rollback coupons --to <sha>. No secret in a command line, file or log.
Tests
npm test runs each test/*.test.js in its own process with PGlite and in-process Network/Sources/
Events mocks. The README's acceptance table maps each rule to its test: lifecycle.test.js (unknown
stays unknown, status fields refused from everyone, AI held for review, expiry at the instant, the
sweep once), reports.test.js (dedupe, latest-per-person, rate limits, no cookies/anonymous),
confidence.test.js (the formula), extension-privacy.test.js (hashing, revocation, scopes) and more.
ov test runs the same.
Conventions
- CommonJS, 4-space indent, single quotes, semicolons; domain in
server/domain/, HTTP in
server/http/, no SQL in routes. - Ids/errors/capabilities from
openvibe-contracts; errors are problem+json with stable codes. - Dates are UTC; an expiry carries its precision and basis, and an unknown one stays
null. - Escape all rendered text; no personal data in URLs, JSON-LD, feeds or the API.
Pitfalls
- A model, a source or staff cannot mark a code working. Only recent people's reports move the
status; an AI-origin code ispendingreview. Do not add an admin "set working" path. - Unknown is not zero/false. No expiry →
null, no reports →confidence: null, no stated
restriction → "none stated". Do not substitute defaults. - Expiry is instantaneous. API/page/feed/sitemap/Search must agree at
t-1msandt; an expired
page is noindex. - The public API is cookie-blind. Do not add session-based personalisation or per-caller bytes.
- Reports are per person per code per day, latest wins; a second report does not add a second
count, and no anonymous report is accepted. - Tracking/affiliate links are not introduced here; a source's link policy is explicit, and the
code's evidence URL must be on the merchant's own domain where the rule says so. - The extension token is a secret: hash at rest, never returned after creation, revocation immediate.
- No Offer/validity JSON-LD — the README deliberately omits it; do not add price/validity markup.
Documentation
- [README.md](README.md) — the rules, tables, statuses/confidence, routes, API, extension credentials,
events, capabilities, acceptance, security and deploy. - [docs/capabilities-proposal/](docs/capabilities-proposal/), [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json).
- OpenVibe.Contracts holds the
coupons.*shapes; Sources owns the item contract.