Margin

Bite-sized news cards in your Chrome side panel — one story

Visit Website
June 22, 2026 Shipped a no-backend Chrome extension — the platform constraint that reshaped onboarding, and the licensing code that let me skip a server

Just launched Margin on Product Hunt — a Chrome extension that replaces endless feed-scrolling with a swipeable stack of news cards in the browser's side panel.

Posting here because two technical decisions while building this solo ended up being the actual product decisions, and I think the code tells that story better than a feature list would.

The constraint: sidePanel.open() needs a real click

My first plan was simple — auto-open the panel the moment someone installs the extension, so they see the product immediately. Chrome won't allow it:

chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// sidePanel.open() requires a user gesture — can't call it here.
// So instead: open a real tab with a button. The click on THAT
// button is the gesture Chrome wants.
chrome.tabs
.create({ url: chrome.runtime.getURL('src/welcome/index.html') })
.catch(() => {})
}})

That one platform limitation rewrote my entire first-run flow. Instead of "user installs → product just appears," it became "user installs → tab opens explaining where to find the icon and pin it → they click a button that finally opens the panel." More steps, but it's also the only way it actually works — and arguably it's a better flow anyway, since people now get told where the icon lives instead of guessing.

Skipping a backend for the paid tier

For the $1.99/mo tier, I didn't want to run a server just to check "is this user paid." Used Lemon Squeezy as merchant of record — checkout happens on their hosted page, user gets a license key by email, and I validate it against their public License API directly from the extension:

const res = await fetch('https://api.lemonsqueezy.com/v1/licenses/activate',
{
method: 'POST',
headers: { Accept: 'application/json' },
body: new URLSearchParams({
license_key: key,
instance_name: 'margin-extension',
}),
})

No secret key client-side (their License API is designed to be called publicly), no payment data ever touches my code, and "is this user Plus" is just: is there a valid key in local storage. For a solo project, the appeal wasn't avoiding the work of building a backend — it was avoiding the work of running one forever afterward.


Wrote up more of the build (scroll-gesture handling, the on-device AI summary fallback logic, a luminance-based image-cropping script I ended up needing for store screenshots) here:

https://dev.to/mohanvenkatakrishnan/building-margin-a-privacy-first-news-reader-inside-chromes-side-panel-32c

Curious if anyone else here has hit Chrome's gesture-requirement wall on sidePanel or notifications APIs — feels like the kind of platform quirk that doesn't show up until you're mid-build.

https://www.producthunt.com/products/margin-4

Comment

About

I built Margin because I noticed my own morning routine was broken: Open five news sites, skim headlines, close most tabs without actually reading anything.