Wiring Stripe into a browser extension sounds straightforward until you actually try it. The first approach most developers reach for — creating a checkout session from the popup — runs immediately into CORS restrictions. The second approach — using Stripe.js client-side — works but exposes your pricing logic and forces you to handle redirects awkwardly. The third attempt usually involves an external backend proxy, which adds infrastructure you should not need.
There is a correct architecture for this. It uses the extension’s background service worker as the Stripe session creator, and it is simpler than it sounds once you see it.
Why the Obvious Approaches Fail
Approach 1: Create the checkout session from the popup
Stripe’s checkout session creation (POST /v1/checkout/sessions) requires your secret key. If you make this request directly from the popup’s JavaScript, your secret key is embedded in the extension source code — which anyone can read by loading your extension as unpacked. This is a critical security vulnerability.
Even if you were willing to accept that risk, Stripe’s API does not set CORS headers that allow requests from extension origins. The request is blocked before it reaches Stripe.
Approach 2: Use Stripe.js in the popup
Stripe.js is designed for web pages. It can tokenize card details and redirect to Stripe-hosted checkout pages using a publishable key. But for subscription management and customer portal access, you still need server-side session creation. And the redirect from an extension popup to a Stripe-hosted page, then back to a success URL, does not work cleanly — extension popups close when the browser navigates away.
Approach 3: External proxy server
Some developers spin up a small backend (a Vercel edge function, a Lambda) just to create Stripe sessions. This works but adds an infrastructure dependency, a separate codebase to maintain, and costs money. For an extension that already has a Supabase or Firebase backend, it creates a third system.
The Right Architecture: Service Worker as Stripe Backend
The background service worker in a MV3 Chrome extension can make fetch requests to any URL without CORS restrictions. It runs in an isolated context, separate from any web page, and is treated as a trusted first-party origin by browsers.
This makes it the correct place to call Stripe’s API with your secret key.
The flow works like this:
- The user clicks “Upgrade” in the popup
- The popup sends a typed runtime message to the background service worker:
{ type: 'CREATE_CHECKOUT_SESSION', priceId: 'price_xxx' } - The service worker receives the message, creates a Stripe checkout session using
fetchwith your secret key in the Authorization header - The service worker receives the session URL from Stripe
- The service worker calls
chrome.tabs.create({ url: sessionUrl })to open the Stripe checkout page in a new tab - After the user completes checkout, Stripe fires a webhook to your backend (Supabase Edge Function or Firebase Cloud Function), which updates the user’s subscription status in the database
The secret key never touches client-side code. The CORS problem does not exist. The user gets a clean Stripe-hosted checkout experience in a full browser tab.
Handling the Customer Portal
The customer portal (for managing subscriptions, updating payment methods, canceling) uses the same pattern:
- Popup sends
{ type: 'CREATE_PORTAL_SESSION' } - Service worker creates a portal session via
POST /v1/billing_portal/sessions - Service worker opens the portal URL in a new tab
The only difference is that the portal session requires a Stripe customer ID, which you retrieve from your database before making the API call.
Secret Key Safety
In this architecture, your Stripe secret key lives in the extension’s environment variables, bundled into the service worker at build time. It is not visible in the popup or content script code. However, it is present in the unpacked extension files on the user’s machine.
This is an accepted trade-off for MV3 extensions — the alternative (a separate proxy server) is more complex for marginally better security. If you are concerned about this, the cleanest solution is to not bundle the key at all: instead, have the service worker call your Supabase Edge Function or Firebase Cloud Function, which holds the secret key server-side and makes the Stripe API call. LightningAddon supports both patterns.
What LightningAddon Ships
LightningAddon ships the service worker billing pattern fully implemented. The RuntimeMessageMap in @repo/core includes typed message definitions for CREATE_CHECKOUT_SESSION and CREATE_PORTAL_SESSION. The background service worker handler calls your configured backend (Supabase or Firebase), which creates the Stripe session and returns the URL. The flow from “user clicks upgrade” to “Stripe checkout opens in new tab” is already working when you clone the repo.
You configure your Stripe price IDs and keys in environment variables. Everything else is already done.
Jenny Wilson