Every live store now loads its full Dutchie in‑stock catalog — read live from the same endpoint the storefront serves. June 21, 2026.
The Shop was only showing ~20% of every store's menu — 5,562 of 28,327 in‑stock SKUs fleet‑wide. It wasn't random: the bigger the store, the worse it got. Orillia showed 127 of 1,294 products (10%). Red Hill, 125 of 1,064 (12%). Every large store sat between 10–20%; the small SESH stores around 63–66%.
The tell was that every store loaded ~120 products regardless of its real size — a 108‑SKU store and a 1,294‑SKU store both showed ~120. That's a system ceiling, not a coincidence.
Root cause: the Shop browsed Dutchie's consumer menu() API, which returns ~20 products per query and cannot paginate (every page/cursor argument is rejected). To get more, the app fanned out across ~10 categories (~20 each) → ~120 products total. Past that, there was no way to ask menu() for "the rest."
Why not just call it more? An exhaustive fan‑out (category × brand × strain × subcategory) still tops out near ~50% on big stores and costs ~84 API calls per store — far too slow for a live page, and it still never reaches 100%.
So 100% was structurally impossible through the API the Shop was using. It needed a different door.
The Shop now reads a worker endpoint, /api/catalog, which calls Dutchie's paginated filteredProducts feed (in‑stock only — the same one CannaSync used). The catch: that feed is behind Cloudflare's bot wall for normal calls — so we call it from inside our own Cloudflare worker, which clears the wall. Then:
| Store | Dutchie total | Now loaded | 100% | Old loaded | Old → New |
|---|---|---|---|---|---|
| Orillia | 1,294 | 1,294 | ✓ | 127 | 10% → 100% |
| Red Hill | 1,064 | 1,064 | ✓ | 125 | 12% → 100% |
| Mohawk | 978 | 978 | ✓ | 130 | 13% → 100% |
| Waterloo | 970 | 970 | ✓ | 127 | 13% → 100% |
| Timmins | 1,004 | 1,004 | ✓ | 142 | 14% → 100% |
| Sault Ste. Marie | 886 | 886 | ✓ | 125 | 14% → 100% |
| Etobicoke | 862 | 862 | ✓ | 134 | 15% → 100% |
| Brockville | 848 | 848 | ✓ | 129 | 15% → 100% |
| Aurora | 813 | 813 | ✓ | 123 | 15% → 100% |
| Keswick | 843 | 843 | ✓ | 127 | 15% → 100% |
| Porcupine | 856 | 856 | ✓ | 134 | 16% → 100% |
| Kincardine | 807 | 807 | ✓ | 132 | 16% → 100% |
| Sudbury | 803 | 803 | ✓ | 131 | 16% → 100% |
| Toronto | 682 | 682 | ✓ | 119 | 17% → 100% |
| Strathroy | 696 | 696 | ✓ | 127 | 18% → 100% |
| Cambridge | 760 | 760 | ✓ | 134 | 18% → 100% |
| Sarnia | 750 | 750 | ✓ | 132 | 18% → 100% |
| Brantford | 691 | 691 | ✓ | 127 | 18% → 100% |
| Dundurn | 690 | 690 | ✓ | 123 | 18% → 100% |
| Kitchener | 635 | 635 | ✓ | 118 | 19% → 100% |
| Rymal | 658 | 658 | ✓ | 126 | 19% → 100% |
| Kitchener (Doon) | 703 | 703 | ✓ | 118 | 19% → 100% |
| Wasaga Beach | 653 | 653 | ✓ | 133 | 20% → 100% |
| Port Colborne | 618 | 618 | ✓ | 126 | 20% → 100% |
| Binbrook | 556 | 556 | ✓ | 119 | 21% → 100% |
| Peterborough | 554 | 554 | ✓ | 121 | 22% → 100% |
| North York | 532 | 532 | ✓ | 119 | 22% → 100% |
| Hillside | 460 | 460 | ✓ | 113 | 25% → 100% |
| Welland | 506 | 506 | ✓ | 125 | 25% → 100% |
| Rosedale | 457 | 457 | ✓ | 118 | 26% → 100% |
| Ottawa | 486 | 486 | ✓ | 126 | 26% → 100% |
| Goulais River | 439 | 439 | ✓ | 117 | 27% → 100% |
| London (Adelaide) | 669 | 669 | ✓ | 114 | 27% → 100% |
| London | 425 | 425 | ✓ | 114 | 27% → 100% |
| Oshawa | 455 | 455 | ✓ | 124 | 27% → 100% |
| Thunder Bay | 415 | 415 | ✓ | 121 | 29% → 100% |
| Cobourg | 340 | 340 | ✓ | 104 | 30% → 100% |
| Elliot Lake | 399 | 399 | ✓ | 122 | 30% → 100% |
| Tottenham | 316 | 316 | ✓ | 101 | 32% → 100% |
| Kapuskasing | 330 | 330 | ✓ | 107 | 32% → 100% |
| Port Perry | 346 | 346 | ✓ | 115 | 33% → 100% |
| Carlisle | 321 | 321 | ✓ | 115 | 36% → 100% |
| Stoney Creek | 320 | 320 | ✓ | 117 | 36% → 100% |
| SESH Main St | 115 | 115 | ✓ | 73 | 63% → 100% |
| SESH King St | 111 | 111 | ✓ | 70 | 63% → 100% |
| SESH Komoka | 108 | 108 | ✓ | 70 | 65% → 100% |
| SESH Dalhousie | 103 | 103 | ✓ | 68 | 66% → 100% |
Two locations correctly show 0 — Laurelwood and St. Catharines return totalCount: null from Dutchie itself (no live menu yet — coming‑soon). 0 is the right answer there, not a miss.
Going to 100% surfaced a second, hidden bug: checkout blocked cart items as "no longer available." Cause was ours — the catalog built a placeholder variant id (id|size) where Dutchie's real one is id~size, and the price step searched Dutchie by the product's display name (which doesn't match). Both are now resolved by matching on the option and trusting Dutchie's own add‑confirmation. Re‑tested end‑to‑end on a live store: cart → checkout → Dutchie hand‑off opens with the right items and prices. Genuinely unavailable items still correctly block.
productsCount.