VibeWeek
Home/Grow/Browser Extension as Product Companion — Chat Prompts

Browser Extension as Product Companion — Chat Prompts

⬅️ Back to 6. Grow

If your B2B SaaS workflow happens partly in OTHER applications (web, email, social, customer's CRM, customer's project tool), at some point a customer asks: "Can I do X without leaving the page?" Browser extensions are the answer. 1Password autofills passwords across the web. Grammarly checks grammar on every input. Loom records videos from any tab. Notion Web Clipper saves articles. The list of B2B SaaS with a browser extension grew dramatically 2022-2026 because the extension is sticky (always-on), proximate (where work happens), and a moat (most competitors don't ship one).

The naive shape: "We'll build a Chrome extension." Six months later, a half-finished extension with bugs in Firefox, broken in Safari, rejected by Chrome Web Store, with manifest v3 migration issues. The right shape: a real plan for cross-browser support (Chrome / Edge / Firefox / Safari), manifest v3 from day one, message-passing architecture, content-script discipline, store submission process, automated update flow, and the safety/security posture browser extensions require.

This chat walks through implementing a real browser extension companion to your SaaS: scope decisions, architecture (background / content / popup / options), cross-browser strategy, store submission, update mechanics, and operational realities.

What you're building

  • Browser extension built on manifest v3
  • Cross-browser support (Chrome / Edge / Firefox / Safari)
  • Content scripts (interact with pages users visit)
  • Background service worker (long-running logic; auth)
  • Popup / sidebar UI
  • Options page
  • Auth / SSO with your main app
  • Cross-tab + cross-device state sync
  • Update mechanism
  • Store submission + review compliance
  • Telemetry + crash reporting

1. Decide what your extension does

Help me decide what shape of extension to build.

Three increasingly-deep shapes:

LEVEL 1: SIMPLE COMPANION (the simplest start)
- Read-only or single-action utility
- Examples: bookmark current tab; quick note to your app
- Pros: ships in 2-4 weeks
- Cons: limited utility; user might not install

LEVEL 2: ACTIVE COMPANION (passive observer)
- Reads page content; surfaces relevant info from your app
- Examples: Grammarly (inline grammar check); Honey (deal lookup); LinkedIn-finder (find someone's email)
- Pros: high-utility; sticky
- Cons: 8-16 weeks; permission concerns; performance

LEVEL 3: DEEP INTEGRATION
- Modifies pages; injects UI; takes actions on user's behalf
- Examples: 1Password (autofill); Loom (record from any tab); Notion Web Clipper
- Pros: highest utility; competitive moat
- Cons: 16-32 weeks; manifest v3 limitations; user trust

LEVEL 4: BROWSER-AS-CANVAS
- Extension IS a primary product surface
- Examples: Arc browser (different category); side-panel apps
- Pros: deep integration
- Cons: Chrome/Edge specific; very specialized

DEFAULT FOR MOST B2B SaaS:
- Year 1-2: NO extension (focus on core product)
- Year 2+: Level 1-2 if customers asking
- Year 3+: Level 3 if part of competitive moat

Don't pre-build extensions speculatively. Customer pull required.

Output: scope decision + boundary.

Output: scope statement.

2. Manifest V3 architecture

As of 2026, Chrome / Edge / Firefox have all migrated to Manifest V3. Build manifest v3 from day one.

Key MV3 changes (from MV2):
- Background scripts → service workers (no persistent background)
- Remote code disallowed
- declarativeNetRequest replaces webRequest blocking
- Cross-origin restrictions tighter

Manifest.json structure:

{
  "manifest_version": 3,
  "name": "YourApp Extension",
  "version": "1.0.0",
  "description": "Brief description",
  "permissions": [
    "storage",
    "activeTab",
    "tabs"
  ],
  "host_permissions": [
    "https://*.yourapp.com/*",
    "https://*.example.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*.yourapp.com/*"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "options_page": "options.html",
  "icons": { ... },
  "web_accessible_resources": [...]
}

Key components:

A. SERVICE WORKER (background.js)
- Long-running logic
- Auth refresh tokens
- API calls to your backend
- Message routing
- Note: SW can be terminated by browser; persist state in chrome.storage

B. CONTENT SCRIPT (content.js)
- Runs in page context (DOM access)
- Sandboxed (separate from page's JS)
- Can inject UI; modify DOM
- Communicates with SW via chrome.runtime.sendMessage

C. POPUP (popup.html / popup.js)
- Click extension icon → popup
- Most common UI surface
- Limited size; ephemeral (closes on click-away)

D. OPTIONS PAGE
- Extension settings
- Persistent; full page

E. SIDE PANEL (Chrome 114+)
- Persistent sidebar UI
- Newer; Chrome / Edge

Architecture pattern:

User clicks icon → Popup opens → Communicates with SW → SW calls your API → Returns data → Popup displays
User on website → Content script injected → Detects page state → Communicates with SW → SW calls API → Returns → Content script renders UI

Implement:
1. manifest.json with required permissions
2. Service worker setup
3. Content script for target sites
4. Popup UI (React / Vanilla JS / your framework)
5. Options page
6. Message-passing patterns

Output: extension architecture.

3. Cross-browser strategy

Chrome dominates (~70% market share). But Edge, Firefox, Safari matter.

Shared codebase approach:

- Manifest v3 mostly compatible across Chrome / Edge / Firefox
- Safari has its own quirks (uses different webkit-based browser API)
- Most code shared; conditionals per browser API differences

Tooling:

WebExtension Polyfill
- Mozilla's polyfill: lets you use `browser.*` (promise-based) in Chrome
- npm install webextension-polyfill
- Cross-browser code: import { browser } from 'webextension-polyfill'

Build tools:
- Webpack / Vite / Rollup
- Plasmo (modern extension framework)
- WXT (newer; opinionated)
- Build per-browser variants from same codebase

Per-browser build outputs:
- Chrome: dist/chrome (manifest v3)
- Edge: dist/edge (similar to Chrome)
- Firefox: dist/firefox (slight manifest differences)
- Safari: dist/safari (Xcode wrapper required)

Differences to handle:

Chrome / Edge:
- Mostly identical
- Manifest v3 stable
- Service worker model

Firefox:
- Manifest v3 supported (since 2024)
- Background service worker BUT also supports event pages (less restrictive)
- Some differences in action vs browser_action

Safari:
- Requires Xcode wrapper (Safari Web Extension)
- Different APIs in some cases
- App Store distribution
- Different review process

Strategy:
- v1: Chrome + Edge (90% of market; same code)
- v2: Firefox (5% of market; small additions)
- v3: Safari (4% of market; Xcode wrapper)

Implement:
1. Single codebase with browser polyfill
2. Build pipeline producing per-browser dist
3. Per-browser manifest variations
4. Test on each browser before release

Output: cross-browser maintainable codebase.

4. Authentication + SSO with main app

Critical: extension auth integration with your main SaaS.

Patterns:

PATTERN A: SHARED COOKIE / SESSION
- User logs into yourapp.com in browser
- Extension reads session cookie
- Extension uses session for API calls
- Pros: zero-friction auth (already logged in)
- Cons: requires user to be logged in to web; cookies cross-domain restrictions

PATTERN B: OAUTH FLOW
- Extension opens new tab → OAuth flow → redirects back to extension callback
- Extension stores token in chrome.storage
- Token used for API calls
- Pros: works even when not logged into web
- Cons: more friction first-time

PATTERN C: API KEY (DEV TOOLS)
- User generates API key in app settings
- Pastes into extension options
- Pros: simple
- Cons: friction; not for consumer SaaS

DEFAULT 2026: Pattern A (cookie-based) with Pattern B fallback.

Implementation:

Cookie-based:

// Background service worker
async function getAuthToken() {
  const cookies = await chrome.cookies.getAll({ domain: 'yourapp.com', name: 'session' })
  const sessionCookie = cookies.find(c => c.name === 'session')
  if (!sessionCookie) {
    return null  // user not logged in
  }
  // Use cookie value as token; or exchange for API token
  return sessionCookie.value
}

OAuth flow:

const AUTH_URL = 'https://yourapp.com/oauth/authorize?client_id=...&redirect_uri=...'
const TOKEN_URL = 'https://yourapp.com/oauth/token'

async function startAuth() {
  const redirectUri = chrome.identity.getRedirectURL('callback')  // chromiumapp.org
  const url = `${AUTH_URL}&redirect_uri=${encodeURIComponent(redirectUri)}`
  
  const result = await chrome.identity.launchWebAuthFlow({
    url,
    interactive: true,
  })
  
  const code = new URL(result).searchParams.get('code')
  const tokenResp = await fetch(TOKEN_URL, {
    method: 'POST',
    body: JSON.stringify({ code, redirect_uri: redirectUri }),
  })
  
  const { access_token, refresh_token } = await tokenResp.json()
  await chrome.storage.local.set({ access_token, refresh_token })
}

State across tabs:
- chrome.storage.local for persistent
- Sync state via storage events
- Service worker mediates state changes

Logout:
- Clear chrome.storage
- Clear cookies if applicable
- Notify all tabs / content scripts

Implement:
1. Auth flow (cookie-based primary; OAuth fallback)
2. Token refresh logic
3. Cross-tab state sync
4. Logout flow
5. UI states (logged-in vs logged-out)

Output: auth that works without making customers re-login.

5. Permissions discipline

Extensions request permissions at install. Users see them. Excessive permissions = lower install rate + store rejection.

Permission categories:

REQUIRED PERMISSIONS:
- storage (always)
- activeTab (current tab only; preferred over tabs)

OPTIONAL (request when needed):
- tabs (all tabs; broader)
- cookies (specific domains)
- notifications
- contextMenus
- alarms

HOST PERMISSIONS:
- "https://*.yourapp.com/*" (your domain; required)
- "https://*.example.com/*" (specific 3rd party)
- "<all_urls>" (everything; AVOID; users distrust)

Best practices:

A. Request minimum
- activeTab > tabs (only current tab; user clicks extension)
- Specific hosts > <all_urls>
- Optional permissions requested when feature triggered

B. Optional permissions (request at runtime)

if (!await chrome.permissions.contains({ origins: ["https://example.com/*"] })) {
  await chrome.permissions.request({ origins: ["https://example.com/*"] })
}

Justify each permission in store listing.

C. Avoid permissions you don't need
- "history" — almost never needed
- "downloads" — only if downloading
- "browsingData" — rarely needed
- "geolocation" — rarely needed for B2B

Common Chrome Web Store rejections:
- "Excessive permissions"
- "Not all permissions listed are used"
- "Host permissions too broad"

D. Privacy posture
- Privacy policy required (URL in manifest)
- Data handling clear
- Don't ship "spy" extensions (track every page; sell data)

Implement:
1. Audit permissions list; remove unused
2. Use activeTab + optional permissions where possible
3. Privacy policy publicly hosted
4. Document each permission's purpose

Output: minimal-permissions extension that passes review.

6. Content script patterns

Content scripts run in page context. They're how you "augment" other websites.

Key patterns:

A. DETECT PAGE STATE
- URL pattern (e.g., LinkedIn profile page)
- DOM elements (e.g., "Compose" button on Gmail)
- Page metadata (Open Graph; structured data)

B. INJECT UI (overlay, sidebar, inline)
- Floating button
- Sidebar / panel
- Inline annotations on page elements
- Match site's visual style (or distinct extension style)

C. MODIFY PAGE (carefully)
- Don't break the host site
- Use Shadow DOM to isolate styles
- Don't override user-visible elements

D. COMMUNICATE WITH SERVICE WORKER
- chrome.runtime.sendMessage / onMessage
- For API calls (CORS often easier from SW)
- For persistence (chrome.storage)

Example: LinkedIn email finder

1. Content script detects: location.pathname starts with '/in/'
2. Extracts profile data from DOM
3. Sends to service worker
4. SW calls your API: "find email for this profile"
5. Returns email
6. Content script injects email next to profile

Code:

// content.js
function detectProfile() {
  if (location.pathname.startsWith('/in/')) {
    const profile = {
      name: document.querySelector('.profile-name')?.textContent,
      title: document.querySelector('.profile-title')?.textContent,
      company: document.querySelector('.company')?.textContent,
    }
    
    chrome.runtime.sendMessage({ type: 'find_email', profile }, (response) => {
      if (response.email) injectEmailUI(response.email)
    })
  }
}

function injectEmailUI(email) {
  const button = document.createElement('div')
  button.className = 'yourapp-extension-email'
  button.textContent = email
  document.querySelector('.profile-section').appendChild(button)
}

// Run on DOM ready + on URL change (SPA navigation)
detectProfile()
const observer = new MutationObserver(() => detectProfile())
observer.observe(document.body, { childList: true, subtree: true })

CSS isolation:
- Use Shadow DOM
- Or namespace your classes (.yourapp-extension-*)
- Avoid !important hacks

Performance:
- Don't observe large subtrees aggressively
- Debounce mutations
- Defer non-critical work to idle

Implement:
1. Content script for target sites
2. UI injection patterns
3. Style isolation
4. Communication with SW
5. Performance + cleanup

Output: content scripts that don't break host sites.

7. Update + distribution

Browser extensions update through stores.

Distribution channels:

CHROME WEB STORE
- Submission via Developer Dashboard
- Review takes 1-7 days typically
- $5 one-time developer fee
- Single ZIP upload
- Update reviews can take longer

EDGE ADD-ONS
- Microsoft Edge Add-ons store
- Free
- Faster review (often 1-2 days)
- Same package as Chrome (mostly)

FIREFOX ADD-ONS (AMO)
- Mozilla AMO
- Free
- Review can vary (1-7 days; auto-publish for trusted devs)
- Different manifest tweaks

SAFARI APP STORE
- Distribute via Mac App Store
- $99/yr Apple Developer
- More rigorous review
- Xcode wrapper required

Update flow:
- New version bumped in manifest.json
- Submit to each store
- Auto-update for users (within 24-48h typically)
- Force-update mechanism if critical (via app-side check)

Versioning:
- Semver: major.minor.patch
- Track in CRM what version users have (telemetry)
- Sunset old versions if breaking changes

Self-hosted (enterprise):
- Some enterprise customers want self-hosted extension
- Distributed via group policy (Chrome Enterprise)
- Update via private CRX hosting

Implement:
1. CI/CD pipeline for builds per browser
2. Submission scripts per store
3. Telemetry for version distribution
4. Force-update mechanism (rarely; for critical security)

Output: distribution + update mechanics.

8. Operational realities + edge cases

Walk me through:

1. Service worker terminated mid-operation
- MV3 SW can be killed; restart on next message
- Persist state in chrome.storage
- Don't rely on in-memory state

2. User on flaky network
- API calls fail; retry with backoff
- Cache last-known state
- Show "offline" UI

3. User logged out of main app
- Detect 401 from API
- Prompt re-login (open new tab to login page)
- Don't break passively

4. Page changes mid-action (SPA navigation)
- MutationObserver to detect URL changes
- Cleanup + re-detect on URL change
- Cancel in-flight requests if no longer relevant

5. User has multiple devices / browsers
- chrome.storage.sync for cross-device sync (4KB limit; small data)
- chrome.storage.local for local-only (10MB)
- Cloud-side state for large data

6. Popup closes mid-action
- Popup ephemeral; closes on click-away
- Long operations should run in SW
- Use chrome.action.setBadgeText for status

7. Browser updates break manifest
- MV2 → MV3 was the big one (2024 deadline)
- Future changes possible; monitor browser release notes
- CI tests against beta browser releases

8. Privacy / legal
- GDPR / CCPA compliance for any data collection
- Privacy policy required by all stores
- Don't collect data you don't need

9. Extension blocks (corporate firewall)
- Some enterprises block all extensions
- Some allowlist specific extensions
- Document for IT teams

10. Performance impact
- Slow extension = slow browser; users uninstall
- Profile in DevTools
- Lazy-load; don't run on every page if narrow scope

11. Extension compatibility (with other extensions)
- Conflicts possible (CSS clobbering; event handler races)
- Test with popular extensions installed
- Use Shadow DOM aggressively

12. Bug reports + telemetry
- Sentry / similar for crash reporting
- Usage analytics (opt-in; transparent)
- Version tracking for support

13. Store rejection
- Common reasons: excessive permissions, unclear privacy, non-functional
- Address feedback; resubmit
- Appeal process available

14. Managed enterprise deployment
- Chrome Enterprise: Group Policy auto-install
- Documentation for IT admins
- Settings via policy

For each: code change + UX impact + ops consideration.

Output: ops handling.

9. Recap

What you've built:

  • Manifest v3 extension scaffold
  • Service worker + content script + popup architecture
  • Cross-browser support (Chrome / Edge primary; Firefox / Safari secondary)
  • Auth via shared cookie / OAuth fallback
  • Minimal-permissions design
  • Content script with UI injection patterns
  • State persistence via chrome.storage
  • Submission to Chrome Web Store + Edge Add-ons
  • Telemetry + crash reporting
  • Operational handling for SW termination, network issues, updates

What you're explicitly NOT shipping in v1:

  • Safari (defer; Xcode-wrapped App Store path)
  • Firefox (defer if Chrome+Edge gets you 90%)
  • Mobile browsers (limited extension support)
  • Browser-specific tricks (defer; focus cross-browser)
  • AI features inside extension (defer; specialty work)

Ship Level 1 in 2-4 weeks. Level 2 in 8-16 weeks. Level 3 in 16-32 weeks if extension is competitive moat.

The biggest mistake teams make: building extensions speculatively. Without customer pull, extensions are unused — and they're significant maintenance burden.

The second mistake: too many permissions. Users see "this extension can read all your data" and uninstall. Be ruthlessly minimal.

The third mistake: skipping cross-browser early. Code locked to Chrome-specific APIs is hard to retrofit. Use polyfill from day 1.

See Also