May 19, 2026 5 min read

Building an AI Chrome Extension to 10,000 Users: Manifest V3 Lessons from Upwex

In 2023 I co-founded Upwex – an AI copilot for Upwork freelancers that lives inside a Chrome extension. Today it has more than 10,000 users. This post is the architecture write-up I wish I'd had on day one: not the happy path from the docs, but the walls we actually hit with Manifest V3, AI APIs, and the Chrome Web Store review process – and how we got around each one.

The architecture at a glance

An AI extension is really four applications pretending to be one:

  • Content scripts injected into the host site (in our case, Upwork pages), reading the DOM and rendering UI.
  • A service worker – the MV3 replacement for background pages – routing messages, holding no state.
  • A backend API that owns authentication, billing, and every single AI call.
  • A companion web app (React, in our case) for subscription management and analytics dashboards.

The single most important decision we made: no AI calls from the extension itself. Every LLM request goes through our backend. Keys never ship to the client, we can cache and rate-limit centrally, and we can switch model providers without shipping an extension update – which matters, because a Chrome Web Store review can take days.

Service workers are not background pages

The classic MV2 pattern – keep state in a long-lived background page – is gone. An MV3 service worker is killed after ~30 seconds of inactivity, and it will be killed mid-flight if you're careless.

What survived contact with production:

  1. Treat the service worker as a stateless router. Anything that must persist goes to chrome.storage.session or chrome.storage.local immediately. If you find yourself writing let cache = {} at the top level of the worker, you've already written a bug – it just hasn't fired yet.
  2. Long AI generations don't fit in a worker's lifetime. A proposal generation can take 20–40 seconds. We moved to a job model: the worker fires a request to the backend, the backend responds with a job id instantly, and the content script polls (or receives a push) for the result. The worker being killed in between costs nothing.
  3. Alarms over timers. setTimeout beyond the worker's lifetime silently never fires. chrome.alarms is the only scheduling primitive you can trust.

Content-script isolation will surprise you exactly once

Content scripts run in an isolated world: they see the page's DOM but not its JavaScript. On a heavily client-side-rendered site like Upwork this bites in two ways:

  • The data you want is often in the page's JS state, not the DOM. You either parse the DOM defensively or inject a script into the page's main world and message the data out. We do both, depending on the surface.
  • SPAs don't reload pages. Your content script boots once; the site then navigates ten times without a single page load. We ended up with a small router of our own – a MutationObserver-driven detector that answers one question: "which Upwork screen is the user on now?" – and mounts/unmounts our UI accordingly.

Budget real time for this. Selector-based scraping of a site you don't control is a maintenance treadmill; ours is wrapped in a single data-access layer so that when Upwork ships a redesign, the fix lands in one module, not thirty.

Surviving Chrome Web Store review

Store review is the part nobody architecture-diagrams, and it's the part that can freeze your release train for a week. What keeps our reviews passing:

  • Request the minimum permissions and justify every one. Broad host permissions (<all_urls>) invite rejection. We scope to the domains we actually touch.
  • A privacy policy that matches reality. Reviewers check. If your extension sends page content to an AI backend, say so, plainly.
  • No remote code. MV3 bans it outright, and "clever" loaders get extensions pulled. All logic ships in the bundle; only data comes from the network.
  • Keep a plain-text reviewer note explaining what the extension does and which account they can test with. It shortens reviews measurably.

The deeper implication of slow reviews is architectural: move everything volatile server-side. Prompts, pricing logic, feature flags, model choice – all of it lives in our backend and can change in minutes. The extension is a thin, stable shell around a fast-moving service.

AI cost control is a product feature

LLM tokens are your margin. Three things kept Upwex's unit economics sane:

  1. Central caching. Identical or near-identical requests (same job post, same analysis type) hit a cache, not the model.
  2. Model tiering. Cheap, fast models for classification and scoring; the expensive model only for the user-facing generation that justifies it.
  3. Hard ceilings per plan. Every subscription tier has explicit AI quotas enforced at the API layer – not in the extension, where enforcement can be bypassed by anyone with DevTools open.

What I'd tell you before you start

  • Design the extension as a thin client from day one. Store review latency makes fat clients expensive to iterate.
  • Put a single abstraction over the host site's DOM. It will change; only the abstraction should care.
  • Accept that the service worker owns nothing. Storage owns state, the backend owns logic, the worker just routes.
  • Ship crash and error telemetry from the first beta. Extension bugs reproduce on machines you'll never see.

Building an extension like this end to end – architecture, MV3 constraints, AI integration, store review – is exactly the work I do for clients. If you're planning one, the Chrome extension development page describes how an engagement runs, or just email me at sinclar96@gmail.com.

← All posts

Building something in this stack?

Get in touch