Frameworks
Wire a WXT, CRXJS or Plasmo project, or an extension with a bundler of its own.
Which surfaces your extension has — a popup, a side panel, both or neither — is Surfaces; this page is where the files go in each kind of project.
An extension built with a framework or a bundler uses the same files as a plain one: the client, its config and types, the paywall, and the Wall popup. The wiring puts them where that kind of project keeps them. There is no npm package: the client is a plain ES module.
Choose your extension's folder on the tool's Set up page and npx -y toolaby@latest wire <tool-id> do the same thing:
- Plain extension (
manifest.jsonat the root): the manifest is edited and every file goes beside it. - WXT, CRXJS, Plasmo: these generate the manifest at build time, so the files are written and a snippet for the framework's config is printed. The snippet is never written into your TypeScript.
The snippet carries no values. toolaby.manifest.js, written beside the client, derives the extension's id, the storage permission and the Wall's address from toolaby.config.js. Your config spreads it once; swap in the live key and the manifest follows. (Plasmo's manifest is JSON in package.json, which cannot import, so its snippet carries the values.)
WXT
Nothing to paste. wire writes the client at the project root, the Wall popup under public/ (or src/public/ when your entrypoints are in src/), and modules/toolaby.wxt.ts — a WXT module, which WXT loads from modules/ by itself. At build time it:
- merges what the manifest needs — the extension's
key, thestoragepermission, the Wall's address inhost_permissionsandexternally_connectable— into yours, and puts the Wall popup in front of your popup — and, with anentrypoints/sidepanel.html, the Wall's side panel in front of that, the same way; - starts the platform in your background after your own code has run. A
toolaby.startBackground({ handlers })of yours wins; a background that never mentions the platform still starts it; a project with no background at all gets the platform's own (toolaby.background.js); - puts the account bar at the foot of your popup and your side panel — who is signed in, their plan, their account, sign out — unless
toolaby.config.jssaysaccountBar: falseor the tag is already in your page.
wxt.config.ts and entrypoints/background.ts stay exactly as you had them. npx wxt build and the popup is there.
To gate your own actions, your background calls the platform as anywhere else:
import { toolaby } from '../toolaby.js';
export default defineBackground(() => {
toolaby.startBackground({ handlers: { countWords: async () => { const permit = await toolaby.gate(); if (!permit.allowed) return permit; /* … */ } } });
});WXT builds every entrypoint whether or not the manifest names it, so your popup and side panel stay built and the Wall's open them. npx -y toolaby@latest upgrade <tool-id> replaces the module with the current one along with the other platform files.
A complete example, built in the Wall's CI: wxt-word-count.zip. Unzip, npm install, npx -y toolaby@latest wire <tool-id> --force, npm run build.
CRXJS
With manifest.json at the root, a CRXJS project is wired as a plain one; Vite processes the Wall popup as a page. With manifest.config.ts, add inside defineManifest({ … }):
import { manifest as toolaby } from './toolaby.manifest.js';
...toolaby,
action: { default_popup: 'toolaby-popup.html' },CRXJS builds only the pages the manifest names. Keep your popup built as a Vite input in vite.config.ts — and a side panel the same way: side_panel: { default_path: 'toolaby-sidepanel.html' } in the manifest and sidepanel: 'src/sidepanel/index.html' beside popup in the inputs; the wiring prints both when it finds one:
export default defineConfig({
plugins: [crx({ manifest })],
build: { rollupOptions: { input: { popup: 'src/popup/index.html' } } },
});src/background.ts gets the two lines, the import climbing out of src/: import { toolaby } from '../toolaby.js'; toolaby.startBackground();.
Plasmo
Files at the root; background.ts is created and Plasmo picks it up. package.json gets:
"manifest": {
"key": "<the extension's key, from the snippet>",
"permissions": ["storage"],
"host_permissions": ["https://<workspace>.toolaby.app/*"],
"externally_connectable": { "matches": ["https://<workspace>.toolaby.app/*"] }
}Plasmo builds only the pages its manifest names, and its manifest override replaces action whole, so the Wall popup cannot stand in front of yours. Call the gate in your popup and draw the paywall from paywall.js:
import { toolaby } from "./toolaby.js"
import { toolaby, loadPaywall, applyBrand, renderPaywall, refusalOf } from "./toolaby.js"
useEffect(() => {
;(async () => {
const permit = await toolaby.gate()
if (permit.allowed) return setAllowed(true)
// The tool as the background resolved it from the key: its name, the platform's address, the free limit.
const config = await chrome.runtime.sendMessage({ action: "toolaby.config" })
const paywall = await loadPaywall(config)
applyBrand(paywall)
const user = await toolaby.getUser()
renderPaywall(wall.current!, paywall, user, config, { buy: (plan) => toolaby.openPaymentPage(plan), signIn: () => toolaby.openLoginPage() }, refusalOf(user, permit))
})()
}, [])Other bundlers
Do it myself on the Set up page gives the files as a zip, the manifest keys, and the two worker lines, with your values. The rule for any layout:
- The platform (
toolaby.js— the client and the popup UI as one file — withtoolaby.config.jsandtoolaby.d.ts) goes where your build imports from. - The Wall popup (
toolaby-popup.htmland its two-linetoolaby-popup.js) — and, with a side panel,toolaby-sidepanel.htmlandtoolaby-sidepanel.js— reaches the output withtoolaby.jsandtoolaby.config.jsbeside it: copied verbatim, or built as a page of the bundler's. - The manifest gets the
storagepermission, a host permission andexternally_connectablefor your workspace's address, and the Wall popup asaction.default_popup— the Wall's side panel asside_panel.default_path, with thesidePanelpermission (orsidebar_action.default_panel, in Firefox's spelling). - A page of your own that draws the panel in a side panel sets
data-toolaby-surface="sidepanel"on<html>, soensureStyles()does not size it as a 336px popup.
Firefox is not supported yet. It has no externally_connectable, so the sign-in and purchase handoffs need the client's bridge content script (toolaby-bridge.js) on your workspace's pages, and the wiring does not add it.