All case studies

DEV3 Technologies · own product

DEV3 AI Upsell & Cross-Sell Cart Drawer: AI add-ons and bundle offers that checkout honours

Our own Shopify app: a fast slide-out cart that suggests AI-ranked add-ons from the store's catalog and advertises bundle offers using the same rule engine that Shopify's checkout runs to apply them.

Industry
E-commerce (Shopify)

Live project preview

Client systems are private — screenshots shown where permitted.

DEV3 Technologies · own product — screenshots
The drawer after an add to cart: free-shipping bar, bundle progress message and three AI-picked add-ons, each with a short reason

The drawer after an add to cart: free-shipping bar, bundle progress message and three AI-picked add-ons, each with a short reason

The challenge

A cart drawer is the natural place to suggest add-ons and bundle offers, but it sits on every page of the store, so it has to be small and stay out of the theme's way. Shopify's app-embed limit is 10 kB of JavaScript. The drawer's offers also have to be true. If it says "Add 1 more to unlock 15% off", checkout must apply exactly that discount, yet the drawer's message comes from the app server while Shopify applies discounts inside its own WebAssembly sandbox. Recommendations need to be relevant without slowing the drawer down, and still work when the AI is slow, unavailable or over the plan limit. Merchants also want to know whether the add-ons led to orders, without the app collecting shopper data. We wanted one app that handles all of this.

Our solution

The app is a Shopify embedded app on React Router 7 and Polaris, with Postgres (Prisma), Redis and a BullMQ worker. It has three extensions: a theme app embed for the drawer, a Discount Function for bundle discounts, and a Web Pixel for attribution. The AI engine is the Google Gemini API.

Key design decisions

  • One rule engine, two runtimes. The bundle engine is one dependency-free JavaScript module. It is compiled into the Discount Function's WebAssembly and imported directly by the app server, so the drawer's "unlocked" and "add 1 more" messages come from the same allocation that checkout performs. Collection membership is checked live at checkout through inCollections, with the collection IDs passed as input-query variables from a discount metafield.
  • Predictable bundles. Rules are evaluated in priority order and complete sets are allocated greedily from the cheapest matching units. A unit is never discounted by two bundles, and a malformed rule is dropped rather than disabling the whole discount.
  • A drawer that fits the app-embed budget. The embed is a 5.2 kB bootstrap that intercepts the theme's add-to-cart requests and forms, then lazy-loads the drawer UI on idle or first use. Bootstrap and UI together are about 9.5 kB of gzipped JavaScript plus 2.3 kB of CSS. The UI renders in a Shadow DOM, so theme styles can't leak in and the drawer's styles can't leak out.
  • AI ranking with a fallback. Candidates are prefiltered from a Postgres catalog index to those that are active, published, in stock and not already in the cart. gemini-2.5-flash ranks up to 30 of them and writes a short reason for each pick. Observed latency is about 1.6 s, with an 8 s timeout and no retries. Any failure falls back to a rule-based ranking based on shared collections, tags and add-on price, and rankings are cached in Redis per cart.
  • Secured storefront endpoint. The drawer talks to the app only through the Shopify App Proxy. Every request's HMAC signature is verified, and requests are rate-limited per IP and per shop.
  • Anonymous attribution. A strict-sandbox Web Pixel forwards the drawer's custom events and checkout_completed line items. An order line counts as attributed when the same browser added that product from a recommendation within 7 days. No customer names, emails, addresses or IDs are stored.

Architecture

The theme app embed loads the bootstrap, which reads the cart through Shopify's AJAX API and posts it to /apps/upsell/recommend. On the server, the proxy route verifies the signature and then runs recommendations (catalog index, scoring, AI rank, Redis cache) and bundle status (the shared engine) in parallel. The worker keeps the catalog index current with a bulk-operation full sync, debounced product webhooks and a daily resync, and publishes bundle rules to a single automatic app discount. The admin has Recommendations with a "Try it" tester, a bundle editor with product and collection groups, Countdown & design with a live drawer preview, Analytics, and Billing.

Results

The app is in final QA before submission to the Shopify App Store; real-store testing is the next step. What has been built and verified so far:

  • 72 Vitest tests for the app, and typecheck reports 0 errors
  • 22/22 Discount Function tests, including a real WebAssembly build with shopify app function build (works offline) run through Shopify's function runner. The compiled module is 7.2 kB.
  • 37/37 Puppeteer checks on the drawer, covering AJAX and form add-to-cart interception, the Shadow DOM mount, recommendation cards, bundle progress and unlock, and the free-shipping bar
  • shopify theme check reports 0 offenses on the theme extension
  • A 5.2 kB bootstrap that keeps the drawer under the 10 kB app-embed JavaScript limit, with about 9.5 kB of gzipped JavaScript and 2.3 kB of CSS in total
  • AI ranking with gemini-2.5-flash observed at about 1.6 s, with a rule-based fallback on any failure

Related case studies