Guides

Building Browser Extensions with open-source tools

This guide outlines the technical implementation of a Manifest V3 browser extension that integrates AI features. It focuses on solving common MV3 challenges including service worker persistence, secure API key handling, and cross-context communication between content scripts and background processes.

4-6 hours7 steps
1

Initialize the Project with Plasmo

Use Plasmo to abstract the complex Manifest V3 boilerplate. Plasmo handles the generation of the manifest.json and optimizes the build process for different browsers.

terminal
pnpm create plasmo --with-react --with-typescript my-ai-extension
cd my-ai-extension
pnpm dev

⚠ Common Pitfalls

  • Avoid manual manifest.json edits as Plasmo generates this file dynamically from your source code
  • Ensure you are using the correct Node version to avoid build-time errors with Vite
2

Configure Manifest Permissions for AI and Storage

Define the necessary permissions in the package.json (Plasmo's configuration entry point). You need 'storage' for settings and 'host_permissions' for the AI API endpoints to bypass CORS issues.

package.json
{
  "manifest": {
    "permissions": ["storage"],
    "host_permissions": ["https://api.openai.com/*"]
  }
}

⚠ Common Pitfalls

  • Requesting excessive permissions (e.g., '<all_urls>') will trigger manual reviews and likely rejection from the Chrome Web Store
  • Forgetting to include the specific API domain in host_permissions will cause fetch requests to fail in the background script
3

Implement Secure API Key Management

Do not hardcode API keys. Use chrome.storage.sync to allow users to provide their own keys via an options page, or implement a backend proxy. This step sets up a secure retrieval mechanism from the extension's storage.

lib/storage.ts
import { Storage } from "@plasmohq/storage"

const storage = new Storage()

export const getApiKey = async () => {
  return await storage.get("OPENAI_API_KEY")
}

export const saveApiKey = async (key: string) => {
  await storage.set("OPENAI_API_KEY", key)
}

⚠ Common Pitfalls

  • Storing keys in local storage without encryption is a security risk; sync storage is better but still accessible to anyone with local machine access
  • Hardcoding keys in the source will lead to immediate key revocation if the extension is published to a public repository
4

Create a Background Service Worker for AI Processing

Manifest V3 requires using Service Workers. Perform all AI API calls here to avoid CORS issues in content scripts and to keep the business logic centralized. Use message passing to communicate with the UI.

background.ts
import { Storage } from "@plasmohq/storage"

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === "GENERATE_TEXT") {
    handleAiRequest(request.prompt).then(sendResponse)
    return true; // Keep message channel open for async response
  }
})

async function handleAiRequest(prompt: string) {
  const storage = new Storage()
  const apiKey = await storage.get("OPENAI_API_KEY")
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "gpt-3.5-turbo", messages: [{ role: "user", content: prompt }] })
  })
  return response.json()
}

⚠ Common Pitfalls

  • Service workers are ephemeral; they shut down after 30 seconds of inactivity. Do not rely on global variables for state
  • Forgetting 'return true' in the onMessage listener will cause the response channel to close before the async fetch completes
5

Inject Content Scripts for UI Interaction

Inject a React component into specific web pages to provide the AI interface. This allows users to interact with your extension directly on the websites they visit.

content.tsx
import type { PlasmoCSConfig } from "plasmo"

export const config: PlasmoCSConfig = {
  matches: ["https://*.twitter.com/*", "https://*.linkedin.com/*"]
}

const AiButton = () => {
  const handleClick = async () => {
    const response = await chrome.runtime.sendMessage({ type: "GENERATE_TEXT", prompt: "Summarize this page" })
    console.log(response)
  }
  return <button onClick={handleClick} style={{ position: 'fixed', bottom: 20, right: 20 }}>AI Assist</button>
}

export default AiButton

⚠ Common Pitfalls

  • Injected styles can clash with the host page's CSS. Use Shadow DOM or highly specific CSS modules to prevent layout breakage
  • Content scripts cannot directly access the AI API due to CORS; they must always route requests through the background service worker
6

Implement Keep-Alive for Long AI Tasks

Since MV3 service workers can terminate during long-running AI streams, implement a heartbeat or use the 'chrome.offscreen' API if you need to maintain a connection longer than the 30-second window.

⚠ Common Pitfalls

  • Relying on setInterval in the background script is unreliable as the timer will be paused when the worker suspends
  • Excessive use of offscreen documents can lead to performance degradation and store rejection if not properly justified
7

Prepare for Chrome Web Store Review

Audit your code for compliance. Ensure all code is bundled without remote execution (no eval or external script loading). Generate a production build and verify the size of the ZIP file.

terminal
pnpm build
# Check the build/chrome-mv3-prod folder for the final output

⚠ Common Pitfalls

  • Using 'eval()' or 'new Function()' will result in an automatic rejection from the Chrome Web Store
  • Minified code that is unreadable by reviewers can lead to delays; provide source code in the 'Developer Tools' section of the dashboard if requested

What you built

Building for Manifest V3 requires a shift toward asynchronous, event-driven architecture. By offloading API logic to service workers and using frameworks like Plasmo, developers can focus on AI features while ensuring security and store compliance. Always prioritize user privacy and minimize permission requests to ensure a smooth review process.