DEV Community

Cover image for How we built a live has_active_wishlist flag for Shopify (and why it was harder than it looks)
Shaun Reynolds
Shaun Reynolds

Posted on

How we built a live has_active_wishlist flag for Shopify (and why it was harder than it looks)

Most Shopify wishlist apps have the same quiet flaw: the wishlist data lives in the app, and the merchant's email platform never sees it. The store knows who wants what — but can't email on it.

We built WishList Ninja to fix exactly that. Here's how the interesting bits work.

The core idea: profile properties, not just events

Everyone fires events ("Added to Wishlist"). Events are fine for triggering flows, but useless for segmentation — you can't build a clean segment of "people with items saved right now" from a two-month-old event.

So on every save/remove we write profile properties to Klaviyo, Mailchimp or Omnisend in real time:

  • has_active_wishlist — true only while the wishlist has items in it
  • wishlist_count — items currently saved
  • wishlist_value — what they're worth
  • top_saved_item — the product they want most

That first flag sounds trivial. It isn't.

Why a boolean was the hard part
**
has_active_wishlist has to flip **false
again — otherwise it's just "has ever used the wishlist," which is worthless. So every path that empties a wishlist has to re-evaluate it:

  • shopper removes the last item ✅
  • shopper buys the last item (orders webhook marks items purchased) ✅
  • guest list merges into an account ✅
  • and the subtle one: saved collections don't count. A shopper following a collection but holding zero items is not "active intent." Items only.

One rule, five code paths. The rule lives in exactly one function so the paths can't drift apart.

*7 flow triggers from 2 webhooks
*

The email moments merchants actually want are mostly store-side changes, not shopper actions:

Added to Wishlist · Price Drop · On Sale · Low Stock · Back in Stock · Collection Saved · New in Saved Collection

Five of those seven come from diffing products/update webhooks against what shoppers have saved: price went down → find everyone holding that product → fire Price Drop into their ESP. No polling, no cron scanning the catalog.

*Multi-ESP fan-out without domino failures
*

Supporting three ESPs means three APIs that can each be down, rate-limited, or misconfigured. Early version: Klaviyo throwing meant Mailchimp and Omnisend never got the event. Bad.

Now each delivery is isolated — a failure is caught and logged, and the job only retries if nothing was delivered. One dead API key can't block the other platforms.

*The storefront budget: ~12KB
*

The whole storefront (hearts on product cards, slide-out drawer, wishlist page, share links, save-for-later on the cart) is vanilla JS, minified to under ~12KB. No framework, no fonts, no CSS libraries. Core Web Vitals are a ranking signal; an app that slows the store is stealing from the merchant to fund itself.

*Stack
*

Laravel + MySQL + Redis on Cloudways, Shopify theme app extension for the storefront, Mailjet for transactional email — and a large amount of Claude Code for the build itself.

Launching on the Shopify App Store now. If you run (or build for) a Shopify store, I'd love your take: wishlist-ninja.com 🥷

Happy to go deeper on any of this in the comments — especially the webhook diffing.

Top comments (0)