Written by Huifer, solo developer and maintainer of TanStack Ship. I shipped a Chrome extension + SaaS companion stack for a creator-tools product over 11 working days in March 2026. The seven-module layout, $0 idle cost, and 50ms cold-start number below come from that build, not from a marketing deck. I deployed to production on day 11 with 47 paying customers on day 14 and 412 paying users by month three.
Verified sources: Chrome MV3 Manifest Reference · Chrome Network Requests Docs · TanStack Start Documentation · Cloudflare Workers Documentation · Cloudflare D1 Documentation · Cloudflare Durable Objects · Better Auth Edge Session Guide · TanStack Ship GitHub
Last updated: 2026-05-05 · Changelog
TL;DR: A "Chrome extension + SaaS" stack is one vertical, not two products. TanStack Ship ships the seven modules you need (extension manifest, SaaS app, auth bridge, Stripe billing, edge sync layer, admin dashboard, deploy pipeline) wired together with one
pnpm dev. Total idle cost: $0. Cold-start p99: 50ms. End-to-end build time for a real product: 11 days. Below: the field notes, the seven modules, the cost math, the production code, and the three cases where you should not pick this stack.
Why Chrome Extension + SaaS Is a Real Vertical
Browser extensions used to be solo weekend projects. In 2026 they are a product category with their own revenue shape, customer acquisition loop, and unit economics. A creator-tools Chrome extension I shipped in March 2026 hit $1,800 MRR by month three with 412 paying users, and the SaaS companion (a dashboard for usage analytics and team management) was the dominant revenue line, not the extension itself.
The market has matured on every front. MV3 is the only Chrome Web Store target as of 2024. Stripe ships dedicated extension billing patterns including the in-extension checkout flow. And Cloudflare Workers — see the Workers documentation — make the backend trivially cheap at low scale by collapsing the runtime into V8 isolates that spin up in milliseconds instead of pulling a Node container. The vertical is real and the unit economics work.
Three product shapes dominate the category, and the seven-module layout only fits one of them. Knowing which shape you are building determines the rest:
- Browser-first — the extension is the product; the SaaS dashboard is optional (e.g., 1Password, Grammarly). Marginal SaaS revenue. Most extensions never escape this shape.
- Companion-product stack — the extension is the daily-driver surface; the SaaS is where teams, billing, and analytics live (e.g., Loom, Notion Web Clipper, Reader Mode apps). Most revenue. This is the shape this post covers.
- Extension-as-channel — the SaaS is the real product; the extension is a marketing/acquisition surface (e.g., Linear, Superhuman). Lowest extension stickiness. Different economics.
This post is about the middle shape — the companion-product stack — because that is where TanStack Ship's seven-module layout earns its keep. The other two shapes need different stacks and the decision tree at the bottom names them.
The 7 Modules That Ship Together
The build sequence below is what I followed for the creator-tools product. Each module took between half a day and two days. The order matters: deploying a working MV3 manifest first forced me to make UX choices early, which avoided over-design on the SaaS side and kept the seven modules honest.
Module 1: Extension Manifest (MV3) + Messaging Layer
The first commit was a working MV3 manifest that loads on any page, injects a content script, and round-trips a message to the extension's service worker. No SaaS, no auth, no UI. The Chrome team's MV3 manifest reference and network requests docs are the only two documents I needed for this module.
MV3 forces you to give up background pages in favor of service workers. Service workers are event-driven and shut down after 30 seconds of inactivity. The implication: any state you want to persist must live in chrome.storage.session (cleared on browser restart) or chrome.storage.local (persistent across sessions). TanStack Ship's extension template wires both with typed wrappers so the rest of the codebase treats them as a synchronous key-value store.
Module 2: SaaS Companion (TanStack Start)
The web app that hosts team dashboards, billing portals, and admin tools. I used TanStack Start because the Server Functions pattern lets me write the extension's API endpoints and the SaaS routes in one codebase with shared types. End-to-end type safety from extension popup to service worker to Cloudflare Worker to D1. No API client codegen step, no manual schema sync, no drift.
The SaaS side also carries the marketing surface: landing page, pricing, blog, login. TanStack Ship ships these routes pre-built with Stripe integration, Paraglide JS for i18n, and SEO meta tags wired. For our pricing comparison breakdown, this is where the dollar math happens.
Module 3: Auth Bridge (Better Auth Session Shared Between Ext + SaaS)
The hard part. The extension popup cannot use cookies reliably because of third-party cookie restrictions on the host page; the SaaS uses cookies. Without a bridge you end up with two parallel login flows and users get confused.
I share the Better Auth session by issuing a short-lived (15-minute) signed JWT from the SaaS login endpoint, which the extension stores in chrome.storage.session and attaches to every API call as Authorization: Bearer. Refresh on expiry. The bridge is invisible to feature code because it lives in a shared lib/ that both the extension and the SaaS import.
The session verification lives on the Worker side and uses Better Auth's edge-compatible verifier. No Node-only crypto, no jsonwebtoken workaround. The whole bridge is roughly 80 lines of code plus tests.
Module 4: Stripe Billing Server (Webhook Idempotent)
Stripe Checkout for upgrades, customer portal for downgrades, webhook handler with idempotency keyed on event.id (a pattern I learned the hard way — see the postmortem in our docs for the exact incident). The webhook is wrapped in a Durable Object for exactly-once processing across Stripe's retry attempts.
The billing module ships three flows out of the box: one-time purchase, recurring subscription, and metered usage. The creator-tools product uses metered usage (per-highlight pricing for free users, flat subscription for paid). Adding a fourth flow — seat-based team pricing — took half a day because the schema and webhook handler are extensible by design.
Module 5: Cloudflare Workers Sync Layer (KV + Durable Objects)
Real-time sync between extension state and the SaaS. Cloudflare Durable Objects hold per-user presence and live edit state; KV holds feature flags and config. Workers connect the two.
The pattern that took the longest to get right: the extension's "save highlight" event needs to update the SaaS dashboard in under a second without the user pressing refresh. The solution was a Durable Object per user that holds an in-memory list of recent highlights and writes to D1 asynchronously. The SaaS dashboard reads from D1 on page load and subscribes to a Server-Sent Events stream for live updates. End-to-end latency: 200ms median, 800ms p99.
Module 6: Admin Dashboard (Extension Install Analytics, MRR, Churn)
The MRR/ARR dashboard, churn cohorts, extension install funnel, and a "first-30-days activation" view. This is the module that pays for the whole stack: without it you cannot tell which marketing channel is producing paying users versus free-tier squatters.
For the creator-tools product, the activation view surfaced one brutal insight within the first week: 62% of extension installers never opened the popup a second time. That single metric drove three product changes that lifted day-30 retention from 18% to 41%. No amount of static analysis of the codebase would have surfaced that.
Module 7: Edge Function Deploy + GitHub Action
A GitHub Action that deploys the SaaS to Cloudflare Workers on main push and publishes the extension to the Chrome Web Store on a tag. The deploy pipeline is the unsexy module that everyone underestimates — mine took a full day because of the Chrome Web Store review queue (3 days average for updates, 7 days for first submissions) and the OAuth dance to get the publish API token.
The action also runs the Worker bundle size check and fails the deploy if any Server Function exceeds 1MB (the Workers limit). This single check has caught three regressions before they reached production. Without it, a heavy Stripe SDK import would silently push you past the bundle limit and Workers would refuse to deploy.
What You Skip (and What You Still Build)
The seven modules above are not "free." TanStack Ship gives you the wiring: project structure, auth glue, billing glue, deploy pipeline, admin shell. What you still ship yourself:
- The actual product surface — the popup UI, the content script's interaction with the host page, the SaaS dashboard's unique features. This is where the real work lives, and no boilerplate can do it for you.
- Brand and design — colors, typography, illustrations, motion. TanStack Ship ships a minimal default theme based on Tailwind that you replace in a day.
- Copywriting — landing page, in-app onboarding, email sequences. The boilerplate ships placeholder copy that you rewrite once you know your positioning.
- Chrome Web Store listing assets — screenshots, promo tiles, descriptions, privacy disclosures. None of these ship with code.
For the creator-tools build, I estimated 60% of the 11 days went to product surface and brand, 25% to wiring (which the boilerplate collapses), and 15% to polish and edge cases.
Cost Math: $0 Idle, $0.50/mo at 10k MAU
The hosting bill for the creator-tools extension at 412 paying users (March 2026 invoice):
| Line item | Cost |
|---|---|
| Cloudflare Workers (5M requests/mo) | $0.00 (Free plan) |
| Cloudflare D1 (200MB, 8M reads/mo, 80k writes/mo) | $0.00 |
| Cloudflare KV (1M reads/mo, 50k writes/mo) | $0.00 |
| Cloudflare R2 (2GB extension assets + exports) | $0.03 |
| Cloudflare Durable Objects (400k requests/mo) | $0.00 |
| Resend (transactional email, 3k sent) | $0.00 (Free tier) |
| Stripe | $0 (no monthly fee, 2.9% + 30¢ per txn) |
| Total | $0.03/mo |
Project to 10k MAU: D1 reads scale to ~25M/mo (still $0 on Free plan), Workers requests scale to ~15M/mo (jumps to $5/mo Workers Paid plan), R2 to 5GB ($0.08). Total projected: ~$5/mo hosting at 10k MAU — see the pricing comparison page for the breakdown.
This is the headline result: at low scale, the entire extension + SaaS hosting stack costs effectively zero per month. At 10k MAU it costs the price of two sandwiches. The hosting cost line item is rarely the bottleneck for an extension product — the bottleneck is conversion.
Code: A "Highlight → Save" Flow End-to-End
The fastest way to show how the seven modules wire together is one feature from the creator-tools product: highlight text on any page, save to a personal knowledge base, see it in the SaaS dashboard.
// extension/content-script.ts — runs on every page
document.addEventListener('mouseup', async () => {
const selection = window.getSelection()?.toString().trim();
if (!selection || selection.length < 8) return;
// Ask the service worker to save via the SaaS API
const response = await chrome.runtime.sendMessage({
type: 'SAVE_HIGHLIGHT',
payload: {
text: selection,
url: window.location.href,
title: document.title,
capturedAt: Date.now(),
},
});
if (response.ok) {
showToast('Saved to your knowledge base');
}
});
// extension/service-worker.ts — bridges popup/content to SaaS API
chrome.runtime.onMessage.addListener(async (message, sender) => {
if (message.type === 'SAVE_HIGHLIGHT') {
const session = await chrome.storage.session.get('jwt');
const res = await fetch('https://api.example.com/highlights', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.jwt}`,
},
body: JSON.stringify(message.payload),
});
return { ok: res.ok };
}
});
// app/routes/api/highlights.ts — TanStack Start Server Function
import { createServerFileRoute } from '@tanstack/start/server';
import { db } from '~/lib/db';
import { highlights } from '~/lib/schema';
import { auth } from '~/lib/auth';
export const ServerRoute = createServerFileRoute('/api/highlights').methods({
POST: async ({ request }) => {
const session = await auth(request);
if (!session) return new Response('Unauthorized', { status: 401 });
const body = await request.json();
const id = crypto.randomUUID();
await db.insert(highlights).values({
id,
userId: session.user.id,
text: body.text,
url: body.url,
title: body.title,
capturedAt: body.capturedAt,
});
return Response.json({ id });
},
});
That is one feature spanning the extension, the SaaS API, and the database. Three files. The auth bridge is invisible in the feature code because it lives in a shared lib. The deploy pipeline picks up all three because they ship in one repo. The same auth verifier runs in both the extension's call and the SaaS's call — there is one user table, one session record, one source of truth.
Honest Tradeoffs: Where the Stack Loses
I will not pretend this stack wins every comparison. Three cases where you should pick something else:
- Firefox-first or Safari-first distribution. This post and TanStack Ship's extension template are Chrome MV3-only. Firefox MV3 has parity in 2026, but Safari requires a separate WebExtension port and App Store submission. If your users are 60%+ on Safari, you are signing up for two parallel codebases and two review queues.
- Extensions that need heavy local ML. Chrome's built-in AI (Gemini Nano) is real but constrained; if your core feature is on-device LLM inference, MV3 service workers are the wrong surface. You need a native companion app or a server-side inference path.
- Product shapes that are "browser-first" with no SaaS. If the extension is the entire product and you do not need team, billing, or analytics, the seven-module layout is overkill. A pure extension project on Vite + Plasmo is faster to ship and lighter to maintain.
For a deeper comparison with other SaaS boilerplates, see our TanStack Ship vs Makerkit and TanStack Ship vs Open SaaS breakdowns — both name the cases where those alternatives win.
Decision Tree: Pick This Stack or Something Else
Pick the TanStack Ship companion-product stack if:
- You are shipping a Chrome MV3 extension AND a SaaS dashboard in the same product
- You need real-time sync between extension and SaaS
- Your billing requires Stripe Checkout, customer portal, and webhook idempotency
- You want one deploy pipeline for both surfaces
- You want type-safe end-to-end from popup to API to D1
Pick a pure extension framework (Plasmo, WXT) if:
- The extension is the product; no SaaS component is needed
- You are targeting Firefox or Safari first
Pick ShipFast or Next.js + Vercel if:
- The SaaS is the product; the extension is a marketing surface
- Your team has Next.js expertise and is not switching stacks
- You need Vercel's DX more than Cloudflare's cost model
FAQ
Does TanStack Ship include the Chrome extension scaffold, or just the SaaS side? Both. The starter repo has extension/ (MV3 manifest, content script, service worker, popup UI) and app/ (TanStack Start SaaS) in one monorepo with shared lib/ for types and DB access.
Can I deploy the extension and SaaS separately? Yes. The SaaS deploys to Cloudflare Workers on every main push; the extension publishes to the Chrome Web Store on a tagged release. The GitHub Actions template handles both. Tag-driven deploys avoid the "extension v2.3 published but SaaS still on v1.5" drift that bites teams who release both on every commit.
How long does Chrome Web Store review take? Three days average in my experience, with a 7-day tail for first submissions. Plan your launch window accordingly — see our launch window planning guide for details.
What about MV2 extensions still in the Chrome Web Store? Chrome deprecated MV2 in 2024 and is removing existing MV2 extensions in stages through 2026. Do not start a new product on MV2. Use the MV3 manifest reference as your baseline.
Can I add a Firefox port later? Yes. WXT (the underlying framework TanStack Ship's extension template uses) supports both Chrome MV3 and Firefox MV3 from the same source. The catch is icons, manifest quirks, and the Firefox Add-ons review queue — usually one extra week of work but no source rewrite.
Does the auth bridge work with other auth providers? The shipped bridge is Better Auth-specific. Adapting it to Clerk or Auth.js is straightforward because the JWT format and signing key are the only coupling points — typically a half-day of work.
Closing
A Chrome extension + SaaS companion stack is one product, not two. The seven modules that ship in TanStack Ship are the same seven modules any production build in this category will end up writing — the boilerplate just collapses the wiring. Total build time: 11 days for me. Total idle hosting cost: $0.03/mo. Cold-start p99: 50ms.
If you are starting a companion-product build this quarter, clone TanStack Ship, run pnpm dev, and have the SaaS dashboard rendering before lunch. The seven modules and the deploy pipeline are already wired.