Key Takeaways
- A Shopify discount app is four hard problems wearing a trench coat: enforcing discounts with Shopify Functions, rendering on the storefront without wrecking performance, authenticating an embedded admin, and moving config between all three without stale data.
- Enforcement belongs to Shopify Functions and native discount objects, not your backend; your job is to configure them cleanly, one per rule.
- The storefront is a performance minefield: inject JavaScript carelessly and you pay in layout shift, load time, and stale prices.
- This page is a hub. Each section links to a focused deep-dive on one problem and exactly how we solved it in production.
A Shopify discount app looks simple from the merchant's side: set up a volume discount, a per-customer price, or a buy-one-get-one, and it just works at checkout and on the product page. Underneath, that simplicity is held up by four subsystems that each have their own failure modes, and by the plumbing that carries configuration between them.
These are our engineering notes from building one. This page frames the whole problem space and links to detailed, code-level write-ups of the specific problems that were worth solving carefully. If you are building anything in the Shopify app or theme ecosystem, the deep-dives stand on their own; this hub is the map.
The Four Subsystems
Every feature in the app touches some combination of four moving parts:
- Enforcement at checkout, via Shopify Functions and native discount objects.
- Presentation on the storefront, via a theme app extension.
- Administration in the embedded Shopify admin, via a React app and a Laravel backend.
- Configuration transport, via app-owned metafields that carry rules from the backend to the storefront and to the Functions.
The interesting engineering lives at the seams between these, where a decision made in one has to be honored, performantly and without going stale, in another.
Enforcement: Let Shopify Functions Do the Math
The first instinct when building a discount app is to compute discounts yourself. The right move is to configure Shopify to compute them. Shopify Functions run at checkout, inside Shopify's infrastructure, and are the supported way to apply custom discount logic. Your backend's job is to create and maintain the discount objects and feed each one a clean config.
The architecture decision that paid off most here was modeling one automatic discount per rule rather than merging every rule into a single app discount. Per-rule discounts give each rule its own title, schedule, and combination settings in Shopify admin, and they hand scheduling and precedence back to the platform instead of forcing your backend to reimplement them.
Deep-dive: One Automatic Discount per Rule: A Shopify Functions Architecture covers the create/update/delete sync, stale-GID recovery,
combinesWith, native segment gating, and why the Function code never changes when you split the architecture.
Presentation: The Storefront Is a Performance Minefield
The storefront widget is where most of the subtle engineering is, because it runs on someone else's theme, after their content, and has to look instant without being instant.
Three problems dominate:
Layout shift. A widget that injects itself with JavaScript after paint pushes the page around and tanks your Cumulative Layout Shift. The fix is to predict the widget's presence and size on the server and reserve the space before paint.
Deep-dive: Stop Layout Shift in JS-Injected Shopify Widgets covers the SSR gate in Liquid, the responsive skeleton, and deferring the widget's JavaScript past LCP without re-introducing shift.
Stale data versus repeated work. Recompute pricing on every page view and the widget lags and hammers your proxy; cache it naively and you show prices that are no longer true. The resolution is to cache the matched rule but always recompute the displayed number.
Deep-dive: A Storefront Cache That Won't Serve Stale Prices covers the sessionStorage model, currency-only invalidation, content-hash invalidation of matches, and the one trade-off we accepted.
Administration: Authenticate Without the Redirect Dance
The admin UI is an embedded React app inside a Shopify Admin iframe, talking to a Laravel backend. The authentication model is the modern one: managed installation plus token exchange, which removes OAuth redirects entirely. App Bridge mints a short-lived session token, the backend verifies it on every request, and the first time it needs an access token it exchanges the session token for one.
Getting this right is as much about security as about ergonomics: verifying the token's claims in the correct order, and sourcing the shop domain only from the signed token so one merchant can never act on another's store.
Deep-dive: Embedded Shopify Auth Without OAuth Redirects (Token Exchange) covers session-token validation, inline provisioning and refresh, the single-retry client, and the cross-tenant IDOR you must avoid.
Configuration Transport: Move Rules Without Bloating or Staling
Config has to travel from the backend to the storefront and to the Functions. We carry it in app-owned metafields: the storefront reads one JSON value per feature, and each discount carries its own single-rule config.
The lesson that bit us here was that the format of the IDs inside those payloads is not cosmetic. Storing full GIDs in a value that holds every rule bloats it toward Shopify's size caps and slows the storefront, which fetches it on every page. Storing numeric IDs instead cut the payload roughly threefold, and comparing by numeric tail kept the change backward compatible during rollout.
Deep-dive: Shrink Shopify Metafields: Store Numeric IDs, Not GIDs covers the producer/consumer split, the backward-compatible match, the one-time resync, and which IDs to deliberately leave as GIDs.
How the Seams Connect
The reason these are one app and not four is that the seams are where correctness lives:
- A pricing rule configured in the admin becomes a per-rule discount (enforcement), a line in a storefront metafield (presentation), and a per-discount config metafield (the Function's input). All three must agree.
- The metafield format decision (numeric IDs) is driven by the storefront's read-on-every-page performance need, but must not break the Function's input, so the Function config keeps its GIDs.
- The storefront cache must invalidate when the admin edits a rule, which it does for free because the injected rules are hashed into the cache key.
- The auth model shapes every admin request, and its cross-tenant discipline protects all the config the other subsystems rely on.
A change that looks local almost never is. That is why each deep-dive ends by pointing back here.
The Deep-Dives
| Problem | Subsystem | Read |
|---|---|---|
| Layout shift from JS-injected widgets | Presentation | Stop Layout Shift in JS-Injected Shopify Widgets |
| Metafield payloads bloating toward the size cap | Config transport | Shrink Shopify Metafields: Store Numeric IDs, Not GIDs |
| Scheduling and precedence you shouldn't reimplement | Enforcement | One Automatic Discount per Rule |
| OAuth redirects in an embedded app | Administration | Embedded Shopify Auth Without OAuth Redirects |
| Fast storefront pricing without stale numbers | Presentation | A Storefront Cache That Won't Serve Stale Prices |
A position: fixed overlay rendering behind a sticky column |
Administration | The Sticky-Column Trap |
| Widget styles colliding with the merchant's theme | Presentation | Make a Storefront Widget Survive Any Merchant's Theme CSS |
| A validation function that silently enforced nothing | Enforcement | The Shopify Validation Function Config Path That Hid a Bug |
| Slow first paint in a no-SSR embedded SPA | Administration | Optimizing LCP in an Embedded Shopify Admin SPA |
| Silent-failure traps in Polaris Web Components | Administration | Polaris Web Components Gotchas Nobody Documents |
Frequently Asked Questions
Should a Shopify discount app compute discounts in its own backend?
No. Checkout enforcement should run through Shopify Functions and native discount objects. Your backend creates and maintains those objects and supplies each one a config; Shopify applies the math at checkout. Reimplementing discount resolution in your backend risks disagreeing with what Shopify actually applies, especially once you use combinesWith.
What is the hardest part of a Shopify storefront widget?
Performance, specifically avoiding layout shift and stale data while running on someone else's theme after their content loads. Both are solvable by moving decisions to the server (reserve space before paint) and caching the stable decision rather than the volatile number.
Does managed installation mean I write no auth code?
You write less, and none of it is an OAuth redirect. You still verify the session token on every request, provision and refresh an access token via token exchange, and enforce cross-tenant isolation. The auth deep-dive walks through exactly what remains.
Written by Duke, based on the production implementation of the Flicky Shopify discount app. Each linked deep-dive draws its code from the shipping codebase, simplified for readability.