openvibers/openvibe.food AGENTS.md
AGENTS.md
AGENTS.md — OpenVibe.Food
Project Overview
OpenVibe.Food answers "what should we eat?" for a household. It finds food banks, soup kitchens, community fridges,
free pantry boxes and budget grocery stores near a place (OpenStreetMap), plans a shop within a budget, carries a food
database with nutrition and typical prices, keeps a pantry with a deterministic "what can I make", and adds barcode,
photo/receipt and AI recipe features. It also follows FDA food recalls from OpenVibe.Sources, compares delivery apps
by opening their own searches, keeps a shared meal plan with a household Chat room, and runs a reviewed recipe library.
- Domain:
openvibe.food· Port: 4970 · Service id:food· Env prefix:FOOD - Runtime: Node 22, Express 4, PostgreSQL 18 through
openvibe-sdk/db(PGlite in development/tests), Valkey 9 for
shared limits. - Read first: [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example).
Network context: identity/accounts are OpenVibe.Network (OAuth client food, PKCE S256). Place data is
OpenStreetMap (Nominatim/Overpass, ODbL, attributed); barcode products are Open Food Facts (ODbL, only the barcode is
sent, answers cached a day); FDA recalls, BLS average prices, USDA ERS prices and Open Food Facts product rows are
read from OpenVibe.Sources. AI recipe ideas and pantry-photo recognition call OpenVibe.AI
(food.suggest_recipes, food.read_pantry_photo). Household chat creates a room through OpenVibe.Chat and reads it
through food.household.read. Checked recipes are OpenVibe.Search documents. Event delivery is OpenVibe.Events. Food
stores no accounts, no money and no third-party data beyond the cached public rows.
Commands
npm ci # install (contracts/sdk/shared/publishing pinned by tag)
npm run dev # http://localhost:4970, PGlite under data/pglite
npm start # node server/index.js
npm test # every test/*.test.js on temp PGlite, mock Network/AI/Sources, no internet
npm run test:pg # the same through PostgreSQL + PgBouncer
npm test -- pantry # only test files whose name contains the word
node --check <file.js> # syntax check (no linter configured)
No build step: pages are server-rendered; assets [public/css/](public/css/) and [public/js/](public/js/)
(barcode-scan.js, pantry-photo.js). Every page works without JavaScript; the barcode/photo pages are the only
camera-assisted paths.
Architecture at a Glance
- Entry: [server/index.js](server/index.js) — listener, telemetry/retention, the Events subscriptions, the recipe
library, Search index, recall/price/Open-Food-Facts followers and the "food near" warm-up; graceful stop.FOOD_DRILL
keeps boot-time work off. - App factory: [server/app.js](server/app.js) — CSP, SSO,
/auth,/internal/events,/internal/households/:id/chat-context,
static//shared, the share API,/api/v1, agent layer, pages, 404/error handler. - Config: [server/config.js](server/config.js) —
load(env)is pure; production/etc/openvibe/food.env. - Product logic: [server/food/](server/food/) —
foods.js,planner.js,pantry-match.js,recipes.js,
library.js,recalls.js,official-prices.js,usda-prices.js,off.js,barcode.js,places.js,
upstream.js(the only OSM client),delivery.js,household-chat.js,box-links.js,search-index.js,
sharing.js,store.js,near-warm.js. - HTTP: [server/http/](server/http/) —
api.js,pages.js,library-routes.js,recall-pages.js,
box-pages.js,principal.js,caller-limits.js,router.js,discovery.js. - Auth/account data: [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js),
[server/identity/account-data.js](server/identity/account-data.js). - Rendering: [server/render/](server/render/) (
layout.js,html.js,markdown.js,short.js), agent twins
[server/agent-pages.js](server/agent-pages.js), [server/agent-descriptor.js](server/agent-descriptor.js). - Database: [server/db.js](server/db.js) and [migrations/](migrations/):
food_geo_cache,food_plans,
food_pantry,account_data_events,food_recipe_suggestions,telemetry_samples,food_library_recipes,
food_library_runs,food_recalls,food_ingest_cursor,food_off_products,food_official_prices,
food_usda_prices, sharing tables and the household/plan share rows. - Deploy: [deploy/systemd/openvibe-food.service](deploy/systemd/openvibe-food.service) (127.0.0.1:4970,
/etc/openvibe/food.env,/opt/openvibe.food, databaseov_food) and
[deploy/nginx/openvibe.food.conf](deploy/nginx/openvibe.food.conf) (TLS, www→apex, auth/API/forms zones, 4 MB on the
photo route,/internaland/metricsclosed).sudo ovhost deploy food; rollback on a failed/api/ready.
Ownership and Boundaries
Food owns its plans, pantry, recipe suggestions/library, recall rows and cursor, cached OSM/price/Open-Food-Facts data,
share rows and event receipts. It is not the authority for identity (Network), place/store data (OSM), products
(Open Food Facts), recall/price source records (Sources), AI output (AI), the wallet (Network), Chat rooms (Chat), the
search index (Search) or event delivery (Events). The food list, prices and nutrition are carried from Tools' grocery
source, not authored here. Food never calls the FDA, never fetches a delivery app, and never returns raw rows.
Auth and Capabilities
- A person is the session cookie or a Network user token; a cookie-authenticated write must come from
openvibe.fooditself. Per-caller limits are in [server/http/caller-limits.js](server/http/caller-limits.js)
(FOOD_LIMITS_MINUTE/HOUR); the place search has a tighter budget, recipe suggestions 10/person/day. - Capabilities (Contracts):
food.plan.read|write,food.pantry.read|write,food.recipes.suggest(proposed), and
the internalfood.household.readgranted to Chat. Public reads (/places,/foods) need none. A service token
needs the route's capability; a person acting for themself needs none. - Food calls no capability on another service for its public reads. Outbound: OSM, and the named OpenVibe
services (Network, Events, AI, Sources, Chat, Search, Coupons/Reviews for a food box's codes/score). - Errors are RFC 9457 problem+json with stable
food.*codes. WithOV_AI_INTERNAL_URLunset, recipe ideas answer
503food.ai_off; a photo outside the AI/pantry limits is refused before upload.
Pages and API
Pages: /, /near, /plan, /plans/p/:id, /foods, /foods/:slug, /pantry, /pantry/barcode, /pantry/photo,
/recipes, /recipes/library, /recalls, /recalls/:number, /delivery, /updates, plus discovery (/llms.txt,
sitemap, robots, Markdown twins) and /openapi.json.
API /api/v1: GET /ping, GET /places, GET /foods[/:slug], POST /plans, GET /plans[/:id], GET /pantry,
PUT /pantry, POST /recipes/suggest, GET /recipes; the one Share sheet's API
(/api/v1/plans/:id/share, requests/transfer, /share/people, /shared-with-me). /internal/events is loopback;
/internal/households/:id/chat-context is loopback + food.household.read; /api/health, /api/ready,
/release.json, /metrics are machine endpoints.
Conventions
- CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through
[server/http/router.js](server/http/router.js); never log request bodies or a person's photo/pantry content. - New schema = a new
migrations/NNNN_name.sql; never edit one that has run. Keepfood_prefixes (plus the shared
account_data_events/telemetry_samples/share tables). - The only outbound clients are [server/food/upstream.js](server/food/upstream.js) (Nominatim + Overpass — paced
once a second per host, identified withFOOD_OSM_USER_AGENT, cachedFOOD_GEO_CACHE_TTL_MS, bounded, stale
fallback, ODbL attribution on every answer) and the named OpenVibe service clients. Never fetch a user URL (a
delivery link, a listing, an image). - AI output is validated against the food list and priced with Food's own typical prices; it is labelled
AI-suggested. A photo sends only the picture to AI; AI keeps the photo's hash, and a receipt's prices/card/store
details are never read out. - Barcode: only the barcode is sent to Open Food Facts with Food's User-Agent; the picture never leaves the device;
answers cached a day and capped. A person confirms the match before anything is added. - Recalls are shown exactly as Sources holds the report, with openFDA's note (unvalidated, not for medical decisions,
no FDA endorsement);/recalls/:numberis noindex because the source's registry entry asks. - Delivery pages open each app's own search in a new tab — Food fetches none of them and shows no invented prices or
fees. The meal planner is deterministic and a budget is a hard cap. - CSP: add origins to
helmetin [server/app.js](server/app.js). Phone first: every page fits 390 px; no Font
Awesome (shared glyphs). - "Calm reduces, only Off removes": a household/share preference reduces visibility; only an explicit delete removes.
- Empty states use the shared kit; plans/about pages must show real content first (no placeholders).
Key Pitfalls
- OSM policy: one request a second per host, a contact User-Agent, cached answers, an around-query bound, and
attribution. A test points the client at a stand-in; a real call in a test is a bug
([test/places.test.js](test/places.test.js), [test/helpers/osm.js](test/helpers/osm.js)). - A household plan's Chat room is created as the owner through Chat's room API, with members added by subject and
the AI member linked to the plan; Chat reads the plan throughfood.household.read(loopback, service token). Never
read a Chat room directly or post as the AI yourself. - Recipe library: one theme every six hours; every ingredient must be a food slug or the recipe is dropped; only
staff-checked recipes are indexable;/recipes/library.json?since=(oldest first, ≤100) is Blog's feed. - Search documents are written through
openvibe-publishing/search-relay; a hide/unpublish writes a tombstone.
The library's AI text is labelled and noindex until checked. - Photo route allows 4 MB at nginx and resizes to 1280 px on the device first; ten photos a person a day. Do not
raise either without the owner's decision. - Account export/deletion runs through
/internal/eventsonly, is signature-verified and idempotent; the export
carries the person's plans/pantry/suggestions and the deletion erases them (shared plans stay for the others). - Drill mode (
FOOD_DRILL) keeps the OSM warm-up, followers, library, search sync and timers off. - A share is revoked by the owner immediately;
canViewis checked on every read path, not only the share route.
Testing
npm test runs each file in its own process on temp PGlite with a mock Network and fixture OSM/AI/Sources: places and
attribution, planner arithmetic and budget cap, food pages/sitemap, pantry and deterministic matches, recipes/AI
validation/limits, barcode, Open Food Facts, official/USDA prices, recalls, recipe library and Search, household chat,
sharing, boxes, delivery links, account data, SSO/JWKS, limits, secret-leak/session/redirect, layout, cache, nginx
zones and perf budgets. npm run test:pg for PostgreSQL. Run the narrowest file first, then the suite; always
node --check changed files.
Documentation
- [README.md](README.md) — purpose, what it does, owns/does-not-own, dependencies, capabilities, API, data sources and
their terms, configuration, development, acceptance, deploy, account data, security. - [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list.
- [.env.example](.env.example) — variable names only.
- [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md).