docs/tickets/pms-roadmap.md holds a standing not yet on standing
inventory: "manual entry is the number-one abandonment cause for pantry apps,
and half-updated inventories are worse than none, because trust dies." Its
named trigger is purchase units, which partly shipped (D-105, "the remainder
deliberately held").
Severity WARNING, not BLOCKER: the roadmap states a position with a named trigger, not a locked decision, and nothing in D-116/D-117/D-118 forbids inventory. Surfaced for Rex, never auto-adopted. Four things have moved since that call was written, all verified below.
G-01 · The barcode is an identity device, not a size device. grocy ships an
Open Food Facts lookup as its default (README, config-dist.php:74; default
since changelog/77_4.4.0_2025-01-24.md). Its single request asks for
product_name,image_url,product_name_<locale> and nothing else
(plugins/OpenFoodFactsBarcodeLookupPlugin.php:20). It does not request
quantity. Unit falls back to a user preset and the purchase-to-stock factor is
hardcoded to 1. After ~19 months as the shipped default, size is not taken
from the lookup at all. Independently corroborated by hand the same day: OFF
returned quantity: "144 fl oz" for barcode 0049000028911, describing a
12-pack rather than a can. Implication: a scanner does not answer Seq 4 by
itself. Sizes still come from Rex.
G-02 · Size belongs on the barcode row, not the product row. Barcodes are
their own table, many per product, each carrying its own amount and unit:
product_barcodes(product_id, barcode, qu_id, amount, shopping_location_id, last_price, note) (migrations/0103.sql, plus one later ADD note TEXT; no
later CREATE supersedes it). Scanning a 12-pack code means 12, scanning the
single means 1, same product. Hand-entered once, free thereafter. This is the
answer to G-01 and the shape the DFC buy: "tub" gap wants: per-barcode, not
per-good.
G-03 · Current state and the ledger are separate, and transactions are undone
rather than deleted. stock is one row per entry carrying its own
best_before_date, purchased_date, open, opened_date, price;
stock_log is append-only with transaction_type, undone, undone_timestamp
(migrations/0049.sql). DFC's orders table already reaches for append-only by
the same instinct (D-118), so this is a familiar shape, not a new one.
G-04 · inventory-correction is the answer to the roadmap's own objection.
Of nine transaction types (services/StockService.php:11-19) four earn their
place in a small household: purchase, consume, product-opened,
inventory-correction. The last exists purely to say "actually there are
three." Drift is expected and recoverable rather than fatal, which is precisely
what half-updated inventories are worse than none assumes is impossible.
Overbuilt for two people: self-production, transfer_from, transfer_to,
stock-edit-new, stock-edit-old (multi-location and home production).
G-05 · "Opened" is first class, with its own shelf life.
products.default_best_before_days_after_open, open/opened_date per stock
entry, and treat_opened_as_out_of_stock defaulting to 1 — grocy's default
is that an opened package counts as out of stock for reordering
(migrations/0207.sql). Cheap to adopt, one integer per good, and it is the
harissa-dying-in-the-fridge-door case Rex named.
G-06 · The reorder engine is one expression, and it opts goods in. The
stock_missing_products view computes min_stock_amount - SUM(on hand) + (opened, when the product treats opened as out), gated on
WHERE p.min_stock_amount != 0. Goods with no threshold never generate list
rows. This is the graduated adoption path: ten tracked goods, 350 left
alone, which defuses the all-or-nothing abandonment risk the roadmap names.
G-07 · Overbuilt for a small household. products carries 31 columns:
locations and transfers, freezing/thawing shelf life, calories, product groups,
shopping locations, stock label printing, parent/sub products, no_own_stock,
move_on_open. One outlier is genuinely interesting —
enable_tare_weight_handling / tare_weight weighs the jar and subtracts the
empty to compute what is left in a pour, which the roadmap called
uncomputable. It needs a kitchen scale in the loop, so it is a real mechanism
rather than a free one.
G-08 · grocy puts derived logic in the database; DFC derives at build time.
54 table definitions against 53 distinct views (defined 228 times) and 67
distinct triggers (defined 121 times), across 250 numbered migrations run by a
migrations table plus filesystem sort (services/DatabaseMigrationService.php).
Views are tiered: base tables, then *_current/*_resolved, then a uihelper_*
family denormalising one screen into one row, then cache__* materialised
tables kept fresh by triggers on stock_log (migrations/0226.sql).
Controllers are consequently near-empty — the whole shopping list screen is
$this->DB->uihelper_shopping_list()->where(...)
(controllers/StockController.php:429).
DFC's shape for contrast: 159 recipe data files, 1913 lines of build scripts
materialising _data/goods.json, 15 templates, 1797 lines of browser JS, and a
135-line Worker over 2 tables with no views and no triggers.
G-09 · DFC already has grocy's view layer, at a different moment.
build-goods.mjs materialising goods.json from the corpus is products_view.
Build time rather than query time. This works because every derived value in DFC
depends only on the recipe corpus, which changes at commit time.
G-10 · Inventory is the first DFC state that changes between builds, and that is its real cost. A stock level moves while someone is in the kitchen, hours after the last build, so build-time materialisation cannot reach it. Inventory therefore requires a runtime derivation layer the 135-line Worker does not have. That layer, not the data model, is the price of the feature.
G-11 · Do not copy the view tier. D1 is SQLite, so grocy's approach is
literally available and stock_missing_products would port unchanged. But 53
views cost 228 definitions because any upstream change forces every dependent
view to be rewritten — visible in one file at migrations/0226.sql, which adds
a trigger and then immediately drops and recreates
uihelper_stock_current_overview. At two-person scale, compute in the Worker
and skip the summary layer until a query is measurably slow.
G-12 · The host's own schema note fires on the first inventory commit.
worker/schema.sql reads: "No migrations framework — one file, IF NOT EXISTS;
adopt migrations only if this ever grows a third shape." It holds two shapes
today, orders and shelf. Inventory is the third.
G-13 · userfields is runtime schema extension without migrations. An
EAV pair (userfields declaring entity/name/caption/type, userfield_values
holding values) lets a user add fields to any entity at runtime. Noted against
the board's Household preferences — a store Rex edits row, whose UI half is
deferred by D-116. Not proposed: D-116 killed the write-back machinery for
reasons unrelated to schema flexibility, so this is a mechanism looking for a
trigger that has not fired.
Raised by Rex 2026-09-01, unresolved and not to be answered by a session: "ultimately i think it might be more of a one person pantry. i dont see brian going through everything… it may be its what i do. or he picks recipes and then i approve pantry or something. im not sure yet. i need to discuss with him."
Load-bearing for everything above. The roadmap's abandonment objection is a two-person discipline problem, and it weakens considerably under a one-operator model. The system currently assumes two: D-116 put both household emails behind the Access gate, and D-118 made shelf answers shared across devices precisely so either person could answer. A one-owner pantry with Brian picking dishes is a different shape than what is built. Settle with Brian before any inventory work is sequenced.
StockService.php, and OpenFoodFactsBarcodeLookupPlugin.php.CREATE plus ALTER rather than reading all
250 migrations individually.quantity coverage is unmeasured. Open Food Facts' search endpoint
returned its "Page temporarily unavailable" HTML on every attempt across the
session, so the population rate of that field is unknown. The product-by-
barcode endpoint worked throughout. Rex's next step was a 10-barcode hand test
of recognition rather than size.public/, 20k lines), recipes, chores, batteries,
tasks, users and permissions. All out of lens.