openvibers/openvibe.vip AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.VIP |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | OpenVibe.VIP (`https://openvibe.vip`) is membership on the OpenVibe network: creator and network plans |
| 6 | with versioned terms, perks, gated-resource policy, members-only posts, and a product-wide answer to |
| 7 | "is this person a member, and may they see this?". VIP owns plans and perks; **Billing owns |
| 8 | subscriptions, prices and the entitlement truth** (ADR-012). |
| 9 | |
| 10 | - **Entry:** `server/index.js`; app `server/app.js`; port `4620`, service id `vip`. |
| 11 | - **API:** `server/api/v1.js`, auth `server/api/auth.js`, limits `server/api/actor-limits.js`. |
| 12 | - **Domain:** `server/domain/` — `plans.js` (versions), `perks.js`, `policies.js` (gated resources), |
| 13 | `memberships.js`, `entitlements.js` (projection), `periods.js`, `checkout.js`, `posts.js`, |
| 14 | `prices.js`, `creators.js`, `cards.js`, `markdown.js`, `account-data.js`, `index.js`. |
| 15 | - **Pages and embeds:** `server/web/` (`pages.js`, `routes.js`, `layout.js`, `session.js`, |
| 16 | `embeds.js`, `forms.js`, `short.js`); the public card/badge/embed at `/embed`; `public/js/vip.js` |
| 17 | and `post-upload.js`; own CSS in `public/css`. |
| 18 | - **Consumer client:** `client/vip-client.js` — the package products use to check membership and gate |
| 19 | a resource, with the product cache and its convergence bound (README "Consumer seam"). |
| 20 | - **Docs:** `docs/cutover-0002-renewal-periods.md` (renewal periods/grace), `SECURITY.md`. |
| 21 | |
| 22 | ## Owns, does not own |
| 23 | |
| 24 | VIP is the only writer of the nine authority tables: `vip_creators`, `vip_plans`, |
| 25 | `vip_plan_versions` (immutable; triggers refuse UPDATE/DELETE except the publish stamp), `vip_perks`, |
| 26 | `vip_plan_perks` (immutable per version), `vip_product_bindings`, `vip_member_preferences`, |
| 27 | `vip_gated_resource_rules` and `vip_migration_maps`. Operational tables: `vip_memberships` (which plan |
| 28 | version a membership was bought under — never whether it is active), `vip_entitlement_projection` (a |
| 29 | cache with `valid_until`), `vip_checkouts`, `vip_membership_periods` (Billing's charge records), |
| 30 | `event_outbox` and `idempotency_receipts`. |
| 31 | |
| 32 | Does not own: subscription contracts, prices, charges, renewals, cancellations, refunds, chargebacks or |
| 33 | canonical entitlements (Billing — VIP never prices anything and never moves money; plan terms carry no |
| 34 | price); identities (Network); products' own resources (only an EntityRef); badge rendering (each |
| 35 | product). |
| 36 | |
| 37 | ## Who calls it, what it calls |
| 38 | |
| 39 | People use the pages/API with a Network user JWT and act on their own plans, perks, rules, members, |
| 40 | memberships and checks; staff with `staff.site.configure` manage network plans. Services use |
| 41 | client-credentials tokens for audience `openvibe.vip` with one `vip.*` capability per route. Products |
| 42 | consume VIP through `client/vip-client.js` / SDK `openvibe-sdk/vip` and the product cache: Chat (member |
| 43 | badge), Community (members-only spaces/threads), Blog (members-only posts), Wiki (VIP spaces/pages); |
| 44 | Live is not wired. VIP calls Billing (`billing.intent.create`, `billing.subscription.manage`, |
| 45 | `billing.entitlement.check`), Network (JWKS, OAuth), Events (Billing deliveries in, VIP outbox out) and |
| 46 | Media (a members-only post's image, `media.object.upload`, `vip` tenant; without it the post has none). |
| 47 | |
| 48 | ## Database and migrations |
| 49 | |
| 50 | PostgreSQL `ov_vip` through `openvibe-sdk/db` (PgBouncer for the app, direct owner role for |
| 51 | migrations); Valkey optionally holds per-actor limit counters. Migrations `0001..0007` include plan |
| 52 | tiers, account data, telemetry, member posts and the outbox rejection index; applied at boot, never |
| 53 | edited after they run, only additive so a rollback works. The immutability of plan versions and |
| 54 | plan-perks is enforced by database triggers, not only by code. |
| 55 | |
| 56 | ## Entitlements and auth |
| 57 | |
| 58 | Every API route checks one capability (service) or ownership (person); the API never reads cookies. |
| 59 | Billing events arrive HMAC-SHA256-signed over the raw body (`VIP_EVENTS_SECRET`, rotation by list), |
| 60 | `source` must be `billing`, the inbox is exactly-once and the ordering guard rejects a stale period. |
| 61 | **Fail closed everywhere a membership is decided:** "unknown" is never "yes"; the projection has a |
| 62 | `valid_until` and a convergence bound, and an expired/unsynced row is not a membership. |
| 63 | |
| 64 | ## Deploy |
| 65 | |
| 66 | `sudo ovhost deploy vip` (git-checkout: fast-forward `/opt/openvibe.vip`, install on a lockfile |
| 67 | change, restart, wait for `/api/ready`; rollback `sudo ovhost rollback vip --to <sha>`). Unit |
| 68 | `openvibe-vip.service` on `127.0.0.1:4620`, env `/etc/openvibe/vip.env`, database `ov_vip`, nginx |
| 69 | `deploy/nginx/openvibe.vip.conf` (live since 2026-10-17). The release migrates at boot. |
| 70 | |
| 71 | ## Commands and tests |
| 72 | |
| 73 | ```bash |
| 74 | npm ci |
| 75 | npm run dev # http://127.0.0.1:4620 |
| 76 | npm test # every test/*.test.js, in-process Network/Billing stubs |
| 77 | npm test -- plans import # only matching files |
| 78 | npm run test:pg # the same on PostgreSQL |
| 79 | npm run subscribe # create the Events subscriptions once |
| 80 | ``` |
| 81 | |
| 82 | `test/run.js` uses `openvibe-shared/test-runner` (60 s, serial); temp databases and stubs, never the |
| 83 | network. Tests cover immutable plan versions, perks/bindings, gated policy incl. sensitive resources, |
| 84 | the Billing projection and convergence, renewal periods/grace/refunds, checkout/cancel, member posts, |
| 85 | the consumer client/cache, the embed/card, account export/deletion (a paid period stays), and the |
| 86 | pages. |
| 87 | |
| 88 | ## Conventions and pitfalls |
| 89 | |
| 90 | - CommonJS, 4-space indent, single quotes, semicolons; RFC 9457 problem+json; escaped HTML and a safe |
| 91 | Markdown renderer (a post source can never carry markup). |
| 92 | - **Never store "is a member".** `vip_memberships` says which version was bought; access is answered |
| 93 | by the entitlement check/projection, and a projection row is a cache with a validated `valid_until`. |
| 94 | - **Never put a price in plan terms.** The terms say Billing sets and charges it; `vip_prices` is a |
| 95 | display of Billing's numbers, not an authority. |
| 96 | - **Immutability is real:** do not add an UPDATE path to `vip_plan_versions`/`vip_plan_perks`; publish |
| 97 | stamps once, then a new version is a new row. |
| 98 | - **A gated resource rule is policy, not content:** add a `{service, type, id}` EntityRef rule and a |
| 99 | product binding; the product still renders its own resource. |
| 100 | - **Billing is exactly-once and ordered:** a redelivery, a stale `billing.entitlement.changed` or a |
| 101 | reversed transaction must converge, not double-count a period or grant access after a refund. |
| 102 | - **Deleting a member does not delete the books:** memberships/preferences/projection/checkouts go, |
| 103 | paid periods stay; a deleted creator is suspended without their name but plan versions members joined |
| 104 | under stay whole. |
| 105 |