What it does
The household app scans a barcode (EAN-8, EAN-13, UPC) and asks barcodes.lookup what it is. The server answers in this order:
- a learned barcode: the list rule most homes filed the code under;
- the product catalog behind an adapter: Open Food Facts in production, a synthetic fake everywhere else. The product's categories map to a list rule (
ruleForCategories), and its net content to one pack in the rule's unit (9000 ml → 9 L); - otherwise
{ status: "unknown" }. A catalog that times out (3 s) or fails also answers unknown, so the app goes on from the list instead of waiting.
After an unknown scan the app shows the list; the item the household picks is sent to barcodes.learn as the code and the rule id, nothing else. The app also keeps the code on the device, so the next scan works offline.
Only the code reaches the catalog, and from the server, never from the phone (ADR-0019). The app shows the ODbL attribution next to every catalog result.
Procedures
| Procedure | Who | Failures |
|---|---|---|
barcodes.lookup · GET /api/barcodes/{code} | anyone, no account | BAD_REQUEST (not 8 to 14 digits) |
barcodes.learn · POST /api/barcodes/learned | anyone, no account | BAD_REQUEST (shape, any extra field, rule on no published list) |
learnInput is strict: a payload that carries a household id, or any other field, is refused.
Data and isolation
| Table | Tenant access |
|---|---|
learned_barcode | select and insert for every trusted session; update of votes and learned_on only; no delete. Policies on app.session_trusted(), like feature_flag |
A row is (code, rule_id), votes and learned_on (a date, never a finer time). There is no Organization, user, device or household column. Teaching a code twice for the same rule adds a vote; the lookup takes the rule with the most votes, so one wrong answer cannot replace the others.
Migrations: 20261106213000_learned_barcode (table, policies) and 20261106213001_learned_barcode_grants.
Configuration
| Variable | Values | Default |
|---|---|---|
BARCODE_CATALOG | fake, openfoodfacts | fake |
Production sets BARCODE_CATALOG=openfoodfacts. Unset or invalid keeps the fake, which knows 4005906003021 and 4008501011009 (still mineral water, a crate of 6 × 1.5 L), 4000000000017 (spaghetti) and 4000000000024 (chocolate, no rule fits).
Tests
node scripts/test-local-postgres.mjs exec pnpm --filter @varu/api exec vitest run src/features/barcodes