ComparEdge Advisor Extension
Technical Reference Manual
Comprehensive technical specification covering architecture, APIs, security model, storage schema, and runtime behaviour of the ComparEdge Advisor browser extension.
01Abstract
ComparEdge Advisor is a Manifest V3 Chrome extension that passively identifies B2B SaaS and AI tools as the user browses the web, and delivers contextual product intelligence -- pricing, ratings, pros/cons, and competitor alternatives -- without requiring any user action or account creation.
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.
Zero tracking. No telemetry. No remote code execution. No eval(). All logic and product data is bundled locally. The only external network request is a voluntary 48-hour data sync with the ComparEdge API -- which is read-only and contains no user-identifiable information.
02Specifications
03System Architecture
The extension is composed of four primary components, each with a distinct execution context and responsibility boundary:
data.js via importScripts(). Handles all chrome.runtime.onMessage events from content scripts and 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 (508 entries), domainMap (464 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", "scripting", "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 48h |
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 508 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 owl icon + rating chip |
| Card | .ce-card | 320px panel; max-height 80vh; flex-column |
| Header | .ce-card-header | Drag handle; product name + rating; minimize + close buttons |
| Body | .ce-card-body | Scrollable content area; stars, price, description, pros/cons, alternatives |
| Footer | inline | CTA buttons: "Compare" and "Full Review" |
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 maintains a voluntary 48-hour data refresh cycle to ensure product data stays current without requiring an extension update. The sync endpoint returns a JSON payload structurally identical to CE_DATA.
| Property | Value |
|---|---|
| Endpoint | https://comparedge.com/extension-sync.json |
| Method | GET |
| Frequency | Every 48 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 collects zero user data. There are no analytics calls, no telemetry, no user identifiers, and no event tracking of any kind. The complete list of external network requests made by the extension:
GET https://comparedge.com/extension-sync.json-- once every 48 hours, contains no user data, purely reads public product data
All other operations (domain matching, card rendering, settings, dismiss tracking) are entirely local to the browser.
XSS Mitigation
All string values from the remote sync payload (product names, descriptions, pros/cons) are HTML-encoded via a sanitize function before insertion into 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_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 | Automatically open card (vs show only badge) |
showBadge | boolean | true | Show floating badge when card is minimized |
position | 'bottom-right' | 'bottom-left' | 'bottom-right' | Default card anchor position |
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 | Aggregate rating (0-5, typically 2 decimal places) |
p | number | null | Starting price per month (USD) |
f | boolean | Has free plan |
cat | string | Category slug (e.g. "crm", "ai-tools") |
d | string | Short description (1-2 sentences) |
pr | string[] | Pros list (3-5 items) |
co | string[] | Cons list (2-4 items) |
Alternative entries in CE_DATA.alternatives[slug]:
| Field | Type | Description |
|---|---|---|
s | string | Slug of the alternative product |
n | string | Display name |
r | number | Rating |
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 | ~180 KB | Compressed; fetched once per 48h |
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.