Anton Kopylov

Stackpeek: See What a Shopify Store Is Built With

Every Shopify storefront tells you what it is built with. The theme name sits on window.Shopify.theme. The apps announce themselves through the scripts they inject: a pagefly.io script tag, a window.jdgm global, a Klaviyo domain in a <link>. The information is right there in the page, in plain sight, and completely useless to a human reading it, because knowing that __PF__ means PageFly requires a lookup table nobody carries in their head.

So I built Stackpeek: a Chrome extension that reads those signals and tells you the theme and the app stack of whatever store you are looking at. One click, no signup. It is live in the Chrome Web Store.

What it actually does

You click the toolbar icon on a storefront. A side panel opens, a collector runs in the page, and a moment later the panel names the theme — including paid themes, with the Theme Store listing and price — and lists the apps it recognized.

The collector gathers a deliberately small set of things: window.Shopify and its theme metadata, the third-party domains appearing in <script src> and <link href>, a specific list of window.* globals, and a couple of public store endpoints. Not page content. Not your history. Just the signals that identify software.

The one decision that mattered

Those signals go to an API. The matching happens on my server, not in the extension.

That is the whole architecture, and it was not the obvious choice — bundling the database into the extension is simpler, works offline, and sends nothing anywhere. I went the other way for one reason: Shopify’s app store has around thirteen thousand apps and grows every week. With a bundled database, every new fingerprint means packaging a release, submitting to the Chrome Web Store, waiting for review, and then waiting again for users to auto-update. Days of latency for a one-line data change. Server-side, an edit is live for everyone immediately.

The second reason is less noble but just as real: a client-side database is a file, and a file can be copied. More on that in a moment.

The costs are honest ones. Detection needs a network round-trip, so it is not instant and it does not work offline. And page signals leave your device, which is a privacy obligation I have to actually keep rather than merely disclose. What the design does preserve is the reason to be an extension at all: the observation point stays inside your real browser session. Storefronts sitting behind Cloudflare that return nothing to a server-side crawler render perfectly fine for a logged-in human, and the collector sees exactly what you see.

Where the fingerprints came from

A detector is only as good as its lookup table, and mine started as somebody else’s — several somebodies’.

I unpacked a dozen competing extensions and found their fingerprint databases sitting in the bundle, readable, because they had made the client-side choice. Merging and normalizing those twelve databases produced 2,107 raw entries that deduped down to 759 unique apps, 415 of them corroborated by two or more independent sources. Every entry looks like this:

{ "name": "PageFly", "category": "Page Builder",
  "url_patterns": ["pagefly\\.io", "pagefly-app"],
  "window_globals": ["__PF__"],
  "dom_selectors": ["script[src*='pagefly.io']"],
  "sources": ["adscope", "inspector", "spypro", "unispy"] }

Themes went differently. I started from a competitor’s snapshot, then realized the Shopify Theme Store publishes sitemap_theme_en.xml — so the catalogue is now rebuilt from the source, 329 themes with current prices, by a script I can re-run whenever I want. The competitor’s snapshot survives only as a record of where it started.

That asymmetry is the point. Their databases were extractable because they shipped in the browser. Mine is the thing I keep.

About the permissions

The extension asks to read the page you are on. That is a real ask, and the category it competes in has earned its suspicion — going through those 33 competitors, I found extensions harvesting emails, one exfiltrating browsing history, and one silently rewriting affiliate links.

So the manifest requests activeTab, scripting, sidePanel, storage, and exactly one host: api.stackpeek.app. No <all_urls>, no history, no analytics. Nothing runs until you click the icon — activeTab is granted by that click, and the collector is injected then, on that tab only.

And because “trust me” is not a security model, the extension source is public and MIT licensed at github.com/tonic20/stackpeek-extension. The backend and the fingerprint database stay private; the code that touches your browser does not. If you are going to install something that reads your pages, you should be able to read it first.

The stack, for the curious: TypeScript and Svelte on WXT for the extension, Rails 8 and Postgres for the API.

← Blog