openvibers/openvibe.rent AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Rent |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Rent is the rental aggregator: every major rental site's own search for one place and filter set |
| 6 | (`/everywhere`), person-posted listings that anyone can search, city rent pages with Census/HUD/BLS/FHFA context, |
| 7 | "Is this rent fair?", an "Affordable housing near you" map-free guide from HUD open data, a personal board of |
| 8 | listings from any site, and a scam screen. The same process also serves **OpenVibe.Homes** (`openvibe.homes`) for |
| 9 | buying, chosen by Host header. |
| 10 | |
| 11 | - **Domain:** `openvibe.rent` (+ `openvibe.homes` in-process) · **Port:** 5010 · **Service id:** `rent` · |
| 12 | **Env prefix:** `RENT` |
| 13 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey |
| 14 | optional for shared limits. |
| 15 | - **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example). |
| 16 | |
| 17 | **Network context:** accounts, sign-in and the staff role are OpenVibe.Network (OAuth client `rent`, PKCE S256; Homes |
| 18 | has its own redirect URI on `openvibe.homes`). Account export/deletion arrives from OpenVibe.Events at |
| 19 | `/internal/events`. The listing scam screen calls OpenVibe.AI (`rent.screen_listing`). Public market data is fetched |
| 20 | from OSM Nominatim, HUD ArcGIS, FHFA and BLS — paced, cached and attributed. Rent stores no account, no role, no file |
| 21 | or image, and owns no event delivery. |
| 22 | |
| 23 | ## Commands |
| 24 | |
| 25 | ```bash |
| 26 | npm ci # install (contracts/sdk/shared pinned by tag) |
| 27 | npm run dev # http://localhost:5010, PGlite under data/ |
| 28 | npm start # node server/index.js |
| 29 | npm test # every test/*.test.js on temp PGlite, mock Network/AI/upstreams |
| 30 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 31 | npm test -- board # only test files whose name contains the word |
| 32 | node --check <file.js> # syntax check (no linter configured) |
| 33 | ``` |
| 34 | |
| 35 | No build step: pages are server-rendered; CSS only under [public/css/](public/css/) (`app`, `board`, `cities`, |
| 36 | `everywhere`, `home`, `homes`, `housing`). Every page works without JavaScript. |
| 37 | |
| 38 | ## Architecture at a Glance |
| 39 | |
| 40 | - **Entry:** [server/index.js](server/index.js) — listener, telemetry, the listing-expiry sweep and the HPI timer, |
| 41 | Events subscriptions and graceful stop. `RENT_DRILL` keeps boot-time work off; the expiry sweep is started **only** |
| 42 | here, never by a page render. |
| 43 | - **App factory:** [server/app.js](server/app.js) — CSP, SSO with per-host auth routes (Rent and Homes), `/auth`, |
| 44 | `/internal/events`, `/shared`+static, `/api/v1`, host routing for openvibe.homes, pages, 404/error handler. |
| 45 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/rent.env`. |
| 46 | - **Listings:** [server/listings/](server/listings/) — `listings.js` (rules/validation), `store.js`, `service.js`, |
| 47 | `screen.js` (AI scam screen), `staff.js`, `expiry.js`. |
| 48 | - **Market data:** [server/housing/](server/housing/) — `places.js`, `upstream.js` (paced/identified), `store.js`, |
| 49 | `hpi.js` (FHFA), `fair-market-rents.js` (HUD), `safmr.js`, `rent-cpi.js` (BLS); city aggregation |
| 50 | [server/aggregate/cities.js](server/aggregate/cities.js), [server/aggregate/everywhere.js](server/aggregate/everywhere.js). |
| 51 | - **Homes:** [server/homes/](server/homes/) — `pages.js`, `city-pages.js`, `sites.js`, `mortgage.js`. |
| 52 | - **Board and saved searches:** [server/board/board.js](server/board/board.js). |
| 53 | - **HTTP:** [server/http/api.js](server/http/api.js), [server/http/pages.js](server/http/pages.js), |
| 54 | [server/http/city-pages.js](server/http/city-pages.js), [server/http/board-pages.js](server/http/board-pages.js), |
| 55 | [server/http/principal.js](server/http/principal.js), [server/http/discovery.js](server/http/discovery.js), |
| 56 | [server/http/router.js](server/http/router.js), [server/http/caller-limits.js](server/http/caller-limits.js). |
| 57 | - **Auth/account data:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js), |
| 58 | [server/identity/account-data.js](server/identity/account-data.js), [server/events-consumer.js](server/events-consumer.js). |
| 59 | - **Frontend/rendering:** [server/render/](server/render/) (`layout.js`, `html.js`, `home-blocks.js`, `short.js`, |
| 60 | `markdown.js`), agent twins [server/agent-pages.js](server/agent-pages.js), |
| 61 | [server/agent-descriptor.js](server/agent-descriptor.js), Homes descriptor |
| 62 | [server/agent-descriptor.homes.js](server/agent-descriptor.homes.js). |
| 63 | - **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `rent_listings`, `rent_reports`, |
| 64 | `rent_listing_checks`, `rent_board_items`, `rent_saved_searches`, `housing_geo_cache`, `area_price_index`, |
| 65 | `rent_fmr_areas`, `rent_ingest_cursor`, `rent_cpi_points`, `rent_index_revisions`, `event_outbox`, |
| 66 | `account_data_events`, `telemetry_samples`. |
| 67 | - **Deploy:** [deploy/systemd/openvibe-rent.service](deploy/systemd/openvibe-rent.service) (127.0.0.1:5010, |
| 68 | `/etc/openvibe/rent.env`, `/opt/openvibe.rent`), [deploy/nginx/openvibe.rent.conf](deploy/nginx/openvibe.rent.conf) |
| 69 | and [deploy/nginx/openvibe.homes.conf](deploy/nginx/openvibe.homes.conf). `sudo ovhost deploy rent`; database |
| 70 | `ov_rent`; rollback when `/api/ready` fails. |
| 71 | |
| 72 | ## Ownership and Boundaries |
| 73 | |
| 74 | Rent owns listings, reports, AI screens, the board, saved searches, the market caches (geocode, HPI, FMR, CPI), the |
| 75 | index sequencer and its outbox. It does **not** own accounts/roles (Network), the site frame (Network), event delivery |
| 76 | (Events), files/images (Media), the external market data (OSM/HUD/FHFA/BLS/Census, read-only and attributed), or the |
| 77 | host/database (Host). A listing carries **no image**; `contact_url` is text and is never fetched. The interface says |
| 78 | plainly that Rent takes no payment from either side of a listing. |
| 79 | |
| 80 | ## Auth and Capabilities |
| 81 | |
| 82 | - **No `rent.*` capability exists and none is needed.** Every write is a person acting for themself (or staff by |
| 83 | Network role); an app/agent/service token cannot post, edit, report or save. `CAPABILITIES` in |
| 84 | [server/http/principal.js](server/http/principal.js) is empty on purpose — do not add one without Contracts. |
| 85 | - A person is a Network Bearer token or the `rent_at` session; a cookie write must be same-origin. Homes has its own |
| 86 | OAuth redirect and auth host. |
| 87 | - Outbound: `events.subscription.manage` only (Network/Events endpoints); the AI screen uses a client-credentials token |
| 88 | for `rent.screen_listing`. No other service capability. |
| 89 | - Listing rules: 10 active listings, 5 posted a day, 30 days each, 3 reports hide one until staff review, 30 per |
| 90 | search page. **A street address is refused** — listings are area-level; never add an exact-address field. |
| 91 | - AI screen: reads the listing text only — **never the poster or the contact link** — and a `review` flag puts it in |
| 92 | the staff queue with the signs and quoted words. It changes nothing else for anyone; staff can mark it fine. |
| 93 | - Errors are RFC 9457 problem+json with stable codes. |
| 94 | |
| 95 | ## API (`/api/v1`) |
| 96 | |
| 97 | `GET /ping`, `GET /housing?q=&kind=`, `GET /listings?q=&kind=&city=&country=&min=&max=&period=&limit=&before=`, |
| 98 | `GET /listings/:id` (contact link only for a signed-in person), `POST /listings`, `PATCH|DELETE /listings/:id`, |
| 99 | `POST /listings/:id/renew`, `POST /listings/:id/reports`, |
| 100 | `GET|POST /saved-searches`, `DELETE /saved-searches/:id`, `GET|POST /board`, `PATCH|DELETE /board/:id`, |
| 101 | staff: `GET /staff/reports`, `POST /listings/:id/state`. Machine endpoints `/api/health`, `/api/ready`, |
| 102 | `/release.json`, `/metrics` are loopback only. |
| 103 | |
| 104 | Pages: `/`, `/listings`, `/listings/:id`, `/post`, `/mine`, `/saved`, `/safety`, `/board`, `/staff`, `/updates`, |
| 105 | `/in`, `/in/<state>`, `/in/<state>/<city>`, `/fair`, `/everywhere`, the housing pages and the openvibe.homes pages; |
| 106 | discovery (`/llms.txt`, sitemaps, robots, Markdown twins) is generated per host. |
| 107 | |
| 108 | ## Conventions |
| 109 | |
| 110 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through the router. |
| 111 | - New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `rent_`/`housing_` prefixes. |
| 112 | - External data is paced (`RENT_HOUSING_MIN_INTERVAL_MS`, one request a second per host), cached and identified with |
| 113 | `RENT_USER_AGENT` ("OpenVibeRent/…", never a browser's). HUD and FHFA are public domain; OSM is ODbL and credited; |
| 114 | Census/HUD/BLS lines stay separate and labelled — never averaged into one number. |
| 115 | - Never fetch a user-supplied URL (a listing link, a board link): store it, render it as a link, and leave it alone. |
| 116 | - A contact URL is shown only to a signed-in person; a hostile title/description is escaped everywhere (page, API, |
| 117 | JSON-LD, Search document). |
| 118 | - CSP: add origins to `helmet` in [server/app.js](server/app.js) per host. No Font Awesome; shared glyphs. |
| 119 | - Phone first: listing, board, city and housing pages fit 390 px; empty states use the shared kit. |
| 120 | - "Calm reduces, only Off removes": a hidden/expired listing is suppressed, not deleted; only the owner's explicit |
| 121 | delete (or staff `removed`) removes it. |
| 122 | - A fair-rent answer shows the median, its margin of error and the size it applies to; no invented precision. |
| 123 | |
| 124 | ## Key Pitfalls |
| 125 | |
| 126 | - **Expiry** runs only from [server/listings/expiry.js](server/listings/expiry.js), started by |
| 127 | [server/index.js](server/index.js). Do not move it into a page or an API read (tests would race it). |
| 128 | - **Reports hide at three** (`rent_reports_once` unique per person per listing); staff restore, and the count is never |
| 129 | shown to the public. |
| 130 | - **Board:** one row per person per link, at most 500; a repeated link updates the same row (200), a different existing |
| 131 | link is 409 `board.duplicate`, full is 409 `board.full`. Board links come from any site; they are never fetched. |
| 132 | - **Saved searches:** one row per distinct filter set; `new_count` compares against `last_seen_at`. |
| 133 | - **AI screen** is bounded, never includes the poster/contact, caches the content hash so the same words are not |
| 134 | screened twice, and is off without the AI URL. A failed run is retried by the sweep, not silently ignored. |
| 135 | - **Homes is a second host in this process:** add routes to the Homes router and its own auth/redirect, not to Rent's |
| 136 | pages; the two hosts must not leak each other's pages or discovery. |
| 137 | - **Drill mode** (`RENT_DRILL`) keeps the expiry sweep, HPI timer, subscriptions and AI sweep off. |
| 138 | - **`event_outbox`** needs its rejected partial index ([migrations/0012_outbox_rejected_index.sql](migrations/0012_outbox_rejected_index.sql)). |
| 139 | - Account deletion removes a person's listings/board/saved searches and anonymizes public rows; redelivery changes |
| 140 | nothing. |
| 141 | |
| 142 | ## Testing |
| 143 | |
| 144 | `npm test` runs each file in its own process on temp PGlite with mock Network/AI/upstreams: listing rules and caps, |
| 145 | street-address refusal, search/facets/paging, contact-link visibility, reports and staff restore, expiry, owner-only |
| 146 | edits, same-origin, saved searches, board, city pages, fair rent, housing/HUD/FMR/SAFMR/HPI/CPI, markets, AI screen, |
| 147 | homes, error pages, discovery, SSO/JWKS, limits, secret-leak/session/redirect, service kit and budgets. |
| 148 | `npm run test:pg` for PostgreSQL. Run the narrowest file first, then the suite; always `node --check` changed files. |
| 149 | |
| 150 | ## Documentation |
| 151 | |
| 152 | - [README.md](README.md) — what it is, owns/does-not-own, data sources and terms, capabilities, what works, API, |
| 153 | configuration, acceptance, deploy, account data, security. |
| 154 | - [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list. |
| 155 | - [.env.example](.env.example) — variable names only. |
| 156 | - [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md), [tools/](tools/). |
| 157 |