BACKEND · v186
Live Dutchie data layer — cut the cache
2026-06-09 · backend architecture update · no frontend change (v185 remains the latest UI) · promos untouched
The problem
The site read product data from a D1 scrape cache — a daily copy of Dutchie kept in our own database. A copy goes stale the instant Dutchie changes, which caused wrong prices (From $Infinity), search that ranked rolling papers above flower, an unreliable "available at N stores" widget, and a fragile hand-patch workflow. Root cause: the cache was a second source of truth that drifts.
The decision (Solution A)
- Dutchie Plus is the sole source of truth. No product data persisted as a second copy.
- A Cloudflare Worker is the live gateway. Cloudflare-origin requests clear Dutchie's bot-challenge; the Hetzner datacenter IP (
77.42.66.58) is blocked — so the Worker, not Hetzner, is the right caller.
- Store-first, no skip → every page is scoped to one retailer, which is what makes the cache unnecessary.
Built + live-verified this update
Isolated preview worker — sessions-availability-edge-demo on Darryl's account, own empty D1. Proven it cannot see prod data, so nothing it does affects the live promos campaign.
- ✓
/api/v2/products — live ranked search/browse. Verified: "blueberry" → flower first (no papers), "510" → cartridges, "gummies" → gummies, "pink kush" → Pink Kush flower #1.
- ✓
/api/v2/product-stores — live cross-store "Available at N stores" by enterpriseProductId (not name-matching).
- Existing live
menu() + checkout-by-IDs round out the data layer.
What V1–V4 confirmed
- Store-scoped is pure-live and fast (menu ~130–540 ms; native
brandId filter; enterpriseProductId on every product; checkout by IDs works).
- One change: Dutchie has no cheap cross-retailer availability query, so "available at N stores" needs a wholesale-rebuilt index (full rebuild on a schedule, hint-only, cart re-validates live) — the correct kind of cache, not the drift-prone scrape.
Search & filters — status
Search ranking — FIXED. The v2 ranker replaced the broken substring match: "blueberry" → flower (not rolling papers), "510" → cartridges, "gummies" → gummies, "pink kush" → Pink Kush flower #1.
Filter pass-through — partially wired. Dutchie's MenuFilter surface, mapped this session:
| Filter | Native in Dutchie? | Live in /api/v2/products? |
| search | ✅ | ✅ |
| category | ✅ | ✅ |
| brand (brandId) | ✅ | ✅ |
| strain type (indica/sativa/hybrid) | ✅ | pending |
| subcategory | ✅ | pending |
| potency (THC / CBD) | ✅ | pending |
| effects | ✅ | pending |
| price range (min/max) | ❌ not native | worker-side, pending |
| sort (price / potency) | ❌ not native | worker-side, pending |
| weight / on-sale | ❌ not native | worker-side, pending |
The first three pass straight through to Dutchie and are live; strain/subcategory/potency/effects are native to Dutchie and wire the same way (pending); price-range/sort/weight/on-sale aren't native — they get applied in the worker after fetch, the same place the ranker runs. The mobile filter UI (the v185 bottom-sheet) is a separate frontend fix already shipped in v185 — untouched by this backend update.
Test plan — 50 QA questions (Opus)
The use-case checklist to run before this goes live. Each maps to a pass/fail test against the live data layer; status filled in as tests run.
A. Store entry & scoping
- Age-gate to mandatory store-pick blocks all browsing until a store is chosen (no skip, no default-to-all)?
- The chosen retailerId persists across refresh, deep-link, and back-button so every page stays store-scoped?
- Deep-linking to a PDP/shop URL with no store selected routes to the picker, not a blank/empty page?
- Switching store mid-session clears/re-validates the cart and refreshes prices to the new store?
- The store list matches the canonical dispensary CSV (UUID + dispensaryId), with closed/unmapped stores excluded?
B. Search & ranking
- Search returns the right #1 for name queries (blueberry to flower, not rolling papers)?
- Multi-word queries work regardless of word order (blue dream / dream blue)?
- Accessories demoted unless intended (510 to cartridges; 510 battery to batteries)?
- A brand-name query surfaces that brand's items even when the brand isn't in the product name?
- Dutchie's fuzzy zero-token-match items are suppressed from the top?
- Empty/whitespace query gives a sensible browse fallback, not an error or empty list?
- Search latency stays acceptable (under ~700 ms p95) on the live path?
C. Filters
- Category filter returns only that category and combines correctly with search?
- Strain-type / potency / subcategory / effects return correct subsets (once wired)?
- Worker-side price-range bounds min/max correctly across a product's variants?
- Sort (price/potency, asc/desc) orders correctly and stays stable across pagination?
- Multiple filters AND together correctly (FLOWER + INDICA + under $40)?
- Removing/clearing a filter restores the full set?
- Zero-match filter combos show a clean empty state, not an error?
D. Browse & pagination
- Category browse paginates past Dutchie's ~20/page with no dupes, no gaps, correct total?
- Cards show correct image, name, brand, min price, in-stock state?
- Out-of-stock products handled consistently per policy (hidden vs sold-out)?
E. Product detail (PDP)
- PDP shows live price + the exact variants/options Dutchie has at that store?
- Multi-variant (3.5g/7g/28g) selection updates price/stock correctly?
- PDP price equals what Dutchie charges at checkout (no drift)?
- Potency/strain/brand/description render live, with graceful blanks when missing?
F. Cross-store Available at N stores
- Lists exactly the stores that stock the product (by enterpriseProductId, not name)?
- Each store's price + qty is live and equals that store's own menu?
- N-of-M counts accurate and sorted by distance/price as intended?
- Behavior verified at 0 / 1 / 50 stocking stores (latency, subrequest budget)?
- A store that errors (404/timeout) is skipped gracefully, not crashing the widget?
- With the rebuilt index live, the cart re-validates the chosen store's price before adding?
G. Cart & Cannabis Act
- Add-to-cart carries product_id + variant_id (not name) through to checkout?
- 30g dried-equivalent cap correct across flower / pre-rolls / vapes / edibles / beverages?
- Gram-math bug fixed (a 355 ml beverage must not count as 355 g)?
- Cart blocks checkout above 30g with a clear message?
- Steppers / remove / subtotal update correctly and match Dutchie pricing?
- Cart is store-scoped (adding a different store's item behaves per policy)?
H. Checkout
- Checkout creates the Dutchie checkout, adds items by ID, lands on the dispensary subdomain with the right items?
- Our cart total equals the Dutchie checkout total at handoff?
- For a Cova-only / not-in-Dutchie SKU, it fails safe (clear message), not silently empty/substitute?
- Checkout works across several different stores, not just the one tested?
I. Live correctness & freshness
- All live endpoints no-cache (or correct short-TTL) so a Dutchie price change reflects within the window (no stale edge cache)?
- No remaining user-facing read path to the old D1 cache?
- A Dutchie stock/price change appears on refresh within the expected TTL?
J. Performance & resilience
- Cross-store fan-out stays within the Worker's 50-subrequest + CPU limits at 50 stores?
- Under Dutchie 429 rate-limiting: backoff/partial results, not a hard fail?
- If seshweed-api (the Dutchie proxy) is down, the site degrades with a clear message, not a blank page?
K. Isolation, security, compliance
- Proven the demo worker cannot read/write the prod/promos D1 under every endpoint?
- The hardcoded Dutchie token is rotated into a secret, and new surfaces meet the floors (Cannabis Act purchase-on-Dutchie, AODA/WCAG 2.1 AA)?
What's next
- Cross-store availability via the rebuilt index (50-store scale).
- Point a demo
/vN/ front-end at the live worker; verify age-gate → store → search → cart → checkout.
- Rotate the Dutchie token into a Wrangler secret.