I run a small live-selling widget for Shopify and WooCommerce stores. Not a startup — one person, one Go binary, SRS for media. A few decisions turned out to matter more than the feature list, so I'm writing them down.
1. Iframe instead of a script tag
The first version was <script src=".../shell.js" data-key="...">. It injected a floating player into the merchant's page.
It broke constantly. Theme CSS leaked in, my CSS leaked out, and every third-party script on the page fought with mine. Worse: the API key sat in the DOM of a public storefront page.
What I ship now:
<iframe src="https://example.com/embed?shop_id=6" width="375" height="667" style="position:fixed;right:20px;bottom:20px;border:0"></iframe> Same iframe, one public shop id, zero secrets in the markup. The merchant pastes four lines. If they break their theme, they don't break the player.
Trade-off: iframe can't inherit the store's fonts. For a player that's fine — isolation is worth more than pixel-matching.
2. SRT for the ingest path, not just RTMP
RTMP is everywhere and everyone supports it. It's also TCP with fixed latency and handles packet loss badly — which is the normal condition for a merchant streaming from a phone on store wifi.
SRT gives you loss recovery and configurable latency for maybe 200ms more delay. For live selling, 200ms of extra delay costs nothing; a frozen frame costs the sale.
I still expose RTMP for compatibility, but the default config in my docs is SRT, and the bitrate defaults are deliberately conservative (1500–2000 kbps, 2s keyframe). Cross-border upload can't sustain 6 Mbps and the failure mode is a black player.
3. Enforce limits on the server, never in the widget
Viewer caps, stream keys, tier upgrades — all of it lives in the Go backend. The widget only renders.
Early on I had a cap check in the frontend so it would "respond faster". It took one person with devtools to watch a 200-viewer room on a 20-viewer plan. Anything that costs money has to be a server-side decision, even if the client shows a cached number.
4. The bug that taught me the most was caching
I deploy static assets by scp'ing them into place and restarting nginx. Everything under /static was served with Cache-Control: max-age=604800, immutable.
The day I added a feature that needed new i18n keys, every returning user got the new HTML with the old dictionary — and saw raw keys like lc_title on screen instead.
Now every asset reference carries a build version. immutable is only safe if the URL changes when the content does. Obvious in hindsight; not obvious at 2am.
What's still hard
Payments. If you're a solo developer outside the US/EU, merchant onboarding is the real boss fight — not the media pipeline. I've been through two providers' review processes and neither outcome was about code.
Feedback welcome
The whole thing is at https://durianlive.com — happy to hear what I got wrong, especially from anyone who has run live video at small scale.


