All Guides
Guide

How to Add Subscriptions to a Chrome Extension

Chrome extensions have unique billing challenges. Learn how to implement subscriptions, handle authentication in a browser context, and gate features by plan.

Published January 10, 2026Updated January 25, 2026

Why Chrome Extension Billing is Different

Chrome extensions operate in a unique environment:

  • **No server by default**: Extensions run client-side in the browser
  • **Limited storage**: Chrome's `storage.sync` and `storage.local` APIs
  • **Background service workers**: MV3 extensions use ephemeral workers
  • **Chrome Web Store**: Google's marketplace has its own rules
  • **Cross-origin restrictions**: Content security policies limit what you can do
  • These constraints make traditional SaaS billing patterns harder. You can't just drop in a checkout page.

    Monetization Options for Chrome Extensions

    Chrome Web Store Payments (Deprecated)

    Google deprecated the Chrome Web Store payment API in 2020. You can no longer use Google's built-in monetization.

    One-Time Purchase via Gumroad/LemonSqueezy

    Sell a license key, validate it in the extension.

    Pros: Simple, no ongoing infrastructure

    Cons: No recurring revenue, license key sharing

    Subscription via Your Own Backend

    Use a subscription platform (like StackBE) to handle billing.

    Pros: Recurring revenue, feature gating, customer management

    Cons: Requires a backend, more complex

    For sustainable Chrome extension businesses, subscriptions are the path to recurring revenue.

    Architecture Overview

    A subscription-enabled Chrome extension has three parts:

    Chrome Extension (client)

    ↕ API calls

    Your Backend or StackBE (subscription management)

    ↕ Payment processing

    Stripe (payment gateway)

    The Flow

    1. User installs extension from Chrome Web Store

    2. Extension prompts to create account (or sign in)

    3. User authenticates via magic link (email-based, works great for extensions)

    4. Extension stores session token in `chrome.storage.sync`

    5. Extension checks subscription status on load

    6. Free users see basic features; paid users unlock premium

    7. Upgrade flow opens checkout in a new tab

    8. After payment, extension detects subscription change

    Step-by-Step Implementation

    1. Set Up StackBE

    Create your StackBE app and define plans:

    Free Plan:

  • Basic feature access
  • Limited usage (e.g., 10 uses/day)
  • Pro Plan ($9/month):

  • All features unlocked
  • Unlimited usage
  • Priority support
  • 2. Authentication in the Extension

    Magic links work particularly well for extensions because:

  • No password manager integration needed
  • Email verification built-in
  • Works across devices (sync via Chrome profile)
  • Popup or Options Page Auth Flow:

    // popup.js or options.js
    async function sendMagicLink(email) {
    const response = await fetch('https://api.stackbe.io/v1/auth/magic-link', {

    method: 'POST',

    headers: {

    'Content-Type': 'application/json',

    'X-Api-Key': YOUR_API_KEY,

    'X-App-Id': YOUR_APP_ID

    },

    body: JSON.stringify({

    email,

    callbackUrl: chrome.runtime.getURL('auth-callback.html')

    })
    });
    // Show "check your email" message
    }

    Auth Callback Page:

    Create an `auth-callback.html` in your extension that captures the token from the magic link URL.

    // auth-callback.js
    const params = new URLSearchParams(window.location.search);
    const token = params.get('token');
    if (token) {
    // Store session in chrome.storage

    chrome.storage.sync.set({ sessionToken: token });

    // Redirect to success view

    window.close();

    }

    3. Checking Subscription Status

    On extension load (or popup open), check the current subscription:

    async function checkSubscription() {
    const { sessionToken } = await chrome.storage.sync.get('sessionToken');
    if (!sessionToken) {
    return { plan: 'none', authenticated: false };
    }
    const response = await fetch('https://api.stackbe.io/v1/subscriptions/current', {

    headers: {

    'Authorization': `Bearer ${sessionToken}`,

    'X-Api-Key': YOUR_API_KEY,

    'X-App-Id': YOUR_APP_ID

    }
    });
    if (response.ok) {
    const subscription = await response.json();
    return {

    plan: subscription.plan.slug,

    status: subscription.status,

    authenticated: true

    };
    }
    return { plan: 'free', authenticated: true };
    }

    4. Gating Features

    Use subscription status to enable/disable features:

    async function initExtension() {
    const { plan, status } = await checkSubscription();
    // Cache the plan locally for performance

    chrome.storage.local.set({ currentPlan: plan });

    if (plan === 'pro' && status === 'active') {

    enableProFeatures();

    } else {

    enableFreeFeatures();

    showUpgradePrompt();

    }
    }

    5. Entitlement Checks

    For granular feature gating, use StackBE's entitlements API:

    async function hasFeature(featureKey) {
    const { sessionToken } = await chrome.storage.sync.get('sessionToken');
    const response = await fetch(

    `https://api.stackbe.io/v1/entitlements/check/${featureKey}`,

    {

    headers: {

    'Authorization': `Bearer ${sessionToken}`,

    'X-Api-Key': YOUR_API_KEY

    }
    }

    );

    const { hasAccess } = await response.json();
    return hasAccess;
    }
    // Usage
    if (await hasFeature('bulk_export')) {

    showBulkExportButton();

    }

    6. Upgrade Flow

    Open Stripe checkout in a new tab:

    async function openUpgradeFlow(planId) {
    const { sessionToken } = await chrome.storage.sync.get('sessionToken');
    const response = await fetch('https://api.stackbe.io/v1/checkout/session', {

    method: 'POST',

    headers: {

    'Content-Type': 'application/json',

    'Authorization': `Bearer ${sessionToken}`,

    'X-Api-Key': YOUR_API_KEY,

    'X-App-Id': YOUR_APP_ID

    },

    body: JSON.stringify({

    planId,

    successUrl: chrome.runtime.getURL('upgrade-success.html'),

    cancelUrl: chrome.runtime.getURL('popup.html')

    })
    });
    const { url } = await response.json();

    chrome.tabs.create({ url });

    }

    7. Detecting Subscription Changes

    After checkout, detect that the subscription has changed:

    // upgrade-success.html script
    // Re-check subscription status and update extension
    async function onUpgradeSuccess() {
    const sub = await checkSubscription();

    chrome.storage.sync.set({ currentPlan: sub.plan });

    // Notify the extension popup/background

    chrome.runtime.sendMessage({ type: 'SUBSCRIPTION_UPDATED', plan: sub.plan });

    }

    Manifest V3 Considerations

    Service Workers

    MV3 uses service workers instead of persistent background pages. They're ephemeral—they can shut down between events.

    Implication: Don't rely on in-memory subscription state. Always read from `chrome.storage`.

    Content Security Policy

    MV3 has stricter CSP. You can't inline scripts or load external scripts in extension pages.

    Implication: All API calls must be from your extension's JavaScript files, not injected scripts.

    Host Permissions

    Declare API permissions in manifest.json:

    {

    "permissions": ["storage"],

    "host_permissions": [

    "https://api.stackbe.io/*"

    ]

    }

    Chrome Web Store Considerations

    Pricing Display

    The Chrome Web Store allows you to describe pricing in your listing. Mention your free tier and link to pricing for paid plans.

    Review Guidelines

    Google reviews extensions with payment flows. Ensure:

  • Clear disclosure of pricing
  • No misleading "free" claims if features are gated
  • Payment processing through established providers (Stripe qualifies)
  • Free vs Paid Split

    Common Chrome extension pricing strategy:

  • **Free tier**: Core functionality, limited usage
  • **Paid tier**: Advanced features, unlimited usage, priority support
  • Install is always free (wider distribution)
  • Upgrade from within the extension
  • Best Practices

    Cache Subscription Status

    Don't check the API on every popup open:

    async function getCachedSubscription() {
    const { cachedPlan, cacheTimestamp } = await chrome.storage.local.get([

    'cachedPlan', 'cacheTimestamp'

    ]);

    // Refresh if cache is older than 5 minutes
    if (!cachedPlan || Date.now() - cacheTimestamp > 300000) {
    const sub = await checkSubscription();

    chrome.storage.local.set({

    cachedPlan: sub.plan,

    cacheTimestamp: Date.now()

    });
    return sub.plan;
    }
    return cachedPlan;
    }

    Handle Offline Gracefully

    Extensions work offline. If subscription check fails:

  • Use cached status
  • Show last known plan
  • Don't lock users out due to network issues
  • Sync Across Devices

    Using `chrome.storage.sync` ensures session data follows the Chrome profile:

  • User signs in on laptop → syncs to desktop
  • Subscription status available everywhere
  • Show Clear Upgrade Paths

    When free users hit limits, show what they're missing:

  • "Upgrade to Pro for unlimited exports"
  • Clear pricing, one-click upgrade flow
  • Don't be aggressive—be informative
  • Revenue Expectations

    Chrome extension economics:

  • **Free install base**: 1,000-100,000+ users
  • **Conversion to paid**: 1-5% is typical
  • **Monthly price**: $5-15 for most extensions
  • **Churn**: 5-10% monthly
  • Example: 10,000 installs × 3% conversion × $9/month = $2,700 MRR

    The key is distribution. Chrome Web Store is free distribution. Pair it with StackBE for the billing backend.

    Next Steps

    1. Set up StackBE and create your plans

    2. Implement auth using magic links

    3. Add subscription checks to your extension

    4. Build the upgrade flow with Stripe checkout

    5. Test thoroughly across Chrome profiles and devices

    For a detailed walkthrough with code examples, see our blog post on adding subscriptions to Chrome extensions.

    Ready to simplify your SaaS backend?

    StackBE combines auth, billing, and entitlements in one API. Get started in minutes, not weeks.

    Get Started Free

    Frequently Asked Questions