Why Chrome Extensions Are a Hidden SaaS Goldmine
The Chrome Web Store has 3 billion potential users. Installing an extension takes 2 clicks. There's no App Store review process (review takes 1–5 days vs weeks for mobile). And because extensions live in the browser, they integrate directly into users' existing workflows.
Extensions that solve a small, specific pain in a workflow people do daily are some of the stickiest products you can build.
| Extension Category | Example Pain | Willingness to Pay |
|---|---|---|
| Productivity | "I copy-paste between tabs 50x per day" | High |
| Writing assistant | "I write emails slowly" | High |
| Research | "I need to summarise pages I'm reading" | Medium-High |
| Job search | "I apply to 20 jobs per day" | High |
| Social media | "I need to schedule posts while browsing" | Medium |
| Developer tools | "I need to format/parse data while testing" | High |

Free Stack for Extension Development
| Tool | Purpose | Cost |
|---|---|---|
| Manifest V3 (Chrome Extension API) | Extension framework | Free |
| React + Vite | Build the popup UI | Free |
| Supabase | User auth + subscription status | Free tier |
| Groq | AI features | Free tier |
| Stripe | Payments | 2.9% + $0.30 per transaction |
Step 1: Understand the Extension Architecture
A Chrome extension has three distinct execution contexts:
┌─────────────────────────────────────────────────────────┐
│ BROWSER │
│ │
│ ┌─────────────────┐ ┌───────────────────────────┐ │
│ │ POPUP │ │ CONTENT SCRIPT │ │
│ │ │ │ (runs in page context) │ │
│ │ What users see │ │ - Can read/modify DOM │ │
│ │ when they click │ │ - Communicates via │ │
│ │ the extension │ │ messages │ │
│ │ icon │ │ │ │
│ └─────────────────┘ └───────────────────────────┘ │
│ │ │ │
│ └──────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ SERVICE WORKER │ │
│ │ (background) │ │
│ │ - No DOM access │ │
│ │ - API calls │ │
│ │ - Auth state │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
Step 2: Create the Extension Structure
mkdir my-extension && cd my-extension
npm create vite@latest popup -- --template react
cd popup && npm install
# Install extension-specific packages
npm install @supabase/supabase-js webext-bridge
npm install -D @crxjs/vite-plugin
my-extension/
├── popup/ ← React app for the popup UI
│ ├── src/
│ │ ├── App.jsx
│ │ └── main.jsx
│ └── vite.config.js
├── content/
│ └── content.js ← Content script (injected into pages)
├── background/
│ └── service-worker.js ← Background service worker
├── manifest.json ← Extension configuration
└── icons/ ← 16, 32, 48, 128px icons
Step 3: The Manifest File (V3)
{
"manifest_version": 3,
"name": "My AI Extension",
"description": "AI-powered productivity tool for Chrome",
"version": "1.0.0",
"permissions": [
"activeTab",
"storage",
"contextMenus"
],
"host_permissions": [
"https://*.supabase.co/*",
"https://api.groq.com/*"
],
"action": {
"default_popup": "popup/index.html",
"default_icon": {
"16": "icons/16.png",
"32": "icons/32.png",
"48": "icons/48.png",
"128": "icons/128.png"
}
},
"background": {
"service_worker": "background/service-worker.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"]
}
],
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
}
},
Step 4: Build the Popup UI
// popup/src/App.jsx
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
function App() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [pageText, setPageText] = useState('')
const [summary, setSummary] = useState('')
const [working, setWorking] = useState(false)
useEffect(() => {
// Get current auth state
supabase.auth.getUser().then(({ data: { user } }) => {
setUser(user)
setLoading(false)
})
// Get selected text from the current page
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'GET_SELECTION' }, (response) => {
if (response?.selection) setPageText(response.selection)
})
})
}, [])
async function summarise() {
if (!pageText) return
setWorking(true)
setSummary('')
// Call your background service worker for the API call
chrome.runtime.sendMessage(
{ action: 'SUMMARISE', text: pageText },
(response) => {
setSummary(response.summary)
setWorking(false)
}
)
}
if (loading) return <div className="p-4">Loading...</div>
if (!user) return <SignIn />
return (
<div style={{ width: 360, padding: 16, fontFamily: 'Inter, sans-serif', background: '#0a0a0a', color: '#fff', minHeight: 200 }}>
<h1 style={{ fontSize: 16, fontWeight: 700, marginBottom: 12 }}>AI Assistant</h1>
{pageText ? (
<div>
<p style={{ fontSize: 12, color: '#737373', marginBottom: 8 }}>
Selected: {pageText.slice(0, 80)}...
</p>
<button
onClick={summarise}
disabled={working}
style={{ background: '#dc2626', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 16px', cursor: 'pointer', width: '100%' }}
>
{working ? 'Summarising...' : 'Summarise Selection'}
</button>
</div>
) : (
<p style={{ fontSize: 13, color: '#737373' }}>Select text on any page, then open this extension.</p>
)}
{summary && (
<div style={{ marginTop: 12, background: '#141414', borderRadius: 8, padding: 12, fontSize: 13, lineHeight: 1.6 }}>
{summary}
</div>
)}
</div>
)
},
export default App
Step 5: Monetise With Stripe
// background/service-worker.js
const FREE_LIMIT = 10 // 10 free summaries per day
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'SUMMARISE') {
handleSummarise(message.text).then(sendResponse)
return true // Keep channel open for async
}
})
async function handleSummarise(text) {
// Check usage limit from storage
const { dailyUsage = 0, lastReset, isPro = false } = await chrome.storage.local.get(['dailyUsage', 'lastReset', 'isPro'])
// Reset counter if it's a new day
const today = new Date().toDateString()
if (lastReset !== today) {
await chrome.storage.local.set({ dailyUsage: 0, lastReset: today })
}
if (!isPro && dailyUsage >= FREE_LIMIT) {
return { error: 'Free limit reached', upgradeRequired: true }
}
// Call Groq API
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${GROQ_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'llama-3.1-8b-instant',
messages: [
{ role: 'system', content: 'Summarise the following text in 3 bullet points. Be concise.' },
{ role: 'user', content: text.slice(0, 3000) }
]
})
})
const data = await response.json()
const summary = data.choices[0].message.content
// Increment usage counter
await chrome.storage.local.set({ dailyUsage: dailyUsage + 1 })
return { summary }
},
Publishing to the Chrome Web Store
- Build your extension:
npm run build - Zip the output folder
- Go to chrome.google.com/webstore/devconsole
- Pay the one-time $5 developer registration fee
- Upload your zip and fill in the listing details
- Submit for review (typically 1–3 business days)
Pricing Strategy
| Model | Price | Pros | Cons |
|---|---|---|---|
| One-time purchase | $9–$29 | Simple, good for simple tools | No recurring revenue |
| Monthly subscription | $4–$15/month | Recurring revenue | Higher churn |
| Freemium | Free + $9/month Pro | Viral growth + revenue | Requires valuable paid tier |
The most successful indie extensions use freemium: generous free tier (enough to prove value), clear paid upgrade (removes limits or adds power features).

