"Unpacked is for you, packed is for everyone else. Lesson 2 is the difference between the developer-mode 'Load unpacked' loop you've been using all quest and the signed .crx that goes to other machines."
Unpacked
The whole quest, you've been working unpacked: a folder on disk loaded via chrome://extensions → Developer mode → Load unpacked. The extension reads its files live from the directory; editing content.js + clicking reload is the entire dev loop. No bundling required, no signing, no upload anywhere.
Unpacked extensions get a generated extension ID per Chrome install — different on each machine. That's fine for development; it becomes a problem if you ship something that hardcodes a specific extension ID (some Native Messaging hosts do this).
Packed
A packed extension is a .crx file: a ZIP of the extension contents plus a signature (RSA-signed by a key you control or by Chrome itself if you publish through the Web Store). The .crx is what installs onto someone else's Chrome.
Two ways to produce one:
- chrome://extensions → Pack extension — local tool. Asks for the extension directory and (optionally) a .pem key file. First run produces both the .crx and a fresh .pem; keep the .pem somewhere safe so future updates sign with the same key (Chrome treats same-key as same extension for updates).
- Chrome Web Store — upload the ZIP of the extension directory, Chrome signs it, the .crx lives in the store. This is the path 99% of public extensions take.
The Same-Key Update Contract
The .pem (private key) controls the extension's identity. As long as you sign each update with the same .pem, Chrome treats the new .crx as an upgrade of the existing extension — users get the new version, their storage persists, the extension ID stays the same. Lose the key and you've effectively orphaned that extension; you'd have to publish a new extension under a new ID and migrate users.
Web Store-published extensions don't need a local .pem because Google manages the key. The extension ID is generated at first publish and never changes.
The Dev Workflow That Survives Distribution
Recommendation: develop unpacked, publish through the Web Store as the primary distribution path. The hybrid:
- One git repo, one source-of-truth manifest.json with the production permissions.
- Local dev:
npm run buildoutputs todist/, chrome://extensions points 'Load unpacked' atdist/. - To ship:
npm run build && cd dist && zip -r ../clipdeck.zip ., upload the zip to the Web Store developer dashboard. - The Web Store handles signing, hosting, and auto-updates; users install with one click.
Sideloading and Why It's Hard
Chrome heavily restricts installing .crx files from outside the Web Store. Desktop Chrome:
- Drag-and-drop install on chrome://extensions used to work; now restricted to extensions installed via enterprise policy or developer-mode unpacked.
- Direct .crx URL → Chrome prompts "This extension can't be added from this website" or similar.
- Group policy install (Chrome Enterprise) is the supported path for sideloaded production extensions.
Workaround for dev teams that don't have Chrome Enterprise: publish unlisted to the Web Store. Each person installs from the unlisted URL; the rest is normal Web Store flow.
The Unlisted Loophole
'Unlisted' on the Web Store means the extension exists but doesn't appear in search or category browsing. People can install if you give them the URL. The submission flow is the same as public (privacy policy, review, the works), but the marketing surface is invisible. This is the right default for private-team extensions that don't justify the Chrome Enterprise overhead.