Bundle discounts are easy until subscriptions show up. Then every off-the-shelf tool hits the same wall: the discount applies to the first order and vanishes on renewal, or worse, applies to renewals it shouldn't. For a DTC supplement brand we work with (two hero products that genuinely work better together) the requirement was precise:
- Buy product A and product B together: 10% off both.
- If they're subscriptions, both on the same delivery cadence. A monthly A paired with a bi-weekly B is not a bundle, it's two subscriptions.
- The discount must survive renewals indefinitely, without re-running any logic.
- And a shopper assembling the same two products by coincidence shouldn't trigger the offer that the storefront bundle widget presents deliberately.
That last one is the interesting bit. The theme's bundle widget stamps a line-item property when it builds the pair. The discount function, a Shopify Function running cart.lines.discounts.generate.run inside Shopify's infrastructure, requires that stamp to match a live campaign before it pays out. No stamp, no discount, even for an identical cart. Campaigns can opt out of the gate when a merchant wants organic pairs to qualify; the point is that it's a decision, not an accident.
Why a Function and not a script or an app
Three properties you can't get any other way:
It runs in the money path, on Shopify's servers. No third-party API call at checkout, no latency budget spent on a vendor, nothing to go down on Black Friday.
Renewals are free. With appliesOnSubscription and an uncapped recurring cycle limit, Shopify copies the discount onto the subscription contract at checkout. Renewal orders carry it automatically. The function never re-executes, so there's nothing to break twelve months in.
Untrusted input stays untrusted. Anything a theme writes into the cart (line properties, attributes, a _bundlePrice hint) is attacker-controllable; a curious shopper can set it in DevTools. The function treats those as claims and validates every one against trusted, merchant-owned data (metafields the storefront can't write) before discounting a cent. Cap the discount server-side and the worst a tampered cart can do is receive the legitimate offer. We wrote that rule up separately: why we treat _bundlePrice as attacker input.
This is the kind of store-specific logic Shopify Functions are for, and the kind of build that belongs on a Checkout Suite Care Plan instead of a volume-priced upsell invoice.
The quantity trick
One subtle failure mode: quantity mismatches. Two of A and one of B is one bundle plus a stray, not two bundles. The function discounts min(units(A), units(B)) matched units on both sides, so inflating one line never over-claims the offer. It's two lines of code and it closes a hole that we've seen live on stores running far more expensive tooling.
What it replaced
The brand was quoted the usual volume-priced SaaS for this. What that fee actually pays for is operations, not the logic. Instead they own a ~200-line function with a fixture suite that runs against the compiled WebAssembly binary (the exact artifact Shopify executes) on every change, plus an admin for campaigns and attribution analytics. The recurring cost is a flat care plan, not a percentage of their growth.
Caffeine and Commerce builds custom discount functions and checkout suites for Shopify stores. If your bundle logic has a "but" in it that no app quite handles, that's our favorite kind of brief. Start a project.


Comments
Every comment here comes from a verified email. Write yours, confirm from your inbox, and it's live.
Loading comments…