Switchboard DocsPolaris-native Shopify Functions

Cart Transform (Signed-Price Line Items)

Reprices individual cart line items via cryptographically signed tokens. Powered by Shopify's cart.transform.run Function target.

This is the most powerful and the most complex of the three features. It lets your storefront tell the function "this line item should cost $X" with a tamper-proof signature. The function verifies the signature, runs safety rails, and emits a lineUpdate operation.

Use it for: shipping protection upsells, VIP discounts, volume-tier pricing, custom upcharges, promotional one-time discounts — anything where you need server-authoritative pricing without burning a discount code.


The flow (5 steps)

┌──────────┐  1  ┌─────────────────┐  2  ┌──────────────┐
│Storefront│ ──▶ │/apps/switchboard│ ──▶ │ Switchboard  │
│   SDK    │     │   /sign-price   │     │   backend    │
└──────────┘     │  (App Proxy)    │     │  signs token │
     │           └─────────────────┘     └──────┬───────┘
     │                                          │
     │   3 token returned                       │
     │ ◀────────────────────────────────────────┘
     │
     │   4  Set on cart line property
     ▼
┌──────────────────┐
│  Cart line item  │
│  _switchboard_   │   At checkout, Shopify Function:
│  price = <token> │     - reads the property
└────────┬─────────┘     - verifies signature against shop's key
         │               - enforces safety rails
         │               - emits lineUpdate operation
         ▼
   ┌──────────────┐
   │   Checkout   │ ── Line displays at signed price ──▶ Customer pays it
   └──────────────┘
  1. Storefront SDK calls the App Proxy with { variant_id, price_cents, rule_id? }.
  2. Switchboard backend (/proxy/sign-price route) validates the request, then mints a signed token using the per-shop signing key (stored encrypted in Postgres, mirrored to a metafield the function reads).
  3. The backend returns { token, exp, propertyKey } to the storefront.
  4. Storefront sets the token on the line item's _switchboard_price property.
  5. At checkout, the Function reads the token, verifies the signature, enforces safety rails, and emits a lineUpdate operation.

The customer cannot fake a price. Even if they edit the property in DevTools, the signature breaks and the line falls back to the variant's original price.


SDK contract — signAndSetPrice

The SDK wraps the whole flow into one call:

// After a product is in the cart, mint + apply the signed price
fetch('/cart.js').then(r => r.json()).then(function (cart) {
  var line = cart.items[0];  // pick the line you want to reprice
  return Switchboard.signAndSetPrice(line.key, 500);  // $5.00 in cents
});

If your shop has more than one active Cart Transform rule, pass ruleId:

Switchboard.signAndSetPrice(line.key, 500, {
  ruleId: 'rule_abc123'  // the rule ID shown in the admin
});

Other useful SDK calls:

// Set the property without re-minting (advanced — token must already exist)
Switchboard.setLineItemProperty(line.key, '_switchboard_price', token);

// Add a brand-new line item with the token already attached, in one round-trip
Switchboard.addLineItemWithProperties(variantId, 1, {
  _switchboard_price: token
});

See /docs/sdk for full SDK reference.


Configuration in the admin

Field What it controls Default
Title Internal label (required)
Status Active / Inactive Inactive
Token TTL How long a signed token stays valid 30 min (1800s)
Minimum price Lowest price the function will honor $0.50 (50 cents)
Maximum price Highest price the function will honor $999.99 (99999 cents)
Allow $0.00 Whether free items are allowed via this transform Off (rejected)
Min % of variant Lower bound as fraction of variant price Off (no floor)
Fail-safe behavior What happens on signature/rail failure use_original

Rule ID is shown in the admin once the rule is saved. Pass it as the third arg to signAndSetPrice if your shop has >1 active rule.


Safety rails — read this before you ship

Cart Transform applies arbitrary prices. The safety rails are your only protection against bugs / abuse. Defaults are conservative for a reason — don't widen them unless you have a specific reason and an understanding of the failure modes.

minPriceCents / maxPriceCents

Hard floor / ceiling in cents. Any token proposing a price outside this range is rejected.

When to tighten: if you only ever reprice to a specific value (e.g. shipping protection at $2.99), set both to 299 so any deviation falls back.

When to widen: rarely. Maybe for high-priced items or aggressive volume discounts.

allowZero

Boolean. When off (default), even a valid token for $0.00 is rejected.

When to flip on: offering legitimate freebies through this transform. Otherwise: leave off — protects against signature collisions or buggy code accidentally minting $0 tokens.

minPercentOfVariant

Optional. Sets a floor as a fraction of the variant's original price. E.g. 0.5 means "the signed price can never be less than 50% of what Shopify thinks the variant costs."

When to set: VIP / loyalty discounts where you want a hard cap on how deep the discount can go regardless of what your code says.

Example: Variant priced at $25. minPercentOfVariant: 0.5. Token for $10 → rejected (below the $12.50 floor). Token for $13 → accepted.

failSafeBehavior

What the function does when verification fails or a safety rail is tripped:

  • use_original (default, only one currently implemented) — line falls back to the variant's price. Customer doesn't see an error. This is the right behavior in 99% of cases.
  • hide_line (reserved) — would remove the line entirely. Not implemented in R2.
  • block_checkout (reserved) — would emit a checkout error. Not implemented in R2.

Worked examples

Shipping protection

Problem: "I want a $2.99 shipping protection toggle on the cart page — like Route, but mine."

Admin:

  • Title: Shipping Protection
  • Status: Active
  • Token TTL: 30 min
  • Min price: $0.50 (default)
  • Max price: $2.99 (lock to this exact price by setting max)
  • minPercentOfVariant: off

Storefront — Liquid block on cart page:

<label>
  <input type="checkbox" id="ship-protect"> Add shipping protection ($2.99)
</label>

<script>
  document.getElementById('ship-protect').addEventListener('change', function (e) {
    if (!window.Switchboard) return;
    if (!e.target.checked) return;

    // 1. Add the protection product (variant id of your protection SKU)
    Switchboard.addLineItemWithProperties({{ section.settings.protection_variant_id }}, 1, {})
      .then(function (cart) {
        // 2. Find the newly added line and sign its price
        var line = cart.items[cart.items.length - 1];
        return Switchboard.signAndSetPrice(line.key, 299);
      });
  });
</script>

The customer can't manipulate the price client-side — even if they swap the token to one for $0, the signature breaks and it reverts to the protection product's variant price.

VIP discount

Problem: "VIP customers get 25% off all line items."

Admin:

  • Title: VIP — 25% off
  • Status: Active
  • minPercentOfVariant: 0.7 (so the discount can never go below 70% of variant — protects against bugs)

Storefront — Liquid in theme.liquid:

{%- if customer.tags contains 'vip' -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (!window.Switchboard) return;
      fetch('/cart.js').then(function (r) { return r.json(); }).then(function (cart) {
        cart.items.forEach(function (line) {
          var discounted = Math.round(line.price * 0.75); // 25% off
          Switchboard.signAndSetPrice(line.key, discounted);
        });
      });
    });
  </script>
{%- endif -%}

Volume tiers

Problem: "Buy 10+ widgets, get them at $4 each. Buy 50+, $3 each."

Admin:

  • Title: Widget volume pricing
  • minPriceCents: 300 (locked floor of $3)
  • minPercentOfVariant: 0.4 (safety: never go below 40% of original)

Storefront — JS watches the cart:

function pricePerUnit(qty) {
  if (qty >= 50) return 300;  // $3
  if (qty >= 10) return 400;  // $4
  return null;  // no override at lower quantities
}

async function rePriceWidgets() {
  const cart = await fetch('/cart.js').then(r => r.json());
  for (const line of cart.items) {
    if (line.product_id !== WIDGET_PRODUCT_ID) continue;
    const price = pricePerUnit(line.quantity);
    if (price === null) {
      // Clear the override
      await Switchboard.setLineItemProperty(line.key, '_switchboard_price', '');
    } else {
      await Switchboard.signAndSetPrice(line.key, price);
    }
  }
}

// Run on every cart change
window.addEventListener('storage', rePriceWidgets);  // simplified — use your cart-update hook

Custom upcharge (engraving)

Problem: "Personalized engraving costs $10 per item. Customer enters text on the product page."

Pattern: when the customer enters engraving text, add a hidden "Engraving" product line and sign its price.

async function addEngraving(text, variantId) {
  // 1. Add the hidden Engraving product with the text as a property
  const cart = await Switchboard.addLineItemWithProperties(variantId, 1, {
    'Engraving text': text,
    _engraved_variant: 'gid://...' // optional: track which item this is for
  });
  // 2. Find the line and sign at $10
  const line = cart.items[cart.items.length - 1];
  await Switchboard.signAndSetPrice(line.key, 1000);
}

Promotional pricing

Problem: "First 100 customers in a launch get 50% off. Without a discount code, without exposing the discount on the URL."

Pattern: your backend tracks order counts. The storefront calls a route on your backend BEFORE the SDK to ask "should this customer get the discount?" If yes, your backend gives the storefront a price; the storefront calls signAndSetPrice with that price.

The Cart Transform rule's safety rails prevent abuse (no price below your floor, percent floor caps the discount depth). The signing key never leaves the server.


Token security model (the short version)

  • Per-shop signing key. Each shop has its own key, generated lazily on first Cart Transform sync. Stored AES-256-GCM-encrypted in Postgres; mirrored as a metafield on the Cart Transform record (so the Function can read it at checkout).
  • Token format. Base64-encoded JSON: { price, variant_id, rule_id, exp, sig }. The signature binds all four payload fields.
  • Custom keyed hash, not HMAC-SHA256. Shopify Functions have a tight instruction budget; standard cryptography libraries are too expensive. Switchboard uses a custom 64-bit keyed hash (FNV-1a-style mixing with Math.imul). Not crypto-grade, but the threat model relies on the signing key being secret — it lives in a metafield never exposed to the storefront.
  • Constant-time signature compare prevents timing attacks.
  • Variant binding. Token includes variant_id. If the customer copies a token from one line to another with a different variant, the signature doesn't verify — the cart's variant id won't match.
  • Rule binding. Token includes rule_id. Tokens are not transferable between Cart Transform rules.
  • Expiry. Token includes exp (unix seconds). Tokens older than the configured TTL are rejected.

See SECURITY.md in the repo for the full threat model + audit.


Constraints

  • Token TTL minimum is 60 seconds. Tighter than that risks valid tokens expiring during slow page loads.
  • Token TTL maximum is 24 hours (86400s). Longer than that and stale tokens become a real abuse vector if the signing key is ever compromised.
  • One Cart Transform rule per shop is the simplest case. Multiple rules require passing ruleId explicitly to disambiguate which one a token should bind to.
  • Public distribution required. Cart Transform's cartTransformCreate mutation rejects apps on private distribution. Flip to Public in Partners before testing.

See SHOPIFY_APP_PLAYBOOK.md §4 in the repo for the distribution-flip checklist.


Fail-safe matrix

Failure Result
No token on line Line at variant price (function ignores)
Malformed token Line at variant price
Bad signature Line at variant price
Expired token Line at variant price
Token for wrong variant Line at variant price
Token violates minPriceCents Line at variant price
Token violates maxPriceCents Line at variant price
Token below minPercentOfVariant floor Line at variant price
Token at $0 with allowZero off Line at variant price
Active rule but no signing key (shouldn't happen) Line at variant price

In every case the customer never sees an error. The line just shows the original price. Your storefront code should detect this case (compare expected price vs cart.items[i].final_price after the SDK call) if you want to surface a "discount didn't apply, please refresh" message.


Testing in the admin

Each saved Cart Transform rule has a Test signing button that opens a sandbox at /app/cart-transforms/<id>/test. Enter a variant GID + price cents, click Sign, and the admin verifies the token round-trip against the same logic the function uses. Useful for confirming your config + signing-key state without touching a storefront.

See SHOPIFY_QA.md for the full reviewer test plan.