DEV Community

Cover image for How to Implement Monthly Subscriptions in Browser Extensions
Himanshu
Himanshu

Posted on

How to Implement Monthly Subscriptions in Browser Extensions

Here’s how i implemented monthly subscription inside my chrome extension without a website, just purely inside chrome extension and serverless function (at no cost).

Quick note: All the code in this tutorial comes from my boilerplate. If you'd rather skip implementing subscriptions, authentication, contact form, UI, landing page and other 1 billion boring stuff, check it out at extFast

Otherwise, let's code it

Overview of the Architecture

Before we dive in, let's understand what we're building:

The Payment Flow:

  1. User clicks "Get Premium" button in your extension popup
  2. Extension calls your serverless function to generate a checkout link
  3. User gets redirected to Polar.sh checkout page
  4. User completes payment
  5. Polar.sh sends webhook to your serverless function
  6. Serverless function stores user data in Supabase
  7. Future premium feature requests verify subscription status

What You'll Need:

  • Serverless functions (deployed on Vercel at no cost)
    • Function 1: Creates checkout links
    • Function 2: Handles Polar webhooks
    • Function 3 (optional): Serves premium features with verification
  • A Polar.sh account with a product (monthly, yearly, or one-time payment)
  • A popup page in your extension with an upgrade button
  • A database (we'll use Supabase) to store premium user data

The best part? Everything runs serverlessly, so you're not maintaining any backend infrastructure!

Tech Stack:

  • Payment Provider: Polar.sh (though you can use Stripe, Lemon Squeezy or any other provider)
  • Serverless Hosting: Vercel (again you can use whatever you want)
  • Database: Supabase (use whatever you want)
  • Extension Framework: i am using wxt but it works with any (vanilla JS, React, Vue, etc.)

(i’ll soon write a blog on Stripe too bcz i implemented it inside my extension without any problem, but for now let's start with polar)

Setting Up Polar.sh

Step 1: Create Your Polar Account

  1. Go to https://sandbox.polar.sh/ for testing (use https://polar.sh for production)
  2. Sign up for a free account

Step 2: Create an Organization

  1. After logging in, create a new organization
  2. Fill in your organization details (name, description, etc.)
  3. This organization will be associated with your products

Step 3: Create Your Product

  1. Navigate to the "Products" section in your Polar dashboard
  2. Click "+ New Product"
  3. Choose your product type:
    • Recurring subscription (monthly/yearly)
    • One-time payment (lifetime access)
  4. Set your pricing (you can start with $5-10/month for testing)
  5. Add a description
  6. Save your Product ID, you'll need this later!

Step 4: Get Your API Keys

  1. Go to Settings → General
  2. Scroll down to the bottom and Copy your Access Token

Store these securely, we'll add them in .env file shortly.

Creating Your Serverless Functions

Now let's build the backend that powers your subscription system. We'll create serverless functions that handle checkout link generation and webhook processing.

Setting Up Your Project Structure

First, install the necessary dependencies:

npm install @polar-sh/sdk @polar-sh/nextjs

/// see docs: https://polar.sh/docs/guides/nextjs
Enter fullscreen mode Exit fullscreen mode

If you're using Next.js (to create your serverless function [basically backend, not website]), your project structure should look like this:

your-project/
├── app/
│   └── api/
│       ├── checkout/
│       │   └── route.ts
│       └── webhook/
│           └── polar/
│               └── route.ts
├── lib/
│   ├── polarClient.ts
│   └── supabaseClient.ts
└── .env.local

Enter fullscreen mode Exit fullscreen mode

Configure Environment Variables (server side, not extension)

Create a .env.local file in your project root:

POLAR_ACCESS_TOKEN=your_polar_access_token_here
POLAR_MONTHLY_PRODUCT_ID=your_product_id_here
POLAR_WEBHOOK_SECRET=your_webhook_secret_here
SUPABASE_URL=your_supabase_url_here
SUPABASE_ANON_KEY=your_supabase_anon_key_here

Enter fullscreen mode Exit fullscreen mode

Create the Polar Client

Create lib/polarClient.ts:

import { Polar } from '@polar-sh/sdk';

export const polar = new Polar({
    server: 'sandbox', // remove this in production
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
});

Enter fullscreen mode Exit fullscreen mode

Function 1: Checkout Link Generator

Create app/api/checkout/route.ts:

import { polar } from '@/lib/polarClient';

async function createCheckoutPolar(userEmail: string | null = null) {
  const results = await polar.checkouts.create({
    products: [process.env.POLAR_MONTHLY_PRODUCT_ID!],
    customerEmail: userEmail,
    successUrl: 'https://yourwebsite.com/checkout-success?checkout_id={CHECKOUT_ID}',
    allowDiscountCodes: true,
  });

  console.log('Polar checkout created:', results);
  return results;
}

export async function POST(request: Request) {
  try {
    const { userEmail } = await request.json();

    const session = await createCheckoutPolar(userEmail);

    return new Response(JSON.stringify({ checkout_url: session.url }), {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error) {
    console.error('Error creating checkout URL:', error);

    return new Response(JSON.stringify({ status: 'error' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

What This Does:

  • Accepts a POST request with optional user email
  • Creates a checkout session via Polar.sh API
  • Pre-fills the user's email on the checkout page (great UX!)
  • Set a redirect url, when user made a successful payment it will redirect to your website (optional)

Pro Tip: You can skip this function entirely and hardcode a checkout link from Polar's dashboard if you don't need email pre-filling. Just go to Products → Checkout Links in Polar and grab the URL.

Deploy to Vercel

  1. Push your code to GitHub
  2. Connect your repository to Vercel
  3. Add all your environment variables in Vercel's dashboard
  4. Deploy!

Vercel will give you an API endpoint like: https://your-project.vercel.app/api/checkout

Save this URL – we'll use it in the extension.

Integrating with Your Extension

Now let's connect your extension to the serverless functions we just created.

Step 1: Create the Checkout Handler

Create a file handleCheckout.ts in your extension:

/// handleCheckout.ts

/// we will call this below function from our background script
/// it will call our vercel serverless function to generate a polar.sh checkout url
/// you can skip it and directly hard-code a checkout url, 
/// you can generate a checkout url by going into products/checkout links inside your polar dashboar
/// and put that url inside you background script and delete this function
/// we are generating checkout url only to pre-fill the user's email on the checkout page
export async function createCheckoutURL(email: string | null): Promise<string | null> {
  const CHECKOUT_ENDPOINT = 'https://your-project.vercel.app/api/checkout';

  try {
    const response = await fetch(CHECKOUT_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userEmail: email || null,
      }),
    });

    if (!response.ok) {
      console.error('Failed to create checkout URL');
      return null;
    }

    const results = await response.json();
    return results.checkout_url;
  } catch (error) {
    console.error('Error fetching checkout URL:', error);
    return null;
  }
}

// call this function from popup (or you can call it from content script too), 
// then it will send a message inside the background script to start the checkout flow
// then background script will call this above "createCheckoutURL" function to generate URL
// once the checkout url is generated, background script will redirect user to the checkout url
export async function invokeCheckout(email?: string) {
  await chrome.runtime.sendMessage({
    type: 'checkout',
    userEmail: email,
  });
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Your Background Script

Create or update background.ts:

import { createCheckoutURL } from './handleCheckout';

async function handleMessages(message: TMessage, sender: any, sendResponse: any) {
  if (message.type === 'checkout') {
    // Generate the checkout link
    const checkoutLink = await createCheckoutURL(message.userEmail);

    if (!checkoutLink) {
      console.error('❌ Failed to create checkout link');
      return;
    }

    // Redirect user to checkout page (no special manifest permission needed)
    chrome.tabs.create({ url: checkoutLink }, function (tab) {
      console.log('✅ Redirecting to checkout:', checkoutLink);
    });
  }
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  handleMessages(request, sender, sendResponse);
  return true; // Required for async responses
});

/// some typescript stuff, ignore it
type TMessage = {
  type: 'checkout';
  userEmail?: string;
};

Enter fullscreen mode Exit fullscreen mode

How This Works:

  1. Extension popup sends a message to background script
  2. Background script calls your Vercel function
  3. Vercel function generates a Polar checkout URL
  4. Background script opens a new tab with the checkout URL
  5. User completes payment on Polar's checkout page

Step 3: Create the Premium Upgrade Card (UI)

In your popup or content script, create a premium upgrade component:

// components/PremiumCardWidget.tsx

import { invokeCheckout } from './handleCheckout';

export default function UpgradeCard() {
  // Get user's email from your auth system or extension storage
  const userEmail = 'user@example.com'; // Replace with actual email

  async function handleUpgradeClick() {
    await invokeCheckout(userEmail);
  }

  return (
    <div className="premium-card">
      <h3>Upgrade to Premium</h3>
      <p>Unlock all features with Premium</p>
      <ul>
        <li>✨ Feature 1</li>
        <li>🚀 Feature 2</li>
        <li>💎 Feature 3</li>
      </ul>
      <button onClick={handleUpgradeClick}>
        Get Premium - $9.99/month
      </button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

User Experience Flow:

  1. User clicks "Get Premium" button
  2. Extension sends message to background script
  3. New tab opens with pre-filled checkout form
  4. User enters payment details on Polar.sh
  5. After payment, user is redirected to your success page
  6. Webhook triggers (we'll handle this next)

Now the most scariest part: Webhooks

Webhooks are how Polar.sh notifies your backend when important events happen (like successful payments or cancellations). This is the crucial piece that keeps your database synchronized with payment status.

Setting Up the Database

First, let's create a Supabase table to store premium users.

SQL Schema for Supabase:

CREATE TABLE PremiumUsers (
  user_email TEXT PRIMARY KEY,
  subscription_status TEXT NOT NULL,
  plan_type TEXT,
  credits_used INTEGER DEFAULT 0,
  subscribed_at TIMESTAMPTZ,
  expires_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  subscription_id TEXT,
  customer_id TEXT
);

Enter fullscreen mode Exit fullscreen mode

Table Structure Explanation:

  • user_email: Primary key, ensures one subscription per email
  • subscription_status: 'active', 'cancelled', 'trialing', etc.
  • plan_type: 'monthly', 'yearly', 'lifetime'
  • credits_used: Track usage if you have consumption limits
  • subscribed_at: When subscription started
  • expires_at: When current period ends
  • subscription_id: Polar's subscription ID
  • customer_id: Polar's customer ID

Create Supabase Client (server, not extension)

Create lib/supabaseClient.ts:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_ANON_KEY!;  /// or Supabase Secret key if you have RLS policy enabled, anon key will not work i guess

export const supabase = createClient(supabaseUrl, supabaseKey);

Enter fullscreen mode Exit fullscreen mode

Function 2: Webhook Handler

Create app/api/webhook/polar/route.ts:

import { Webhooks } from '@polar-sh/nextjs';
import { supabase } from '@/lib/supabaseClient';

// This function handles any subscription status change
async function handleSubscriptionChange(payload: any) {
  try {
    const { error } = await supabase
      .from('PremiumUsers')
      .upsert(
        {
          user_email: payload.data.customer.email,
          subscription_status: payload.data.status,
          subscribed_at: new Date(payload.data.currentPeriodStart).toISOString(),
          expires_at: new Date(payload.data.currentPeriodEnd).toISOString(),
          subscription_id: payload.data.id,
          customer_id: payload.data.customerId,
          plan_type: 'monthly',
        },
        { onConflict: 'user_email' }
      )
      .select();

    if (error) {
      console.error('❌ Supabase upsert failed:', error);
      throw error;
    }

    console.log('✅ Successfully updated subscription for:', payload.data.customer.email);
  } catch (error) {
    console.error('Error in handleSubscriptionChange:', error);
    throw error;
  }
}

// Export the webhook handler
export const POST = Webhooks({
  webhookSecret: process.env.POLAR_WEBHOOK_SECRET!,
  onSubscriptionActive: handleSubscriptionChange,
  onSubscriptionUpdated: handleSubscriptionChange,
  onSubscriptionRevoked: handleSubscriptionChange,
  onSubscriptionCanceled: handleSubscriptionChange, // avoid it bcz "onSubscriptionRevoked" will trigger on user's subscription end, which is better
});

Enter fullscreen mode Exit fullscreen mode

What This Webhook Handles:

  • onSubscriptionActive: New subscription created
  • onSubscriptionUpdated: Subscription plan changed
  • onSubscriptionRevoked: Subscription cancelled/expired
  • onSubscriptionCanceled: User explicitly cancelled (but be a good person and let them use your service until their plan end after 30 days or something, don't instantly stop their access, avoid it)

The upsert operation is clever, it either inserts a new record or updates an existing one based on the email, so you never get duplicates.

Configure Webhook in Polar.sh

  1. Deploy your webhook handler/function to Vercel
  2. Copy your webhook endpoint: https://your-project.vercel.app/api/webhook/polar
  3. Go to Polar dashboard → Settings → Webhooks
  4. Click "Add Endpoint"
  5. Paste your webhook URL
  6. Select the events you want to receive (at minimum: subscription.active, subscription.updated, subscription.revoked)
  7. Copy the webhook secret and paste it inside the .env.local file as POLAR_WEBHOOK_SECRET

Testing Your Webhook:

You can use ngrok to test your webhooks, see this doc

Handling Premium Users

Now that users can subscribe and your database stays updated, let's implement the verification logic for premium features.

Function 3: Premium Feature with Verification (demo)

Create app/api/generate-ai/route.ts (or any premium endpoint):

import { supabase } from '@/lib/supabaseClient';

async function isEligibleUser(userEmail: string): Promise<boolean> {
  try {
    const { data, error } = await supabase
      .from('PremiumUsers')
      .select()
      .eq('user_email', userEmail)
      .single();

    if (error) {
      console.error('Error fetching user:', error);
      return false;
    }

    if (!data) {
      return false;
    }

    const status = data.subscription_status.trim().toLowerCase();

    // Check if subscription is active or in trial period
    if (status === 'active' || status === 'trialing') {
      // Optional: Also verify expiration date
      const expiresAt = new Date(data.expires_at);
      const now = new Date();

      if (expiresAt > now) {
        return true;
      }
    }

    return false;
  } catch (error) {
    console.error('Exception in isEligibleUser:', error);
    return false;
  }
}

async function generateAIContent(userEmail: string, prompt: string) {
  const isEligible = await isEligibleUser(userEmail);

  if (!isEligible) {
    return {
      error: 'Premium subscription required',
      code: 'SUBSCRIPTION_REQUIRED',
    };
  }

  // User is verified premium, do your premium logic here
  // For example, call OpenAI API, generate content, etc.
  const aiResponse = await yourAIFunction(prompt);

  // Optional: Track usage
  await supabase
    .from('PremiumUsers')
    .update({ credits_used: data.credits_used + 1 })
    .eq('user_email', userEmail);

  return {
    success: true,
    content: aiResponse,
  };
}

export async function POST(request: Request) {
  try {
    const { userEmail, prompt } = await request.json();

    if (!userEmail) {
      return new Response(
        JSON.stringify({ error: 'User email required' }),
        { status: 400 }
      );
    }

    const result = await generateAIContent(userEmail, prompt);

    return new Response(JSON.stringify(result), {
      status: result.error ? 403 : 200,
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error) {
    console.error('Error in premium endpoint:', error);
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500 }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Call Premium Endpoint from Extension (demo)

In your extension, whenever you need to access premium features:

async function usePremiumFeature(userEmail: string, data: any) {
  const response = await fetch('https://your-project.vercel.app/api/generate-ai', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userEmail: userEmail,
      prompt: data.prompt,
    }),
  });

  const result = await response.json();

  if (result.error) {
    // Show upgrade prompt
    return null;
  }

  return result.content;
}

Enter fullscreen mode Exit fullscreen mode
one last thing you need to do is set the CORS, in your vercel serverless:
  • configure next.config.js with allowed origins
  • read this doc for more info

Checklist

Let's make sure you've got everything set up correctly:

Polar.sh Setup

  • ✅ Created sandbox/production account
  • ✅ Created organization
  • ✅ Created product with pricing
  • ✅ Copied product ID
  • ✅ Generated API access token
  • ✅ Generated webhook secret (after adding you serverless function endpoint)

Serverless Functions (Vercel)

  • ✅ Installed dependencies (@polar-sh/sdk, @polar-sh/nextjs)
  • ✅ Created checkout endpoint (/api/checkout)
  • ✅ Created webhook handler (/api/webhook/polar)
  • ✅ Added all environment variables
  • ✅ Deployed to Vercel

now you are pretty much done :)

Skip All This

implementing subscriptions is just the beginning. You still need authentication like signin with Google etc, a contact form, a privacy policy page, SEO, a landing page…. and the list goes on.

I packaged all this in extFast so you can skip the boring stuff and ship faster.

thankyou for reading,

bye bye :)

Top comments (0)