DEV Community

seller-mind
seller-mind

Posted on Edited on

How I Stopped Rewriting Payment Webhooks for Every Chrome Extension Idea

When I first started building SaaS products, the initial spark of an idea was always followed by a heavy sigh. I would map out the feature set, sketch the UI, and then hit the inevitable wall: setting up the plumbing. I was struggling with the same repetitive tasks every single time. Wiring up payment webhooks, handling auth, and making sure entitlements actually propagated to the client. It felt like I was spending 80% of my time building the same foundation instead of the actual product. So I decided to build something.

The Boilerplate Tax

The boilerplate tax is real, and it quietly kills side projects. When you have to spend your first weekend just getting subscription billing and authentication to talk to each other, your momentum dies. The cost of ignoring this is not just a few lost hours; it is the psychological toll of starting from scratch. You end up copy-pasting outdated code from an old project, introducing subtle bugs, and eventually abandoning the idea because it feels like a chore rather than a creative endeavor. I needed a way to bypass the plumbing phase entirely, without sacrificing modern architecture.

A Modern Technical Approach

I wanted to build a starter kit that handled the boring stuff out of the box, but for a product shape most boilerplates ignore: a Chrome extension that charges a subscription.

Web app boilerplates assume a server that renders pages. An extension has no server of its own, no session cookie, and no place to hide a secret key. That changes almost every design decision:

Manifest V3 and the suspended service worker. The extension's background script is a service worker, and Chrome will shut it down when idle. Any state you keep in a module-level variable is gone on the next wake-up. Everything that matters has to be in chrome.storage, and every message handler has to assume it is starting cold.

Cloudflare Workers as the only backend. Since the extension bundle is fully inspectable by anyone who installs it, no API key can live there. Every AI call, every entitlement check, every write goes through a Worker that holds the secrets. This is also a nice performance win — the Worker runs close to the user, and the extension just does a fetch to your own domain.

Dodo Payments for billing, not a raw card processor. Dodo acts as merchant of record, which means the tax and compliance surface stays on their side of the fence. For a solo developer selling globally that is the difference between shipping and spending a month on VAT registration.

Here is the entitlement check pattern, which is the piece that took me longest to get right:

// Cloudflare Worker: the extension's only source of truth for "is this user paid?"
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname !== '/api/entitlement') {
      return new Response('Not found', { status: 404 });
    }

    // The extension sends the token it received at sign-in.
    const token = request.headers.get('authorization')?.replace('Bearer ', '');
    if (!token) return json({ error: 'unauthenticated' }, 401);

    const userId = await verifyToken(token, env);
    if (!userId) return json({ error: 'invalid token' }, 401);

    // Webhooks are the only thing that writes this key, so it cannot be spoofed
    // by the client. Cache it briefly to keep the extension snappy.
    const status = await env.KV.get(`entitlement:${userId}`);
    return json({ paid: status === 'active' });
  },
};
Enter fullscreen mode Exit fullscreen mode

The extension side then just asks that endpoint on startup and after any purchase redirect. No payment logic in the bundle at all.

Practical Results and Lessons

What worked exceptionally well was keeping the Worker as a single source of truth. Because the extension never decides for itself whether a user is paid, there is exactly one place to debug when entitlements go wrong.

What surprised me was how much of the difficulty was not code at all — it was the lifecycle. Cancellations, failed renewals, refunds, and chargebacks all have to revoke access at the right moment, and "the right moment" is different for each one. A cancellation should keep access until the period ends. A refund should revoke immediately. A failed renewal should probably grace-period for a day rather than cut someone off mid-task.

If I were to change anything, I would have started by modeling the entitlement state machine on paper before writing a line of webhook code. I wrote the handlers first and the state machine second, which meant rewriting most of them.

Try the Edge SEO Tool

If you are tired of rebuilding the same authentication and billing flows, or if you just want to see how the edge SEO analysis works in practice, I have made the core SEO analysis tool available for free. You can test it out and run a quick audit on your own landing pages right here: https://saaslaunch.dev/free-tools/seo-checker

Full disclosure: I'm the developer of SaaSLaunch, a Chrome extension SaaS starter kit (Manifest V3 + Cloudflare Workers + Dodo Payments). I built this because I needed it myself.

Top comments (0)