Toolaby

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, toolaby.js, which carries the paywall; its config and types; and the Toolaby popup. The wiring puts them where that kind of project keeps them. The client is not an npm package: toolaby.js is one ES module file, written into your project.

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.json at the root): the manifest is edited and every file goes beside it.
  • WXT: the files and a WXT module, modules/toolaby.wxt.ts, which WXT loads by itself. Nothing to paste.
  • CRXJS with manifest.config.ts, and Plasmo: these generate the manifest at build time, so the files are written and the lines for the framework's config are printed. They are never written into your TypeScript.

The printed lines carry no values for CRXJS. toolaby.manifest.js, written beside the client, derives the extension's id, the storage permission and Toolaby'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 lines carry the values.

WXT

Nothing to paste. wire writes the client with its config and types, the Toolaby popup and toolaby.background.js at the project root, 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, the storage permission, Toolaby's address under externally_connectable — into yours, takes out a host permission for Toolaby's address, which it no longer needs, and puts the Toolaby popup in front of your popup — and, with an entrypoints/sidepanel.html, Toolaby'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.js says accountBar: false or the tag is already in your page;
  • copies the Toolaby popup, its side panel, toolaby.js and toolaby.config.js into the build output as they are, so each page finds the client beside it.

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 Toolaby'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 Toolaby's CI: wxt-word-count.zip. Unzip, npm install, npx -y toolaby@latest wire <tool-id> --force, npm run build. For a new extension, the WXT starter is a finished React one wired the same way; its npm run setup makes your tool and wires it. The extension generator makes it with your own name, colour, pages and prices.

CRXJS

With manifest.json at the root, a CRXJS project is wired as a plain one; Vite processes the Toolaby popup as a page. With manifest.config.ts, the command prints what to 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' } } },
});

The background: when src/background.ts exists, the command prints the two lines to add at its top, the import climbing out of src/: import { toolaby } from '../toolaby.js'; toolaby.startBackground();. When there is none, it writes one with those two lines: at the name your config gives, or src/background.ts, which the printed lines then add to the manifest.

Plasmo

Files at the root. The background: with no background.ts, the command creates one with the two lines, and Plasmo picks it up; with one, it prints the two lines to add at its top. package.json gets, from the printed lines:

"manifest": {
  "key": "<the extension's key, from the printed lines>",
  "permissions": ["storage"],
  "externally_connectable": { "matches": ["https://<workspace>.toolaby.app/*"] }
}

Plasmo builds only the pages its manifest names, and its manifest override replaces action whole, so the Toolaby popup cannot stand in front of yours. Your popup keeps the door itself, with the functions toolaby.js exports, and counts uses at the work:

popup.tsx
import { useEffect, useRef, useState } from "react"
import { toolaby, loadPaywall, applyBrand, renderPaywall, refusalOf } from "./toolaby.js"

function IndexPopup() {
  const panel = useRef<HTMLDivElement>(null)
  const [open, setOpen] = useState(false)

  useEffect(() => {
    ;(async () => {
      // The door, as the Toolaby popup keeps it: nothing is spent at an open. The sign-in or the plans come first only
      // when the person may not use the extension yet; free uses are counted at the work, below.
      const user = await toolaby.refresh()
      const mayNot = (!user.paid && user.access === "paid") || (user.signInRequired && !user.account?.linked)
      if (!mayNot) return setOpen(true)
      // The tool as the background resolved it from the key: its name, Toolaby's address, the free limit.
      const config = await chrome.runtime.sendMessage({ action: "toolaby.config" })
      const paywall = await loadPaywall(config)
      applyBrand(paywall)
      renderPaywall(panel.current!, paywall, user, config, {
        buy: (plan) => toolaby.openPaymentPage(plan),
        signIn: (provider?: "google") => toolaby.openLoginPage(provider),
        account: () => toolaby.openAccountPage()
      }, refusalOf(user, null))
    })()
  }, [])

  const run = async () => {
    const permit = await toolaby.gate() // one free use, or a plan's
    if (!permit.allowed) return toolaby.showPaywall(permit) // Plasmo builds no page of Toolaby's: the checkout page, in a new tab
    // your work
  }

  return open ? <main><button onClick={run}>Do the thing</button></main> : <div ref={panel} />
}

export default IndexPopup

The panel shows only when the person may not use the extension yet; while they may, the popup is your extension. Put <toolaby-account> at its foot for the account line: the free uses left and Upgrade. The panel's Continue with Google calls signIn('google'); the optional provider passes it on to openLoginPage(). See Panel.

Other bundlers

Wire it by hand, under step 1 of the tool's Set up page, gives the files (Download the files), what to add to manifest.json, toolaby.config.js, and the two lines for the top of your background, with your values. The rule for any layout:

  • The platform (toolaby.js — the client and the popup UI as one file — with toolaby.config.js and toolaby.d.ts) goes where your build imports from.
  • The Toolaby popup (toolaby-popup.html and its two-line toolaby-popup.js) — and, with a side panel, toolaby-sidepanel.html and toolaby-sidepanel.js — reaches the output with toolaby.js and toolaby.config.js beside it: copied verbatim, or built as a page of the bundler's.
  • The manifest gets the key that gives every copy the same id, the storage permission, your workspace's address under externally_connectable (no host permission: Toolaby needs none), a module background ("type": "module") that starts Toolaby, and the Toolaby popup as action.default_popup — Toolaby's side panel as side_panel.default_path, with the sidePanel permission.
  • A page of your own that draws the panel in a side panel sets data-toolaby-surface="sidepanel" on <html>, so ensureStyles() 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.

On this page