# How they work
You write TypeScript in src/. Your release pipeline bundles it with esbuild
into a single browser-runnable module at dist/panel.js, zips the package, and
attaches it to a GitHub release. ExileCompass downloads that asset, checks it, and runs your
bundle in a sandboxed iframe on an opaque origin. The bundle cannot reach
the app’s DOM, the app’s storage, or any Tauri API.
The host bridge passed to your mount function is the only way out. Every
method on it needs a permission that your manifest declares. If you did not ask for the permission,
the call fails.
# Quick start
The scaffold writes a complete, buildable package, including the manifest, the tsconfig and the release workflow.
git clone https://github.com/juddisjudd/exilecompass
cd exilecompass
node tools/addon-scaffold/create-addon.mjs "My Addon" ../my-addon \
--id dev.you.my-addon --author "You" \
--homepage https://github.com/you/my-addonYour entry point has one default export:
import type { MountFn } from './types';
// The host calls this once, inside a sandboxed iframe. `root` is yours to
// render into; `host` is the only way out.
const mount: MountFn = async ({ root, host }) => {
const saved = await host.storage.get('state');
root.textContent = saved ?? 'Hello from my add-on.';
};
export default mount;bun install
bun run check
bun run buildTo test it before you publish, open the app’s Add-ons hub, choose Install from file, and pick your plugin.manifest.json. The app
loads the add-on straight from the folder, so you do not need a release.
# The package
A built add-on is a flat zip. The installer rejects anything missing:
plugin.manifest.json Exactly this filename. requiredREADME.md The installer rejects a package with no README. requireddist/panel.js The bundle that entry.panel points at. requireddata/default.json Anything that entry.data points at. optionalentry.panel must sit under ./dist/ and end in .js or .mjs. The app never runs your src/. A
repository link is required, so anyone can read the source of an installed add-on.
{
"schemaVersion": "1.0",
"id": "dev.you.my-addon",
"name": "My Addon",
"description": "What it does, in one line.",
"author": "You",
"homepage": "https://github.com/you/my-addon",
"version": "0.1.0",
"kind": "addon",
"entry": {
"panel": "./dist/panel.js",
"data": "./data/default.json"
},
"compatibility": { "app": ">=1.5.0", "pluginApi": "^1.1.0" },
"permissions": ["storage.read", "storage.write", "ui.panel"],
"contributions": {
"view.panels": [
{ "id": "my-addon", "title": "My Addon", "icon": "panel", "pinDefault": false }
]
}
}# Host API
Older hosts may not have every method. Test for a method before you call it, and state what
you need in compatibility.app. The typed copy of this contract lives in the
scaffold’s src/types.ts.
Storage
Key/value strings, scoped to your add-on so no other add-on can read them. The host stores them, not the browser. The sandbox has no usable localStorage.
Returns null when the key was never set.
Values are strings, so call JSON.stringify on anything larger. Keep it small. Add-on storage shares one file with the app’s own settings, so put game data in net.fetchCached instead.
Network
The panel runs on an opaque origin. It cannot call an API that lacks CORS headers, and the browser caches nothing it loads. The host makes every request for you, and only to the hosts your manifest names.
HTTPS GET. Redirects are blocked, so an allowed host cannot send you somewhere else.
Returns a data: URL and caches it on disk for a week. Use it for every image. Without it, the panel downloads every icon again each time it opens.
GET, cached on disk for a day by default and 30 days at most. Use it for large payloads that change once a patch, not once a session. It serves the stale copy when the network is down.
GET or POST, and the only way to read response headers. Both directions are allowlisted: you may set Accept and Content-Type, and you get back x-rate-limit-*, retry-after and content-type. The host owns User-Agent, so traffic stays attributable and cookies never cross. Grants network.fetch for the same host.
App state
Read-only views of what the overlay is doing. Both push changes to an open panel, so you never need to poll.
Which game the overlay targets. game.onChange(cb) fires when the player flips the footer switch.
A snapshot of the player’s imported Path of Building build. builds.onChange(cb) fires on import.
Shell
Hands something off to the rest of the machine.
Opens an https:// URL in the default browser.
# Permissions
The app prints every permission your add-on declares on its card under Add-ons → Installed. Ask for the narrowest set that works. A host pattern covers that host and its subdomains, and nothing wider.
ui.panel Adds a panel. Every add-on with a UI needs this.storage.read Reads its own key/value store.storage.write Writes to that store.network.fetch:<host> HTTPS GET to that host and its subdomains.network.request:<host> POST and response headers for that host. Grants network.fetch for it too.shell.open:<host> Opens that host in the default browser.game.read Which game the overlay is pointed at.builds.read The active imported build.# Publishing
Tag a release
Set the same version in
package.jsonandplugin.manifest.json, then push a matchingvX.Y.Ztag. The scaffolded workflow type-checks your code, builds it, and publishesexilecompass-addon.zipon the release. Keep that asset name. The app uses it to find your package without calling the GitHub API.Add it to the registry
Add your entry to
registry.v1.jsonand open a pull request against exilecompass-registry. Runnpm run validate:registryandnpm run sync:registry, then commit both changed files.Ship updates
After the pull request merges, your add-on appears under Add-ons → Discover in the app. An update takes the same two steps: push a new tag, then raise
latestVersionin the registry.
# Examples
exilecompass-addon-example The reference package, and the smallest complete add-on. Start here.Economy exilecompass-addon-economy Live poe.ninja prices. Shows net.fetch, net.fetchImage and game.onChange in real use.Price Check exilecompass-addon-pricecheck Trade API price checking. Uses net.request for POST and rate-limit headers, and net.fetchCached for the stat database. Its PLAN.md sets out the whole design.If you get stuck on the host bridge, ask in the Discord. The API grows to fit what add-ons actually need.