Mine — grocy — 2026-09-01

Reconcile — against a settled host position

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.

Findings — data model

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.

Findings — architecture

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.

Open question — who operates the pantry

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.

Caps — what this mine did not cover