DEV Community

Cover image for One support widget, seven platforms: what shipping to each marketplace actually takes
Kaven C
Kaven C

Posted on • Originally published at deskcrew.io

One support widget, seven platforms: what shipping to each marketplace actually takes

I build DeskCrew, a support helpdesk, solo. The product is a chat widget plus an AI that answers from your own help docs. Building it was the easy part. Getting it in front of people means shipping the same widget to every platform a customer might already be on: WordPress, Shopify, npm frameworks, forums, docs sites.

This week I shipped it to seven of them. Here is what each one actually took, because the marketing pages never tell you.

The whole product is one line:

<script src="https://deskcrew.io/desk.js" data-key="pub_YOUR_KEY" defer></script>
Enter fullscreen mode Exit fullscreen mode

Everything below is just a different wrapper around getting that tag onto a page. But every platform has its own delivery mechanism, its own review process, and its own trap.

npm frameworks (Astro, Docusaurus, Nuxt): the config-entry pattern

Modern site frameworks do not want a script tag. They want a config entry. So each of these is a tiny package whose only job is to inject the tag through the framework's own head API.

// astro.config.mjs
import deskcrew from '@deskcrew/astro'
export default defineConfig({
  integrations: [deskcrew({ widgetKey: 'pub_...' })],
})
Enter fullscreen mode Exit fullscreen mode

Publishing is npm publish --access public. The trap was not the code. It was auth: enabling 2FA on my npm account silently invalidated the CLI session, and npm publish responded with a 404 on the scoped package, not a 401. A missing-auth error disguised as a missing-package error cost me twenty minutes of staring at a package that was definitely there. Lesson: on npm, E404 on your own scope usually means "you are not logged in," not "it does not exist."

Shopify: the zero-scopes flex

Shopify was the one I expected to be brutal, and it was the smoothest, because of one architectural decision: the app requests zero API scopes.

Most Shopify apps ask for read_products, read_customers, and so on, which drops you into Shopify's protected-customer-data review: a longer, stricter process. But my widget does not need any Shopify data. It ships as a theme app extension (an app-embed block), not the deprecated ScriptTag API, so the merchant flips one toggle in the theme editor and the widget loads. No data access, no protected-data review, fast track.

The block itself is defensive by default:

{%- if block.settings.widget_key != blank -%}
  <script src="https://deskcrew.io/desk.js"
    data-key="{{ block.settings.widget_key | escape }}" defer></script>
{%- endif -%}
Enter fullscreen mode Exit fullscreen mode

Nothing renders without a key, and every merchant-supplied value is escaped. The one non-obvious requirement: the mandatory GDPR webhooks (customers/data_request, customers/redact, shop/redact) plus app/uninstalled, all HMAC-verified with a timing-safe compare, returning 401 on a bad signature. Reviewers test these first.

Discourse: no store at all

Discourse has no marketplace and no review queue. A "theme component" is just a public git repo with an about.json and a common/head_tag.html. Push it to GitHub, and any forum admin installs it with Admin → Customize → Themes → Install → From a git repository. That is the entire distribution story. The moment I pushed, it was installable worldwide.

The head-tag script validates every setting against a strict character class before it touches the DOM, and builds the tag with createElement + setAttribute, never innerHTML, so a forum setting can never inject markup.

Framer: a real plugin with a UI

Framer wanted an actual plugin: a React app that runs inside the editor and writes to the site's custom code via framer.setCustomCode. This one has a review queue and wants gallery images. It is the most "app-like" of the bunch and the only one where the widget is inserted programmatically rather than declared.

The bug that ate a whole afternoon

The best technical lesson this week had nothing to do with any marketplace. It was email.

DeskCrew turns inbound support email into tickets. I wanted hello@deskcrew.io to flow into the product, so I set up Cloudflare Email Routing to forward the domain to my mail pipeline. Every test email vanished. No error, no ticket, nothing.

The logs finally confessed: dropped automated/loop mail (system-sender:bounces). My inbound handler has a loop-guard that drops mail from bounces@, no-reply@, mailer-daemon@ and friends, so an auto-responder can never ping-pong with a bounce address forever. Sensible. But Cloudflare rewrites the envelope sender to bounces@cf-bounce.notify.cloudflare.com on every forwarded message. So the loop-guard was correctly dropping every single forwarded email as if it were a bounce.

Forwarding services and loop-guards are fundamentally incompatible. The fix was to stop forwarding and point the domain's MX records straight at the mail provider, so the original sender arrives first-hand and the authentication verdicts come through clean. Direct MX, no hop.

Rule of thumb: if you forward mail into anything that inspects the sender, the forwarder's envelope rewrite will fight your logic. Prefer direct MX.

What actually mattered

The widget is one script tag. Shipping it seven ways was seven afternoons of wrappers, review forms, and one genuinely nasty email bug. Building the product was never the bottleneck. Distribution is the work, and it is the part nobody shows you.

If you run a Framer, Shopify, WordPress, Discourse or docs site and want to see the widget, it is free with no card at deskcrew.io. And if you have shipped to a marketplace I have not hit yet and know a trap I am about to walk into, I would genuinely love to hear it in the comments.

Top comments (0)