Shipping an AI assistant as a browser extension
How to ship an AI assistant as a browser extension with Manifest V3: service workers, content scripts, keeping API keys on the server, and store review.
By Hamza Ghouri
A browser extension is one of the best places to put an AI assistant. It sits where people already work, it can see the page they are reading, and it is one click away at all times. It is also one of the easiest places to make security mistakes, because everything you ship in an extension is readable by anyone who installs it.
Maslow, an AI assistant platform with speech-to-text, includes a browser extension alongside its backend and dashboards. This post is the architecture I'd use for any AI assistant extension built on Manifest V3, with the parts that tend to surprise people coming from web apps.
The Manifest V3 building blocks
A Manifest V3 extension has a few distinct pieces, and an AI assistant uses most of them:
- Service worker (background): the extension's event-driven backend. It handles messages, makes network calls and holds no long-lived state, because the browser shuts it down when idle.
- Content scripts: run inside web pages. They can read the page and inject UI, but they share the page's environment, so treat them as the least trusted part of your extension.
- Popup or side panel: your extension's own UI, where the assistant conversation lives.
- Storage:
chrome.storagefor settings and session tokens. Use it in place oflocalStorage, which the service worker cannot access.
The service worker going to sleep is the detail that catches people most often. Any state you keep in a global variable disappears. Put it in chrome.storage or fetch it again from your backend.
Rule one: no API keys in the extension
An extension package is a zip file of JavaScript. Anyone can unpack it and read every string. If your LLM provider key is in there, it is public, and you will find out when the bill arrives.
The pattern that works:
- The user signs in to your product (through your web app or an auth flow in the extension).
- The extension stores a user session token issued by your backend, scoped to that user.
- Every AI request goes from the extension to your backend, which checks the token, applies rate limits and quotas, and only then calls the model API with a key that never leaves the server.
That gives you per-user limits, the ability to revoke a single user, usage tracking and one place to change providers. Mobile apps have the same problem, and I cover the backend side in adding AI features to Flutter apps without leaking API keys.
Routing requests through the service worker
Content scripts should never call your API directly. Send a message to the service worker, let it attach the token and make the request, and send back the result:
// service-worker.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== "ask") return;
(async () => {
const { sessionToken } = await chrome.storage.local.get("sessionToken");
if (!sessionToken) return sendResponse({ error: "signed_out" });
const res = await fetch("https://api.example.com/assistant", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({ prompt: message.prompt, pageUrl: sender.tab?.url }),
});
sendResponse(res.ok ? await res.json() : { error: `http_${res.status}` });
})();
return true; // keeps the message channel open for the async response
});
From a content script, chrome.runtime.sendMessage({ type: "ask", prompt }) returns the response. The token stays out of the page's reach, and host permissions only need to cover your API domain for the fetch.
Reading the page safely
The selling point of an assistant extension is context: "summarize this", "reply to this email", "explain this error". That means sending page content to your backend and on to a model. Two concerns follow.
Privacy. Send only what the user asked about. Grab the selection or the main article text, and never silently ship the whole DOM, form values or other tabs. Say clearly in the UI what will be sent.
Prompt injection. Web pages are untrusted input. A page can contain hidden text written to steer your assistant ("ignore previous instructions and..."). Treat page content as data, separate it clearly from instructions in your prompt, and never let the model trigger actions on the user's behalf (sending email, submitting forms) without an explicit confirmation step. I go deeper on this in prompt injection and securing LLM features.
Permissions and the microphone
Permissions are both a security boundary and a conversion problem. A scary install prompt reduces installs, and broad permissions slow down store review. My defaults:
- Prefer
activeTabover broad host permissions. It grants access to the current tab only when the user invokes the extension. - Request host access for your own API domain only. Reserve
<all_urls>for products that truly need to run on every site. - Use optional permissions for features most users will not touch, requested at the moment they enable the feature.
Voice input
If the assistant takes voice input, as speech-to-text products do, microphone access in an extension has extra friction. The service worker has no access to media APIs, and the permission prompt often has to come from a full extension page, because a popup can close while the prompt is open. Capture audio in the popup, side panel or a dedicated extension page, and send it to the backend for transcription. The server side is the same as any voice assistant, which I cover in building a voice AI assistant with speech-to-text.
Shipping and updating
A few release habits save a lot of pain:
- Version the backend API. Users update extensions on their own schedule, so your backend will serve several extension versions at once. Breaking changes need a new endpoint version.
- Keep prompts and model choice on the server. You can improve the assistant without pushing an extension update and waiting for store review.
- Expect review time. Store review can take days. Do not plan a launch around a same-day extension release.
- Write a clear privacy disclosure. Stores ask what data you collect and why. If you've followed the rules above, the answer is short.
The short version
Treat the extension as a thin, untrusted client. The service worker talks to your backend with a user token, the backend holds every secret and every prompt, and page content is handled as untrusted data. Ask for the fewest permissions that work, and keep anything you might want to change quickly on the server.