DEV Community

RAXXO Studios
RAXXO Studios

Posted on • Originally published at raxxo.shop

Shopify Functions Replaced 8 Apps In One Saturday

  • Cancelled 8 paid apps in one Saturday and dropped 180 EUR/month off the app bill to zero recurring spend

  • Shopify Functions ship as compiled wasm with a 256KB binary cap, 5MB sources, and sub-millisecond cold starts

  • Ported a bundle builder, a BOGO discount, payment hiding by cart total, and geo-locked shipping using cart-transform, discount, and customization Functions

  • Local shopify app function run plus structured logs replaced four app-vendor dashboards I never wanted to log into

I spent one Saturday porting eight apps to Shopify Functions. Monthly app spend went from 180 EUR to zero. The merchant gets a faster cart and I get one repo to maintain instead of eight admin panels.

The 8 apps I cancelled and what they were costing

The store had eight apps stacked over two years. Each solved a real thing on the day it was installed. Together they were bleeding 180 EUR per month and adding three to four hundred milliseconds of cart logic that ran inside vendor webhooks.

Here is what the eight did, generically, so I do not name vendors:

  1. A 19 EUR/month bundle-builder that swapped three line items into a single discounted parent.

  2. A 24 EUR/month BOGO app for "buy 2 get 1 free" on a single collection.

  3. A 14 EUR/month app that hid Cash on Delivery for orders above 200 EUR.

  4. A 19 EUR/month app that hid PayPal for B2B customers tagged wholesale.

  5. A 29 EUR/month app that geo-restricted certain SKUs from shipping to two specific countries.

  6. A 19 EUR/month app that re-named shipping methods based on cart weight.

  7. A 29 EUR/month tiered-discount app that auto-applied 10/15/20 percent at 50/100/200 EUR cart totals.

  8. A 27 EUR/month gift-with-purchase app that injected a free SKU once a threshold hit.

That is 180 EUR/month, 2160 EUR/year, for logic that lives inside the cart. Every one of those apps was doing the same dance: receive a webhook, run their own server-side logic, mutate the order or the checkout, write to a vendor database I cannot inspect. Six of the eight had overlapping settings panels, two of them silently disagreed about which discount took priority when both fired, and three asked for permissions I did not want a third party holding (read_customers, write_orders, read_inventory).

The latency was the part nobody talks about. Each external call adds 200 to 400 milliseconds. Two of them ran in series during checkout init, which I could see in the network panel as a visible pause before the express-pay buttons painted.

The actual logic in those eight apps, when you write it down, is roughly 600 lines of TypeScript. Six hundred lines and 180 EUR/month versus six hundred lines and zero EUR/month, running inside Shopify's own infrastructure. The decision was not hard. The Saturday was about whether Shopify Functions could actually replace the eight surfaces, not whether I should bother trying.

The 4 Function types that map to 95 percent of cart apps

Shopify Functions is a free feature on Shopify Plus and Advanced plans. It runs your code as compiled WebAssembly inside the platform, not on a server you rent. Four extension points cover almost every cart-side app on the App Store:

cart-transform: Merge multiple line items into one (bundles), split one line item into several (kits), or update line-item properties on the fly. The only function type that actually reshapes what is in the cart.

discount: Apply product, order, or shipping discounts based on cart contents. Replaces 90 percent of "automatic discount" apps. You return a list of discount targets and the platform applies them. Unlike legacy script editor, multiple discount Functions can stack and you control combine rules per discount.

payment-customization: Hide, re-order, or rename payment methods at checkout. This is what kills the "hide COD over 200 EUR" and "hide PayPal for tag X" apps in one shot.

delivery-customization: Hide, re-order, or rename delivery options. Same shape as payment-customization, different surface. Geo-restrict, weight-based renaming, B2B-only methods, all here.

What Functions cannot do, on purpose: write to external databases, call third-party HTTP APIs, run on a schedule, modify orders post-checkout, or touch customer PII outside the input you ask for. They are pure functions of (input) -> mutations. If the logic needs an outside lookup, you need an app or a webhook, not a Function.

The mental model that finally clicked: Functions are a query against the cart that returns a list of mutations. You ask GraphQL for the fields you need, you return the smallest possible set of changes. That is the whole API surface. Once I stopped trying to do "fetch this product, then check that", and started thinking "what fields do I need declared in the input query, and what mutations do I return", the porting got fast.

The actual ports: code that replaced four of the apps

The eight ports broke down into four distinct shapes. Here are three of them as real code.

Bundle builder (cart-transform, Rust), replacing the 19 EUR/month bundle app:


use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function_target(query_path = "src/run.graphql", schema_path = "schema.graphql")]
fn run(input: input::ResponseData) -> Result {
    let bundle_parent = input.cart_transform.bundle_parent_variant_id;
    let children: Vec<_> = input.cart.lines.iter()
        .filter(|l| l.merchandise.bundle_child.is_some())
        .map(|l| output::CartOperationMergeOperation { cart_line_id: l.id.clone(), quantity: l.quantity })
        .collect();
    if children.len() < 3 { return Ok(output::FunctionRunResult { operations: vec![] }); }
    Ok(output::FunctionRunResult { operations: vec![output::CartOperation::Merge(output::MergeOperation { parent_variant_id: bundle_parent, cart_lines: children, ..Default::default() })] })
}

Enter fullscreen mode Exit fullscreen mode

BOGO 2+1 (discount, JS), replacing the 24 EUR/month BOGO app:


import { DiscountApplicationStrategy } from "../generated/api";

export function run(input) {
  const eligible = input.cart.lines.filter(l => l.merchandise.product.inCollection);
  const totalQty = eligible.reduce((s, l) => s + l.quantity, 0);
  if (totalQty < 3) return { discounts: [], discountApplicationStrategy: DiscountApplicationStrategy.First };
  const freeCount = Math.floor(totalQty / 3);
  return { discounts: [{ targets: eligible.map(l => ({ productVariant: { id: l.merchandise.id, quantity: freeCount } })), value: { percentage: { value: "100.0" } } }], discountApplicationStrategy: DiscountApplicationStrategy.First };
}

Enter fullscreen mode Exit fullscreen mode

Hide COD over 200 EUR (payment-customization, JS), replacing the 14 EUR/month payment-hiding app:


export function run(input) {
  const total = parseFloat(input.cart.cost.totalAmount.amount);
  if (total < 200.0) return { operations: [] };
  const cod = input.paymentMethods.find(m => m.name.toLowerCase().includes("cash on delivery"));
  if (!cod) return { operations: [] };
  return { operations: [{ hide: { paymentMethodId: cod.id } }] };
}

Enter fullscreen mode Exit fullscreen mode

The fourth port (geo-restrict shipping for two countries on three SKUs) was the same shape as the COD hide, swapping paymentMethods for deliveryOptions and reading input.cart.deliveryGroups.deliveryAddress.countryCode. Each Function compiled to a wasm binary between 80 and 180 KB. The whole eight-app replacement landed in 540 lines of source plus four run.graphql query files.

Deploy, test, and the stuff vendor dashboards never showed me

The local loop is the part that actually made one Saturday possible. shopify app function run --input=fixtures/big-cart.json runs the Function against a JSON input on my machine in around 50 milliseconds, prints the mutations, and exits non-zero on schema mismatch. I wrote five fixtures per Function (empty cart, single item, threshold-minus-one, threshold-plus-one, edge case) and ran them on save with a file watcher. Eight apps, forty fixtures, all green before I touched the deploy.

shopify app deploy pushes every Function in the app as one versioned bundle. Rollback is one CLI flag back to the previous version. No "contact support to revert" emails.

In production, Shopify Functions emit structured logs to the Partner dashboard with input, output, fuel used, and execution time per run. I can grep by Function ID and timestamp. Four of the apps I cancelled had dashboards I had to log into separately to see whether their logic even fired, two of them charged extra for "advanced logging". Now I have one log surface for all eight surfaces.

Two limits that bit me, so they will not bite you: the compiled wasm is capped at 256KB and a single Function run is metered in "instructions" (a fuel budget, roughly 11 million instructions per call). The 540-line port came nowhere near either limit, but I caught a regex-heavy first draft of the BOGO Function blowing the fuel budget on a 50-line cart. Replacing the regex with a plain filter dropped the cost by 90 percent. Cold starts are sub-millisecond because the wasm is pre-loaded; I measured the same Function adding around 8 to 12 ms to checkout init versus the 200 to 400 ms the old apps cost. Shipping fast logic is part of why I keep recommending Shopify over hosted carts for solo merchants who want the speed without the platform tax.

For the deeper theme work that surrounds these Functions, I keep notes on Shopify section schema patterns editors actually love and how I pushed the storefront in Shopify theme performance: from 62 to 98 Lighthouse in one weekend.

Bottom Line

One Saturday, 540 lines of code, eight apps gone. The store now runs cart logic inside the platform, in compiled wasm, with 8 to 12 ms added per call and zero recurring app spend. The vendor dashboards are uninstalled. The webhook chain that used to pause checkout init is gone. The merchant has one repo I can read, version, and roll back.

If you are running a small Shopify store and your monthly app bill is creeping past 100 EUR for cart-side logic, sit down with the App Store list and mark which apps are doing pure cart math. Most of them are. Those are the ones a Function can replace in an afternoon. The ones that need outside data or scheduled work are not, and that is fine, keep those.

If you want the project shape I keep reusing for these Saturday ports, I share the full template and the fixtures pattern inside Studio.

Top comments (0)