Repository navigation
Plugin system: what's missing to build a Checkout Links-style plugin (discounts, server carts, cart attributes, storefront slots) #223
Description
Why
We looked at whether Checkout Links (Shopify app, $25/month, 2025 Build Award) could be rebuilt as a Commera plugin.
Checkout Links turns an offer into a URL such as yourstore.com/summer-sale. The link loads a cart with chosen items, quantities and discounts, can prefill the shopper's details, and lands the shopper in one of three places:
- the checkout,
- a branded popup with upsells, or
- the store, with the discount active while they browse.
Every order carries the link code, UTMs and tags. Links can also be scheduled, limited (total or once per customer) and passcode-protected, and they come with QR codes and per-link analytics.
About half of it can be built today. The core flow can't, because the plugin system has five gaps. The gaps aren't specific to this app: coupon, loyalty, bundle, upsell and affiliate plugins hit the same ones.
What a plugin can already do
| Feature | How |
|---|---|
| Link records, schedule, passcode, draft/active | The plugin's own DocType |
| Short URL on the store domain | website_route_rules / www/ in the plugin app |
| QR codes, clicks, revenue per link | Plugin page, plus order_placed / order_paid events |
| Create a link from a product, customer or order | Product, customer and order actions |
| Show which link an order came from | Order card |
| Total-uses limit | Count uses in order_placed |
| Once per customer | validate_cart |
| Order tags | Tag the Sales Order in order_placed |
| Free shipping | delivery_options repriced to 0 (shipping-provider options only) |
What's missing
1. Server-side cart creation
The storefront cart lives in the browser, in Alpine.store('cart') saved to localStorage. It only becomes a Quotation at checkout, in generate_quotation_for_cart / save_guest_cart (commera/api/payments.py). Server code therefore can't fill a visitor's cart, and the SDK only has cart.get_cart and cart.set_cart_fields.
Shopify exposes:
- Cart permalinks:
/cart/{variant}:{qty},…withdiscount,attributes[…],note,ref, andstorefront=trueto land on the cart instead of checkout. - Storefront API
cartCreate: lines, attributes, note, discount codes and buyer identity. It returns acheckoutUrl.
Needed:
- An SDK call such as
cart.create_cart(items, attributes=None, buyer=None)that returns a cart token and a checkout URL. - A storefront contract (e.g.
/cart?load=<token>) that fillsAlpine.store('cart')from that cart.
2. A discount hook
The hooks reference says: "No hook changes an item price, gives a discount or sets the cart total directly."
ERPNext coupons don't fill the gap. get_quotation_for_cart calls _remove_coupon_code on every cart rebuild, so a coupon applied by a plugin is wiped.
Shopify exposes: the Discount Function API. It has product, order and shipping discount targets, reads lines, attributes, metafields and buyer identity, and is combined by each discount's combination settings. Free gifts are an added line plus a 100% product discount. Cart Transform handles price overrides and bundles.
Needed: a cart_discounts hook, run on every pricing pass like cart_fees.
- Input: the cart.
- Output: line discounts (percentage or amount), order discounts and shipping discounts. The shopper sees each discount's label.
- Stacking: Commera defines it once in the hook. Shopify functions can't see each other, which is why Checkout Links writes its own "biggest discount wins" logic.
- Persistence: the discounts carry into the Sales Order. Tax is computed on the discounted amounts.
3. Checkout prefill and a checkout URL
There is no SDK call to set a cart's email, shipping address or delivery option, or to get a URL that opens checkout for that cart. Guest checkout exists (save_guest_cart), but plugins can't call it.
Shopify exposes: the permalink parameters checkout[email] and checkout[shipping_address][first_name|last_name|address1|address2|city|province|country|zip], and cartCreate.buyerIdentity.
Needed: SDK calls to set a cart's buyer (email, phone, shipping address) and delivery option, and cart.get_checkout_url(cart).
4. Cart attributes for guests
A plugin's own Quotation fields already copy to the Sales Order. But set_cart_fields needs an existing cart, and a guest has none until they type an email at checkout. So a link's code and UTMs can't be attached when the shopper lands.
Commera already handles this for its own UTMs: set_attribution_fields (commera/analytics/events.py) reads a cookie at checkout. Plugins have no equivalent, and only source, medium and campaign are captured (no term or content).
Shopify exposes: cart attributes and a cart note, settable from permalinks, the AJAX cart and the Storefront API, and copied onto the order. Permalink ref and source_name set the order source.
Needed:
- A way for a plugin to set key/value cart attributes for a session, cart or no cart, which Commera stamps onto the Quotation and the Sales Order.
- Ideally, also an order-source value a plugin can set.
5. Storefront extension points
The storefront templates/layout.html is a standalone template, so a plugin's web_include_js never loads there. Plugins can't put anything on the storefront: no popup, no banner, no discount badge on a cart line, no "spend ₹X more for a free gift" message.
Shopify exposes:
- Theme app extensions: app embed blocks (a whole-page script or overlay, turned on once in the theme editor) and app blocks (sections the merchant places). Each carries Liquid, JS and CSS assets.
- App proxies: app pages rendered inside the store's theme.
- The AJAX cart API: a public JavaScript cart.
Needed:
- A plugin script/style include in the storefront layout (the "app embed"), with an on/off switch in Settings → Plugins.
- Named slots plugins can render into, such as
cart_line_badge,cart_banner,checkout_bannerandpage_overlay(the "app blocks"). - A documented storefront JavaScript cart API (add, update, set attributes, read discounts and events), so plugins don't depend on the internal
Alpine.store('cart')shape. - A way for a plugin page to render inside the store layout (header, footer, theme), not as a bare page.
Not needed
- Sales channels / publications: Commera is a single storefront.
- Subscriptions and B2B catalogs: Commera doesn't have them yet.
Suggested order
- Gap 2, the
cart_discountshook. It unblocks the most plugins and is the riskiest because it touches money, so it needs the most review. Thecart_feesrow handling is the template. - Gaps 1 and 3, server-side cart creation and checkout prefill.
- Gap 4, cart attributes for guests.
- Gap 5, storefront placements.
With 1–3 done, a Checkout Links-style MVP is doable: links with a fixed or dynamic cart, a discount or free shipping, limits, schedule, passcode, QR codes, attribution and analytics. Item 4 adds the popup and storefront mode.
Rough estimate (not designed yet):
| Work | Estimate |
|---|---|
cart_discounts hook |
1–1.5 weeks |
| Cart creation SDK | 3–4 days |
| Cart attributes for guests | 2–3 days |
| Checkout prefill and storefront placements | about 2 weeks |
| The plugin itself | 1.5–2 weeks |
🤖 Generated with Claude Code
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
NagariaHussain commentedon Oct 4, 2026
Checkout Links: admin interface draft
This is a follow-up to the gap list above: what the Checkout Links admin looks like, what it allows, and where each screen would live in a Commera plugin. It's reconstructed from the public sources below. Only the first two images show the real admin UI. The rest of the layout comes from the help-center text, and the places where I inferred the layout are marked (inferred).
Sources:
1. Where it lives
It's an embedded Shopify admin app with Polaris UI. It is registered as a sales channel, so it sits under Sales channels → Checkout Links with four sub-pages:
Save and Discard sit in Shopify's contextual save bar ("Unsaved changes") at the top. LinkShop, a separate link-in-bio app from the same company, appears beside it.
2. Links page (home)
3. Link editor
This is a two-column Polaris page with a breadcrumb
Links › Create linkand a ⋯ menu.The screenshot confirms the right-column order Link preview → QR Code → Dynamic → Attribution → Schedule, with on/off toggles, and that Name, Checkout URL, Cart and Discounts are on the left. The rest of the order is (inferred).
What each card allows
Link
FCcJ83.yourstore.com/<slug>with a custom slug, copy-to-clipboard and edit. The slug redirects to/a/link/<shortcode>.Cart
Promotions
EUR 45.Popup
Storefront banner (Storefront destination only)
Link preview
QR code (collapsible)
/a/link/<shortcode>, so changing the slug doesn't break prints.Dynamic & B2B
?li=(lines),?c=(customer),?attr=(cart attributes) and?note=.Attribution
cl_utm_*,cl_ref,cl_custom_*andcl_shortcode.Tag orders
Schedule
Usage limit
Passcode / Require login
Scripts
</body>on the popup, e.g. GA tags or translations through a MutationObserver.4. Popup block editor
This is a full-screen editor with the block list on the left and a live preview on the right.
Blocks
Editing
Templates
Document settings: colour scheme. Prices show in the shopper's currency through Shopify Markets.
5. Analytics page
6. Settings page (inferred; no article covers the page itself)
7. Entry points outside the app
8. What the shopper sees
9. Where each screen would live in a Commera plugin
commera/pages/links/index.vue→/commera/plugins/<app>/linkspages/link/index.vue?name=…. Placements have no per-record routes such aspages/links/<id>, so the link to edit goes in a query parameter or in-page state. This is another small gaporder_placed/order_paidand the plugin's own click logcommera/settings/index.vue(a Single DocType form)commera/commandsOne more gap: per-record plugin pages
Mapping the editor above turned up a sixth gap, a small one. A plugin page is only
pages/<name>/index.vue→/commera/plugins/<app>/<name>; there are no per-record routes likepages/links/<id>. A plugin with a list page and an edit page per record, such as links, coupons or bundles, has to pass the record in a query parameter or keep it in page state. A detail route would fix this, e.g.pages/<name>/[id].vue→/commera/plugins/<app>/<name>/<id>, with the parameter available to the page.🤖 Generated with Claude Code
NagariaHussain commentedon Oct 7, 2026
What's done, what's left?
NagariaHussain commentedon Oct 10, 2026
Status: Buy Links plugin
The plugin now exists: bwhtech/commera_buy_links. It is built on what Commera has today, and its GAPS.md lists what it cannot do.
?discount=checkout[...]parameters. Phone is missing: #245/buy/<code>and tags the Quotation in itsvalidatedoc eventcommera_storefront_include_js/_cssandcommera_storefront_blocks. Buy Links' popup uses the include. The on/off switch moves to Settings > Plugins in #244New, smaller gaps found while building it:
before_testson a normal site/to/enredirect drops the query string🤖 Generated with Claude Code
Rl0007 commentedon Oct 20, 2026
@NagariaHussain shall I close this?