docs/addons/addons-overview.md
Wealthfolio addons are TypeScript modules that extend the application's functionality. This guide covers how to build, test, and distribute addons.
New to addon development? Start with our Quick Start Guide to create your first addon.
Addons are TypeScript/React-based extensions that provide access to Wealthfolio's financial data and UI system.
Technical Foundation Each addon is a JavaScript function that receives an
AddonContext object with access to APIs, UI components, and event system.
Integration Capabilities Addons can register new navigation items, routes, and components that integrate directly into Wealthfolio's interface.
Development Environment Built with TypeScript, React, and modern web APIs. Includes hot-reload development server and comprehensive type definitions.
┌─────────────────────────────────────────────────────────────────┐
│ Wealthfolio Host Application │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Addon Runtime │ │ Permission │ │ API Bridge │ │
│ │ │ │ System │ │ │ │
│ │ • Load/Unload │ │ • Detection │ │ • Type Bridge │ │
│ │ • Lifecycle │ │ • Validation │ │ • Domain APIs │ │
│ │ • Context Mgmt │ │ • Enforcement │ │ • Scoped Access │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Individual Addons │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Addon A │ │ Addon B │ │ Addon C │ │ Addon D │ │
│ │ │ │ │ │ │ │ │ │
│ │ enable() │ │ enable() │ │ enable() │ │ enable() │ │
│ │ disable() │ │ disable() │ │ disable() │ │ disable() │ │
│ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │
│ │ API Calls │ │ API Calls │ │ API Calls │ │ API Calls │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Every addon exports an enable function that receives a context object. The
sidebar entry and route are declared in manifest.json
(contributes.routes + contributes.links), so the host renders navigation
without booting the addon; enable only registers the route's component and any
event listeners:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
import { MyComponent } from './MyComponent';
// The host owns a single React root per addon and mounts the route `component`
// itself (with no ctx). Capture the context at enable time so the wrapper can
// hand it down. Do NOT call createRoot yourself — the host manages the root.
let addonCtx: AddonContext | undefined;
const MyRoute = () => (
<QueryClientProvider client={addonCtx!.api.query.getClient() as QueryClient}>
<MyComponent ctx={addonCtx!} />
</QueryClientProvider>
);
const enable: AddonEnableFunction = (ctx) => {
addonCtx = ctx;
// The route `id` MUST match `contributes.routes[].id` in the manifest.
ctx.router.add({
id: 'my-addon',
path: '/addons/my-addon',
component: MyRoute,
});
// Listen to events
const unlisten = ctx.api.events.portfolio.onUpdateComplete(() => {
// Handle portfolio updates
});
// Cleanup: the host owns the React root, so there is nothing to unmount.
ctx.onDisable(() => {
addonCtx = undefined;
unlisten();
});
};
export default enable;
Addons operate under a permission-based security model with three stages:
During installation, addon code is scanned for API usage patterns:
// This pattern is detected:
const accounts = await ctx.api.accounts.getAll();
// Detected permission: accounts.getAll
| Category | Risk Level | Functions |
|---|---|---|
accounts | High | getAll, create |
portfolio | High | getHoldings, update, recalculate |
activities | High | getAll, search, create, update, import |
market-data | Low | searchTicker, sync, getProviders |
assets | Medium | getProfile, updateProfile, updateDataSource |
quotes | Low | update, getHistory |
performance | Medium | calculateHistory, calculateSummary |
goals | Medium | getAll, create, update, updateAllocations |
settings | Medium | get, update, backupDatabase |
files | Medium | openCsvDialog, openSaveDialog |
events | Low | onDrop, onUpdateComplete, onSyncStart |
secrets | High | set, get, delete |
network | High | request (brokered fetch to declared hosts) |
Baseline capabilities are not permissions.
ui,query,toast,logger, andstorageare granted to every addon and must not appear inmanifest.jsonpermissions. Only data categories plusfiles,network,secrets,events,snapshots, andsettingsrequire declaration and consent.
During installation, users see both declared and detected permissions, then approve or reject the addon installation.
The addon context provides access to domain-specific data APIs plus a set of
baseline capabilities (query, storage, toast, logger) that every
addon gets without declaring a permission:
interface AddonContext {
sidebar: SidebarAPI;
router: RouterAPI;
onDisable: (callback: () => void) => void;
api: {
// Baseline capabilities — no permission declaration required
query: QueryAPI; // shared QueryClient (getClient, invalidate, refetch)
storage: StorageAPI; // durable, per-addon key/value store
toast: ToastAPI; // user-facing notifications
logger: LoggerAPI; // scoped logging
// Domain data APIs — declared in manifest `permissions`
accounts: AccountsAPI;
portfolio: PortfolioAPI;
activities: ActivitiesAPI;
market: MarketAPI;
assets: AssetsAPI;
quotes: QuotesAPI;
performance: PerformanceAPI;
exchangeRates: ExchangeRatesAPI;
goals: GoalsAPI;
contributionLimits: ContributionLimitsAPI;
settings: SettingsAPI;
files: FilesAPI;
events: EventsAPI;
secrets: SecretsAPI;
};
}
npm install @wealthfolio/addon-sdk @wealthfolio/ui react react-dom
npm install -D @wealthfolio/addon-dev-tools typescript vite
The development tools include a hot-reload server:
# Start development server
npm run dev:server
# Available on localhost:3001-3003
# Auto-discovered by Wealthfolio
Development Server Structure:
├─ /health # Health check
├─ /status # Build status
├─ /manifest.json # Addon manifest
└─ /addon.js # Built addon code
hello-world-addon/
├── src/
│ ├── addon.tsx # Main addon entry point
│ ├── components/ # React components
│ ├── hooks/ # React hooks
│ ├── pages/ # Addon pages
│ ├── utils/ # Utility functions
│ └── types/ # Type definitions
├── assets/ # Static assets (optional)
├── dist/ # Built files (generated)
├── manifest.json # Addon metadata and permissions
├── package.json # NPM package configuration
├── vite.config.ts # Build configuration
├── tsconfig.json # TypeScript configuration
└── README.md # Documentation
{
"id": "my-addon",
"name": "My Addon",
"version": "1.0.0",
"main": "dist/addon.js",
"description": "Addon description",
"author": "Your Name",
"sdkVersion": "3.6.1",
"minWealthfolioVersion": "3.6.1",
"enabled": true,
"contributes": {
"routes": [{ "id": "my-addon" }],
"links": {
"sidebar": [
{
"id": "my-addon",
"route": "my-addon",
"label": "My Addon",
"icon": "squares-four",
"order": 100
}
]
}
},
"permissions": [
{
"category": "accounts",
"functions": ["getAll"],
"purpose": "List accounts"
},
{
"category": "portfolio",
"functions": ["getHoldings"],
"purpose": "Read holdings"
}
]
}
The host mounts that route at /addons/my-addon, derived from the manifest
id. Omit path for the root, or use a relative suffix such as
"path": "reports/:year" for a nested page.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │ │ │ │
│ ZIP File │───▶│ Extract │───▶│ Validate │───▶│ Analyze │
│ │ │ │ │ │ │ Permissions │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ │ │ │
│ Running │◀───│ Enable │◀───│ Load │◀─────────────┘
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
Addons that declare contributes.routes boot lazily: the host reads the
manifest into a ContributionRegistry and renders the addon's sidebar entries and
routes without executing any addon code. The addon's enable function runs
only on the first visit to one of its routes — not eagerly at startup. Addons
without contributes stay eager and run at load time. This keeps startup
fast and lets the host draw navigation for addons that have never been opened.
Each addon receives an isolated context with scoped secret storage:
// Addon "my-addon" accessing secrets
await ctx.api.secrets.set("api-key", "value");
// Stored as: "addon_my-addon_api-key"
Addons have access to Wealthfolio's UI component library:
import { Button, Card, Dialog, Input, Table } from '@wealthfolio/ui';
import { AmountDisplay, GainAmount, CurrencyInput } from '@wealthfolio/ui/financial';
import { TrendingUp, DollarSign } from 'lucide-react';
function MyComponent() {
return (
<Card className="p-6">
<div className="flex items-center space-x-2">
<TrendingUp className="h-4 w-4" />
<span>Portfolio Growth</span>
</div>
<div className="mt-4">
<AmountDisplay value={1234.56} currency="USD" />
<GainAmount value={123.45} percentage={5.2} />
</div>
</Card>
);
}
Available libraries:
components/financial) for amounts, gains, and
currency inputsStandard Vite configuration externalizes host-provided dependencies as ESM:
// vite.config.ts
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
const hostProvidedDependencies = [
"@tanstack/react-query",
"@wealthfolio/addon-sdk",
"@wealthfolio/addon-sdk/host-api",
"@wealthfolio/addon-sdk/host-dependencies",
"@wealthfolio/addon-sdk/manifest",
"@wealthfolio/addon-sdk/permissions",
"@wealthfolio/addon-sdk/types",
"@wealthfolio/addon-sdk/utils",
"@wealthfolio/ui",
"@wealthfolio/ui/chart",
"date-fns",
"lucide-react",
"react",
"react-dom",
"react-dom/client",
"react/jsx-dev-runtime",
"react/jsx-runtime",
"recharts",
];
export default defineConfig({
plugins: [react(), tailwindcss()],
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
build: {
lib: {
entry: "src/addon.tsx",
fileName: () => "addon.js",
formats: ["es"],
},
outDir: "dist",
minify: true,
sourcemap: false,
rollupOptions: {
external: hostProvidedDependencies,
},
},
});
{
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"dev:server": "wealthfolio dev",
"clean": "rm -rf dist",
"package": "mkdir -p dist && zip -r dist/$npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md",
"bundle": "pnpm clean && pnpm build && pnpm package",
"lint": "tsc --noEmit",
"type-check": "tsc --noEmit"
}
}
Because each addon runs in an isolated sandbox, the host classifies common failure modes and surfaces them as a toast plus an inline panel in the addon frame:
localStorage/sessionStorage call (which throws in
the sandbox). Use ctx.api.storage instead.router.add({ id })
doesn't match a declared contributes.routes[].id.PermissionError thrown for unauthorized API callsUsers can install addons directly from ZIP files. To publish your addon in the Wealthfolio Store, contact [email protected].