Plugins overview
How PuntoSuite plugins extend the desktop POS through a typed, worker-isolated BodegaAPI — and a map of the rest of the plugin docs.
PuntoSuite is a desktop point-of-sale app. Plugins let you extend that desktop app without forking it — adding behavior on store events, a button in the topbar, a theme, persisted settings, and read-only access to your shop's data.
PuntoSuite uses a mediated extension model (Shopify / VS Code style). Plugins never touch the DOM and never get raw Node.js or filesystem access by default. Instead, everything goes through a single typed interface, the BodegaAPI, that the host controls. This keeps a cash-handling POS safe while still giving plugins real reach.
The mediated model
A plugin is a small ESM module that exports two functions:
import type { BodegaAPI } from '@bodegasuite/plugin-sdk'
export async function activate(api: BodegaAPI): Promise<void> {
// runs on load AND on every reload — register everything here
}
export async function deactivate(): Promise<void> {
// runs on disable/uninstall (optional) — call your disposers here
}
Three properties define the model:
- Typed and mediated — your only interface to the app is the
BodegaAPIobject passed toactivate. There is no globalwindow, no DOM, nodocument, and no raw IPC. Import the types from@bodegasuite/plugin-sdk. - No DOM, no Node by default — plugins cannot reach the renderer's DOM at all. Node built-ins
(
net,fs,child_process,worker) are denied unless you declare the matching capability in your manifest. Capabilities gate Node built-ins; permissions gate BodegaAPI surfaces — they are different gates (see Security). - Worker-isolated — non-bundled plugins run in a
node:worker_threadsworker by default. All host access flows over a mediated RPC bridge, so a misbehaving plugin cannot reach the app's internals.
What a plugin can extend
| Capability | BodegaAPI surface | Notes |
|---|---|---|
| React to events | hooks.addAction(name, cb, priority?) | Six core action hooks fire today (see below). |
| Transform values | filters.addFilter(name, cb, priority?) | First core filter sale:receiptTitle is live (host re-validates the return); other names not wired yet. |
| Topbar button | ui.registerPanel({ location: 'navbar' }) + ui.onNavItemClick | sidebar / settings panels are accepted and recorded but not rendered yet (reserved). |
| Theming | ui.registerTheme, ui.setTheme, ui.getActiveTheme | Select a built-in light / dark palette. Custom tokens are not applied today. |
| Toasts | ui.notify(message, { type? }) | Fire-and-forget; rate-limited (~5 per 10s per plugin). |
| Persisted config | config.get(key), config.set(key, value) | Per-plugin JSON; survives restarts. |
| Read-only data | db.products.getById/list, db.sales.getById/list | Requires ipc:products:read / ipc:sales:read. Auto-scoped to the active business. |
| Locale | i18n.getLocale() | Read the host UI locale ('es' / 'en') and localize your own strings. |
Action hooks fired by core today
These are the only events core fires right now:
app:ready
sale:afterComplete
sale:afterRefund
inventory:afterAdjust
inventory:lowStock
cashClosure:afterSubmit
Every addAction / addFilter / register* call returns a disposer. Track them and call them
in deactivate(). See Lifecycle and the full
API reference.
Where plugins run and load from
- Runtime: non-bundled plugins run in a worker thread, isolated from the renderer and from each other. Host access is only via the BodegaAPI RPC.
- Discovery roots:
- Dev:
<userData>/pluginsonly. - Prod:
<resourcesPath>/plugins(bundled), then<userData>/plugins(installed/sideloaded). - The repo's
packages/plugin-*andplugins/directories are source/mirror only — not auto-loaded.
- Dev:
- Local sideload test (Windows): build, then copy
manifest.json+dist/into%APPDATA%/BodegaSuite/plugins/<manifest.id>/, restart, and confirm the sideload in Settings > Plugins. A dropped folder never auto-runs.
Trust tiers
Plugins are loaded fail-closed by trust tier:
| Tier | Source | Behavior |
|---|---|---|
| A — bundled | Shipped with the app | Auto-loads. |
| B — marketplace | Signed / digest, advisory trust | Still requires operator confirmation. |
| C — sideload | Dropped in by an operator/dev | Operator must confirm in Settings before it loads. |
An Ed25519-signed, anti-rollback blocklist is checked every boot as a kill switch. See Security for the full model.
Is a plugin right for me?
A plugin is a good fit when you want to:
- Run logic when a sale completes, a refund happens, stock is adjusted, or a cash drawer closes.
- Add a topbar button (e.g., a theme toggle) and persist a small amount of operator preference.
- Read products or sales for the active business to drive your own feature.
A plugin is not the right tool (today) when you need to:
- Restructure the app layout, replace screens, or render arbitrary UI — there is no DOM access.
- Modify sale totals, tax, or tender — the host owns financial integrity; no filter is invoked yet.
- Write to the database — the data API is read-only today.
If that fits, start with the Developer quickstart.
Map of the plugin docs
| Page | What it covers |
|---|---|
| Developer quickstart | Scaffold, build, sideload, and see your plugin run. |
| Lifecycle | activate / deactivate, reloads, and disposer discipline. |
| API reference | Every BodegaAPI namespace, method, and signature. |
| Contributions | Navbar panels, themes, icons, and host caps. |
| Hooks and API | The action hooks core fires and how to use them. |
| Manifest | manifest.json fields, permissions, capabilities, and net.allow. |
| Security | Worker isolation, trust tiers, permissions vs capabilities, kill switch. |
| Publishing | Package, run the checklist, and publish to the Marketplace. |
| Troubleshooting | Why a plugin won't load, auto-disable, and common mistakes. |
A real example
com.puntosuite.theme-toggle (source: packages/plugin-theme-toggle/src/index.ts) registers a
contrast navbar button and a dark theme, flips light/dark via ui.setTheme on click, persists
the choice with config, re-applies it on activate, and lets the host revert to light when it is
uninstalled. It's a complete, copy-pasteable tour of the mediated model — walked through in the
Developer quickstart.