Manifest V3 + GitHub Actions CI/CD + Chrome & Firefox deploy.
Build your extension. Push to deploy.
English | νκ΅μ΄
Part of Starter Series β Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy Β· Discord Bot Β· Telegram Bot Β· Browser Extension Β· Electron App Β· npm Package Β· React Native Β· VS Code Extension Β· MCP Server Β· Python MCP Server Β· Cloudflare Pages
- Currently implemented β MV3 manifest (Chrome + Firefox), CI (validate Β· permission audit Β·
npm auditΒ· lint Β· test Β· build Β· built-zip extension smoke), CD (Chrome Web Store + Firefox Add-ons + GitHub Release), CodeQL workflow, end-to-endchrome.storage.syncsettings example (options page β content script β background) with Jest behavioral coverage for settings, popup, options, content, and background flows, Node-version lockstep test across.nvmrc+ workflow YAMLs, version-bump scripts, live-reload viaweb-ext, privacy-policy template, package metadata that dry-runs cleanly withnpm pack --dry-run --json, and a one-command store-asset generator (npm run capture:store) that stages the extension with Playwright to produce CWS screenshots + promo tile + demo screencast and extract listing copy fromstore-assets/STORE_LISTING.md. - Planned β none on a public roadmap. This is a starter, not a product; features land when a downstream extension needs them.
- Design intent β Zero build step, vanilla JS, raw browser APIs. The point is to ship a working extension on day one and let an LLM read the code without first learning a framework. Coverage gates are baseline-aware (anchored to the current baseline, not aspirational) β they catch regressions, not author shame.
- Non-goals β Bundling (Vite/Parcel/webpack), TypeScript by default, UI frameworks (React/Vue/Svelte), single-page-app routing, opinionated state libraries. Those are real needs β they belong in WXT or Plasmo. See the comparison table below.
- Redacted β none. Template ships no private data, no embedded credentials, no third-party identifiers.
Via create-starter (recommended):
gh repo create my-extension --template starter-series/browser-extension-starter --clone
cd my-extension && npm install && npm run devOr clone directly:
git clone https://github.com/starter-series/browser-extension-starter my-extension
cd my-extension && npm install && npm run devBefore adapting the template, verify the repo surface:
npm test
npm run lint
npm run lint:css
npm run build:chrome
npm run smoke:extension
npm audit --audit-level=high
npm pack --dry-run --jsonManual setup (step-by-step)
# 1. Click "Use this template" on GitHub (or clone)
git clone https://github.com/starter-series/browser-extension-starter.git my-extension
cd my-extension
# 2. Install dependencies
npm install
# 3. Load in Chrome
# β chrome://extensions β Enable Developer Mode β Load unpacked β select project root
# 4. Build zip for store
npm run build:chromeβββ manifest.json # MV3 manifest (Chrome + Firefox)
βββ src/
β βββ popup/ # Extension popup (HTML + JS)
β βββ options/ # Options page (HTML + JS)
β βββ background/ # Service worker
β βββ content/ # Content script (JS + CSS)
βββ assets/icons/ # Extension icons (16/32/48/128)
βββ shotkit.config.js # Store-asset scenes (consumed by shotkit)
βββ store-assets/ # Listing copy + fixtures/templates (outputs gitignored)
βββ .github/
β βββ workflows/
β β βββ ci.yml # Validate, audit, lint, test, build
β β βββ cd.yml # Deploy to Chrome Web Store
β β βββ cd-firefox.yml # Deploy to Firefox Add-ons
β β βββ setup.yml # Auto setup checklist on first use
β βββ PULL_REQUEST_TEMPLATE.md
βββ docs/
β βββ CWS_SETUP.md # Chrome Web Store publishing guide
β βββ AMO_SETUP.md # Firefox Add-ons publishing guide
β βββ PRIVACY_POLICY_TEMPLATE.md # Privacy policy template for store
βββ package.json
- Manifest V3 β Chrome + Firefox cross-browser support
- CI Pipeline β Manifest validation, permission audit, security audit, lint, test, build verification
- CD Pipeline β One-click deploy to Chrome Web Store or Firefox Add-ons + auto GitHub Release
- Version management β
npm run version:patch/minor/majorto bumpmanifest.json - Security β CI warns on risky permissions, broad host access, and dependency vulnerabilities
- Dev mode β
npm run devfor live-reload withweb-ext - Starter code β Popup with toggle + options page + background + content script
- Settings storage example β
chrome.storage.syncend-to-end: options form β content script β live updates - Store-ready β OAuth setup guide + privacy policy template
- Store-asset generator β
npm run capture:storecaptures CWS screenshots, a promo tile, and a demo screencast from staged extension files via Playwright (no manual screenshotting) - Template setup β Auto-creates setup checklist issue on first use
| Step | What it does |
|---|---|
| Validate manifest | Checks manifest.json is valid JSON with required fields + version format |
| Audit permissions | Warns on risky permissions and broad host access |
| Security audit | npm audit for dependency vulnerabilities |
| Lint | ESLint (JS) + Stylelint (CSS) |
| Test | Jest behavioral tests for settings, popup, options, content, and background flows |
| Build verification | Builds zip and verifies required entries plus size |
| Extension smoke | Extracts dist/extension.zip into a temp directory, loads that built artifact in Chromium, and checks install defaults β popup β options β content-script behavior |
| Workflow | What it does |
|---|---|
CodeQL (codeql.yml) |
Static analysis for security vulnerabilities (push/PR + weekly) |
Maintenance (maintenance.yml) |
Weekly CI health check β auto-creates issue on failure |
Stale (stale.yml) |
Labels inactive issues/PRs after 30 days, auto-closes after 7 more |
| Step | What it does |
|---|---|
| Version guard | Fails if git tag already exists for this version |
| Build | Zips manifest + src + assets |
| Upload | Publishes to Chrome Web Store via API |
| GitHub Release | Creates a tagged release with zip attached |
| Artifact | Saves zip as GitHub Actions artifact |
How to deploy:
- Set up GitHub Secrets (see below)
- Bump version:
npm run version:patch(orversion:minor/version:major) - Go to Actions tab β Deploy to Chrome Web Store β Run workflow
- Choose publish target (
defaultortrustedTesters) β Run
| Secret | Description |
|---|---|
CWS_EXTENSION_ID |
Your Chrome Web Store extension ID |
CWS_CLIENT_ID |
Google OAuth2 client ID |
CWS_CLIENT_SECRET |
Google OAuth2 client secret |
CWS_REFRESH_TOKEN |
Google OAuth2 refresh token |
See docs/CWS_SETUP.md for a detailed setup guide.
| Secret | Description |
|---|---|
AMO_JWT_ISSUER |
AMO API key (JWT issuer) |
AMO_JWT_SECRET |
AMO API secret |
See docs/AMO_SETUP.md for a detailed setup guide.
# Live-reload in Chromium
npm run dev
# Bump version (updates manifest.json)
npm run version:patch # 1.0.0 β 1.0.1
npm run version:minor # 1.0.0 β 1.1.0
npm run version:major # 1.0.0 β 2.0.0
# Build store zip
npm run build:chrome
# Load the built zip artifact in Chromium and smoke install/defaults/popup/options/content paths
npm run smoke:extension
# Lint & test
npm run lint # JS
npm run lint:css # CSS
npm test
# Release/package checks
npm audit --audit-level=high
npm pack --dry-run --jsonThe npm package metadata is present so npm pack --dry-run --json can be used as
a template-boundary check. The tarball allowlist includes the files a downstream
project needs to keep the starter useful: extension runtime files, docs, version
scripts, GitHub workflows, and the tracked store-asset sources
(STORE_LISTING.md, fixtures, and promo templates).
Generated files stay outside that boundary. dist/, coverage/, screenshots,
demo videos, and store-assets/description.md are build or capture outputs and
are ignored by git.
The template ships with a small chrome.storage.sync example that wires an options page, a content script, and a background service worker together. It's the canonical answer to "how do I store settings and read them from a content script?"
Where to look:
| File | Role |
|---|---|
src/settings.js |
Shared UMD module: DEFAULTS, getSettings(), validators. Same code runs in the options page, content script, and tests. |
src/options/options.html + options.js + options.css |
Full options page with 3 settings (boolean toggle, hex color, newline-separated blocked-domains list), hex/domain validation, and an aria-live status region. |
src/content/content.js |
Loads settings, bails out if disabled or the host is blocklisted, and reacts live to chrome.storage.onChanged. |
src/background/background.js |
On onInstalled (reason install), seeds defaults into chrome.storage.sync only for keys the user hasn't already set. |
tests/settings.test.js |
Jest coverage for defaults, stored values, invalid-value coercion, and host-match edge cases. |
Open the Options page in a tab: options_ui.open_in_tab is set to true, so Chrome opens it as a full tab (right-click extension icon β Options, or chrome://extensions β Details β Extension options).
Why sync and not local? chrome.storage.sync roams the user's preferences with their Google/Firefox profile. Use local for caches or device-specific state, session for values that die with the browser session.
Firefox compatibility. Modern Firefox (109+, which this template targets via browser_specific_settings.gecko.strict_min_version) exposes chrome.storage.sync directly β no webextension-polyfill needed. Add the polyfill only if you need promise-returning APIs without callbacks, or if you must support Firefox < 109.
Producing Chrome Web Store assets by hand β five screenshots, a promo tile, a demo clip, the listing copy β is the chore that quietly delays releases. This template generates them from one command:
npm run capture:install # one-time: download the Playwright Chromium
npm run capture:store # produce assets into store-assets/Outputs land in store-assets/: one PNG per scene (1280Γ800), a promo tile
(440Γ280), demo.webm, and description.md (listing copy extracted from
STORE_LISTING.md). Flags: --scene <name> (capture just one), --no-video,
--live-gt, --freeze (see shotkit.config.js).
How it works. shotkit.config.js is the seam. The shotkit engine
(shotkit) owns build β launch β screenshot β caption β promo β
video β description; your shotkit.config.js owns the project-specific parts: which
extension dir to load, an optional setup() (e.g. a fixture HTTP server), and the
scenes that drive the extension into each money-shot state. A scene is just:
The capture:store script resolves shotkit from the public GitHub source
package until the unscoped npm package is published, so a fresh template clone
does not depend on a scoped npm package or an unpublished registry name.
{ name: '01-feature', caption: 'What this shows',
async run({ page, context, extensionId, env }) {
await page.goto(`${env.baseUrl}/some-page`);
// β¦drive the UI, wait until it has renderedβ¦
} }The shipped scenes are a working demo against this starter's own highlighter; swap in scenes for your own extension once the scaffold is copied.
- Design intent β Playwright loads a staged unpacked extension via
launchPersistentContext(--load-extension)and waits for content to render before capturing a fixed viewport. That removes the load-vs-capture race that desktop screenshotters hit (half-fetched UI). The separatenpm run smoke:extensioncommand is the shipped-bundle gate: it extractsdist/extension.zipand drives the installed extension artifact. Captures are deterministic (login-free fixtures, frozen translations/data) so they're reproducible in CI. - Trademark-safety β shotkit composites a configurable disclaimer band
onto every screenshot and the promo tile (
disclaimerinshotkit.config.js), so a "not affiliated" line can't be forgotten when an extension interoperates with a third-party brand. - Non-goals β this is a clean automatic screencast and a tidy promo graphic, not a voiceover ad or agency-grade artwork. It captures real UI; it does not embellish it.
Capture runs a real Chromium β headed by default locally (
HEADED=0runs headless; verified). It can also run entirely in CI: Actions β "Capture store assets" β Run workflow regenerates everything and uploads astore-assetsartifact. Usenpm run smoke:extensionfor the built-zip smoke gate.
- Edit
manifest.jsonβ name, description, permissions, match patterns - Replace icons in
assets/icons/ - Build your popup in
src/popup/ - Configure settings in
src/options/ - Add background logic in
src/background/ - Add page injection in
src/content/ - Copy
docs/PRIVACY_POLICY_TEMPLATE.mdand fill in your details
Note: The default content script is scoped to
https://example.com/*as a safe placeholder. Replace it with the sites your extension truly needs. Broad host access is kept inoptional_host_permissionsfor user-granted expansion; Chrome Web Store review is stricter whencontent_scripts.matchesstarts broad.
WXT and Plasmo are excellent frameworks that abstract away browser extension internals. This template takes a fundamentally different approach:
| This template | WXT / Plasmo | |
|---|---|---|
| Philosophy | Thin starter with CI/CD | Full framework with runtime |
| Build system | None (raw files) | Vite / Parcel (required) |
| Learning curve | Read the browser APIs directly | Learn the framework's abstractions |
| CI/CD | Full pipeline included | Not included |
| Dependencies | dev-only toolchain, 0 runtime | 100+ |
| AI/vibe-coding | LLMs generate clean vanilla JS | LLMs must understand framework conventions |
| Best for | Utility extensions, scripts, simple tools | Complex apps with multi-page UIs |
Choose this template if:
- You want to understand what your extension actually does, line by line
- You need production CI/CD out of the box (no other template provides this)
- You're using AI tools to generate extension code β vanilla JS produces the cleanest AI output
- Your extension is a utility, not a full application
Choose WXT/Plasmo if:
- You need React/Vue/Svelte components in your extension UI
- You want file-based routing and auto-imports
- Your extension has complex multi-page architecture
This template intentionally uses vanilla JavaScript to keep the zero-build-step philosophy. If you need TypeScript:
- Add
typescriptto devDependencies - Add a
tsconfig.json - Add a
tscbuild step topackage.json - Rename
.jsfiles to.ts
This keeps TypeScript opt-in rather than forcing a build pipeline on everyone. For many extensions (content scripts, simple popups, background listeners), vanilla JS is all you need.
PRs welcome. Please use the PR template.