ComparEdge Copilot Extension
Technical Reference Manual
Comprehensive technical specification covering architecture, APIs, security model, storage schema, and runtime behaviour of the ComparEdge Copilot browser extension.
01Abstract
ComparEdge Copilot is a Manifest V3 Chrome extension that recognises a SaaS or AI tool by its domain as you browse, and surfaces what the vendor's own page tends to bury: the real starting price, the annual-billing discount, one sharp hidden-cost line, key features, and cheaper or pricier alternatives. No account, no sign-in.
The extension operates on a static content script + pull model: the content script is declared in the manifest and automatically injected by Chrome into every page. Upon page load, the content script queries the background service worker for product data associated with the current domain. If a match exists and the product has not been dismissed by the user, an interactive card is rendered inside an isolated Shadow DOM container.
No in-browser tracking, no telemetry beacons, no remote code, no eval(). Logic and product data ship inside the package. The extension makes one automatic request: a read-only data refresh every 24 hours. Links you click through carry a campaign tag so the site can attribute the referral, the way any shared link does. No user identifier, no browsing history.
02Specifications
03System Architecture
The extension is composed of four primary components, each with a distinct execution context and responsibility boundary:
data.js via importScripts() and answers every chrome.runtime.onMessage from content scripts and the popup.document_end into every page. Immediately sends CE_READY to background on load. Renders all UI inside an isolated Shadow DOM. Has zero access to page JavaScript context.data.js loaded via <script> tag for direct data access.CE_DATA global object. Contains products map (490+ entries), domainMap (457 eTLD+1 to slug mappings), and alternatives map. Used via importScripts in background and <script> in popup.Manifest & Permissions
// manifest.json -- abbreviated
{
"manifest_version": 3,
"permissions": ["activeTab", "storage", "tabs", "alarms"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" },
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}]
}Service Worker
The service worker (background.js) is the central coordinator. It handles three classes of events:
| Event | Handler | Behaviour |
|---|---|---|
onUpdated (complete) | handleTab() | Updates toolbar badge when tab finishes loading |
onActivated | handleTab() | Updates badge when user switches tabs |
alarms.onAlarm | sync handler | Fetches fresh data from sync endpoint every 24h |
onMessage (CE_READY) | data delivery | Returns product data to content script on page load |
onMessage (CE_RECHECK) | restore flow | Sends CE_INIT to tab after user restores dismissed product |
onInstalled | init | Clears legacy injection cache, triggers immediate sync |
Content Script
The content script follows a pull model: instead of waiting for the background to push data, it requests data upon initialisation. This eliminates all timing-dependent race conditions inherent to the previous push-injection model.
// content.js -- initialisation sequence
(function() {
// 1. Register message listener (CE_INIT + CE_THEME)
chrome.runtime.onMessage.addListener((msg) => { /* ... */ });
// 2. Pull model -- request data from background
chrome.runtime.sendMessage({ type: 'CE_READY' }, (response) => {
if (chrome.runtime.lastError) return;
if (response?.product) initCard(response.slug, response.product, response.alts);
});
})();Popup Interface
The popup renders four tab panels. Tab state is ephemeral (resets on popup close). Product data is accessed directly from CE_DATA for the Search tab, avoiding async IPC overhead.
| Tab | Data Source | Description |
|---|---|---|
| This Page | background via chrome.tabs.query | Shows product card for the active tab URL |
| Search | CE_DATA direct | Real-time fuzzy search across all 490+ products |
| Dismissed | chrome.storage.local | Lists all dismissed products with restore action |
| Settings | chrome.storage.local | Theme, auto-show, badge visibility, card position |
Data Layer
data.js is the sole source of product intelligence in the bundled extension. It defines the CE_DATA global on the globalThis / window scope depending on execution context.
// CE_DATA structure
const CE_DATA = {
products: {
"shopify": { n: "Shopify", r: 4.5, p: 25, f: false, cat: "website-builders",
d: "...", pr: ["..."], co: ["..."] },
// 507 more entries...
},
domainMap: {
"shopify.com": "shopify",
"notion.so": "notion",
// 462 more entries...
},
alternatives: {
"shopify": [{ s: "wix", n: "Wix", r: 4.2 }, /* ... */]
}
};04Domain Matching Algorithm
The domain resolution algorithm performs eTLD+1 extraction with subdomain walk-up, ensuring that app.notion.so, www.notion.so, and notion.so all correctly resolve to the notion product slug.
function extractBaseDomain(hostname) {
hostname = hostname.replace(/^www\./, '');
// Try exact match first
if (CE_DATA.domainMap[hostname]) return hostname;
// Walk up subdomains: app.hubspot.com -> hubspot.com -> com
const parts = hostname.split('.');
for (let i = 1; i < parts.length - 1; i++) {
const candidate = parts.slice(i).join('.');
if (CE_DATA.domainMap[candidate]) return candidate;
}
return null;
}Resolution Steps
- Strip
www.prefix from hostname - Check exact match in
domainMap - Walk up subdomain hierarchy (e.g.
app.hubspot.comtohubspot.com) - Return
nullif no match found (no card rendered, badge cleared)
05Injection Model
Version 1.0 adopts a static content script declaration model, superseding the previous dynamic injection approach (chrome.scripting.executeScript) which suffered from injection cache invalidation bugs and race conditions.
Dynamic injection required managing an injection cache (ce_injected in storage) to prevent double-injection. This cache became stale after extension updates, page reloads, and tab switches, causing silent injection failures. Static declaration removes this complexity entirely -- Chrome handles lifecycle management natively.
06Card UI & Shadow DOM
The card UI is rendered inside a Shadow DOM attached to a custom host element (id="comparedge-advisor-root") appended to document.body. This provides complete style isolation -- page CSS cannot leak in, and extension CSS cannot leak out.
// Shadow DOM initialisation
const host = document.createElement('div');
host.id = 'comparedge-advisor-root';
const shadow = host.attachShadow({ mode: 'closed' });
// CSS variables defined on #ceWrapper (not :host -- avoids all:initial conflict)
shadow.innerHTML = `
<style>${getStyles()}</style>
<div id="ceWrapper">
<div class="ce-badge" id="ceBadge">...</div>
<div class="ce-card" id="ceCard">...</div>
</div>
`;
document.body.appendChild(host);Card Structure
| Element | Class | Description |
|---|---|---|
| Wrapper | #ceWrapper | CSS variable scope; .ce-light class for theme |
| Badge | .ce-badge | Floating 44x44px button; shows the ComparEdge icon + rating chip |
| Card | .ce-card | 320px panel; max-height 80vh; flex-column |
| Header | .ce-card-header | Drag handle; product logo + name + rating; minimize + close buttons |
| Body | .ce-card-body | Scrollable: stars, price, annual-discount chip, hidden-cost hook, key features, alternatives (click to preview) |
| Footer | inline | CTAs: "See all plans & prices", "Compare alternatives", cost calculator |
Drag & Persist
The card is freely draggable via mousedown/mousemove/mouseup listeners on the header. Position is persisted to chrome.storage.local under the ce_position key. On card initialisation, saved position is validated against the current viewport dimensions -- if out-of-bounds, the position is cleared and the card resets to its default bottom-right anchor.
// Bounds validation on position restore
const left = parseFloat(pos.left);
const top = parseFloat(pos.top);
const maxLeft = window.innerWidth - 340;
const maxTop = window.innerHeight - 80;
if (left >= 0 && left <= maxLeft && top >= 0 && top <= maxTop) {
host.style.left = pos.left; host.style.top = pos.top;
} else {
chrome.storage.local.remove('ce_position'); // reset
}07Card Lifecycle
States
| State | Trigger | DOM Effect | Storage |
|---|---|---|---|
| Visible (open) | initCard() or badge click | .ce-card.ce-open added | - |
| Minimized | minimize button click | card closes, badge shows | - |
| Dismissed (30d) | close button click | host.remove() | ce_dismissed[slug] = Date.now() |
| Restored | Popup - Dismissed - Restore | popup sends CE_INIT to tab | delete ce_dismissed[slug] |
Dismiss Flow
Restore Flow
08Theme System
The extension supports three theme modes: dark (default), light, and auto (follows prefers-color-scheme). Theme state is applied to two distinct scopes:
| Scope | Mechanism | Class Toggle |
|---|---|---|
| Popup | document.documentElement.classList | theme-dark / theme-light on <html> |
| Content card | shadow.getElementById('ceWrapper').classList | ce-light on #ceWrapper |
Real-time Theme Propagation
When the user changes theme in the popup Settings tab, applyTheme() updates the popup DOM and simultaneously sends a CE_THEME message to the active tab content script, which then calls applyCardTheme(). This ensures both surfaces update without requiring a page reload.
// popup.js -- theme change handler
function applyTheme(theme) {
document.documentElement.classList.remove('theme-dark', 'theme-light');
const isDark = theme === 'dark' || (theme === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.add(isDark ? 'theme-dark' : 'theme-light');
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
if (tabs[0]?.id)
chrome.tabs.sendMessage(tabs[0].id, { type: 'CE_THEME', theme }).catch(() => {});
});
}09Remote Sync
The extension refreshes its catalog every 24 hours so pricing stays current without shipping a new version. The endpoint returns the same payload shape as the bundled CE_DATA, built by the same server module (extensionPayload.js) that generates the bundled file, so the two can never drift.
| Property | Value |
|---|---|
| Endpoint | https://comparedge.com/extension-sync.json |
| Method | GET |
| Frequency | Every 24 hours via chrome.alarms |
| Trigger | Also fires immediately on onInstalled |
| Storage key | ce_remote_data + ce_sync_time |
| Fallback | Bundled CE_DATA used if sync fails or stale |
When serving a CE_READY request, getProductData(slug) checks ce_remote_data first. If the remote data contains an entry for the slug, it takes precedence over the bundled data. This allows product information to be updated without publishing a new extension version.
10Security Model
Privacy Architecture
The extension stores no personal data and sends no telemetry from your browser. Domain matching, card rendering, settings and dismissals all run locally. It makes exactly one automatic network request:
GET https://comparedge.com/extension-sync.jsonโ a read-only data refresh every 24 hours. It fetches public product data and sends nothing about you.
When you click through to comparedge.com, the link carries ?utm_source=comparedge-copilot. The site records that referral โ which product, which surface, and the country resolved at the edge โ in its extension_events table, so we can see what the extension is actually used for. This is ordinary referral attribution, the same as any campaign link: no user identifier, no cross-site profile, and nothing about your browsing leaves the machine on its own.
XSS Mitigation
Every string from the data payload (product names, descriptions, features, hooks) is HTML-encoded by a sanitize function before it touches innerHTML:
function s(str) {
if (typeof str !== 'string') return '';
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}This ensures that even a compromised sync endpoint cannot inject executable HTML or JavaScript into the card UI.
Content Security Policy
The extension enforces a strict CSP for extension pages:
"script-src 'self'; object-src 'self'"This prohibits inline scripts, eval(), remote script loading, and all plugin objects within the popup and options pages. Content scripts run in an isolated world and are not subject to the page own CSP.
Shadow DOM Isolation
The card UI uses a closed Shadow DOM (attachShadow({ mode: 'closed' })). This means:
- Page JavaScript cannot access the shadow root via
element.shadowRoot - Page CSS selectors cannot target elements inside the shadow tree
- Extension CSS cannot inadvertently style page elements
- User scripts and page-level XSS cannot tamper with the card UI
11Storage Schema
All persistent state is stored in chrome.storage.local. The following keys are used:
| Key | Type | Description | Lifetime |
|---|---|---|---|
ce_dismissed | Record<slug, timestamp> | Products dismissed by user; values are Date.now() timestamps. TTL = 30 days. | 30 days per entry |
ce_settings | { theme, autoShow, showBadge, position } | User preferences from Settings tab | Permanent |
ce_position | { left: string, top: string } | Last saved card drag position (CSS pixel values) | Until reset |
ce_remote_data | CE_DATA shape | Remote sync payload; overrides bundled data per-slug | Until next sync |
ce_sync_time | number | Unix timestamp of last successful sync | Permanent |
12Message API
| Message Type | Direction | Payload | Description |
|---|---|---|---|
CE_READY | content - background | - | Content script requests product data for current page. Background responds with { slug, product, alts } or null. |
CE_PRODUCT | content/popup - background | { slug } | Requests any product by slug so the card and popup can preview an alternative in place. Background responds with { slug, product, alts }. |
CE_INIT | background/popup - content | { slug, product, alts } | Instructs content script to render the product card. Used in restore flow. |
CE_THEME | popup - content | { theme: 'dark'|'light'|'auto' } | Propagates theme change to card in real time. |
CE_RECHECK | popup - background | { tabId, url } | Triggers background to send CE_INIT to the specified tab after restore. |
13Settings API
| Key | Values | Default | Description |
|---|---|---|---|
theme | 'dark' | 'light' | 'auto' | 'dark' | UI colour scheme for popup and content card |
autoShow | boolean | true | Open the card automatically on a match (vs. showing only the badge) |
showBadge | boolean | true | Show the rating in the toolbar icon badge |
position | 'br' | 'bl' | 'tr' | 'tl' | 'br' | Default card corner: bottom/top + right/left |
showAlts | boolean | true | Show the alternatives block in the card |
showPros | boolean | true | Show the key-features block in the card |
14Product Data Schema
Each product entry in CE_DATA.products conforms to the following shape:
| Field | Type | Description |
|---|---|---|
n | string | Product display name |
r | number | ComparEdge rating shown to users (0-5, one decimal) |
c | string | Category slug (e.g. "crm", "llm") |
d | string | Short description |
f | boolean | Has a free plan |
p | number | null | Lowest paid price; used only to compare cheaper/pricier |
price | string | Display price label: "$12/mo", "$0.15/1M tokens", "free tier" or "custom pricing" |
save | number? | Max annual-billing discount as a percent (omitted when none) |
hook | string? | One curated real-cost line: a hidden fee, seat minimum, or renewal jump |
pr | string[] | Up to 3 concrete features (from the product feature set, not review-platform blurbs) |
ver | string? | Verification month, e.g. "July 2026" |
calc | 'llm'? | Marks LLM tools so the card links the token calculator |
Alternative entries in CE_DATA.alternatives[slug]:
| Field | Type | Description |
|---|---|---|
s | string | Slug of the alternative product |
n | string | Display name |
r | number | ComparEdge rating |
price | string | Display price label |
pn | number | null | Numeric floor, used for the cheaper/pricier badge |
15Performance
| Metric | Value | Notes |
|---|---|---|
| Extension package size | ~1.5 MB | Dominated by 533 WebP logos (32x32) |
| Content script execution | <5ms | IIFE, single async message round-trip |
| CE_READY response time | <10ms | Storage read + object lookup |
| Card first render | <20ms | After CE_READY response received |
| DOM overhead per page | 1 element | #comparedge-advisor-root + shadow tree (only on matched pages) |
| Memory (non-matched pages) | ~12 KB | content.js parse + one pending message + listener |
| Sync payload size | ~300 KB | JSON, gzip on the wire; fetched once per 24h |
16Changelog
All version history, release notes, and architectural changes are tracked in the extension changelog. Each entry documents new features, bug fixes, security updates, and data layer changes with exact dates and version numbers.
The changelog follows Semantic Versioning: MAJOR.MINOR.PATCH. Breaking changes increment MAJOR, new features increment MINOR, bug fixes increment PATCH.