Wealthfolio Addon Development
Learn how to build addons for Wealthfolio to extend its functionality with custom tools and integrations.
Wealthfolio addons are TypeScript modules that extend the application’s functionality through a sandboxed runtime. This guide covers how to build, test, and distribute addons for Wealthfolio 3.6 and later.
New to addon development? Start with our Quick Start Guide to create your first addon.
What are Wealthfolio Addons?
Addons are TypeScript/React-based extensions that provide access to Wealthfolio’s financial data and UI system through a brokered API bridge.
Technical Foundation
Each addon is an ES module that exports an enable(ctx) function. Wealthfolio loads the module inside a sandboxed iframe and passes an AddonContext object with access to APIs, route rendering, events, logging, secrets, and network access.
Integration Capabilities
Addons declare their pages and navigation in manifest.json (contributes.routes + contributes.links), so the host builds the sidebar and knows every route without running addon code. Routes must live under the addon’s own namespace, such as /addons/hello-world for hello-world-addon. An addon with declared routes boots lazily — its enable(ctx) runs on the first visit to one of its routes, then renders inside its own iframe.
Development Environment
Built with TypeScript, React, and modern web APIs. Includes hot-reload development server, comprehensive type definitions, and host-provided dependencies to keep addon bundles small.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ 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() │ │
│ │ onDisable │ │ onDisable │ │ onDisable │ │ onDisable │ │
│ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │ UI/Routes │ │
│ │ API Calls │ │ API Calls │ │ API Calls │ │ API Calls │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘Basic Addon Structure
Every addon exports an enable function that receives a context object:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
import { Button } from '@wealthfolio/ui';
function MyAddonPage({ ctx }: { ctx: AddonContext }) {
return (
<div className="p-6">
<h1 className="text-2xl font-semibold">My Tool</h1>
<Button onClick={() => ctx.api.toast.success('Hello from my addon')}>
Show toast
</Button>
</div>
);
}
// The host owns a single React root per addon and mounts the route `component`
// itself. Capture the context at enable time so the route wrapper can hand it
// down. (Do NOT call createRoot yourself — the host manages the lifecycle.)
let addonCtx: AddonContext | undefined;
const MyAddonRoute = () => (
<QueryClientProvider client={addonCtx!.api.query.getClient() as QueryClient}>
<MyAddonPage ctx={addonCtx!} />
</QueryClientProvider>
);
const enable: AddonEnableFunction = (ctx) => {
addonCtx = ctx;
// The sidebar entry and route are declared in manifest.json `contributes`, so
// the host builds navigation before this addon boots. The runtime route `id`
// MUST match `contributes.routes[].id`.
ctx.router.add({
id: 'my-addon',
path: '/addons/my-addon',
component: MyAddonRoute,
});
// Listen to events
let unlistenPortfolio: (() => void) | undefined;
void ctx.api.events.portfolio
.onUpdateComplete(() => {
// Handle portfolio updates
})
.then((unlisten) => {
unlistenPortfolio = unlisten;
});
// Cleanup. The host owns the React root, so there is no root to unmount.
ctx.onDisable(() => {
addonCtx = undefined;
unlistenPortfolio?.();
});
};
export default enable;Permission System
Addons operate under a permission-based security model with three stages:
1. Static Analysis
During installation, addon code is scanned for API usage patterns:
// This pattern is detected:
const accounts = await ctx.api.accounts.getAll();
// Detected permission: accounts.getAll2. Permission Categories
| Category | Risk Level | Common Functions |
|---|---|---|
accounts | High | getAll, create |
portfolio | High | getHoldings, getHolding, update, recalculate, valuations |
activities | High | getAll, search, create, update, saveMany, import |
market-data | Low | searchTicker, syncHistory, sync, getProviders, dividends |
assets | Medium | getProfile, updateProfile, updateQuoteMode |
quotes | Low | update, getHistory |
performance | Medium | calculateHistory, calculateSummary, calculateAccountsSimple |
currency | Low | getAll, update, add |
financial-planning | Medium | getAll, create, update, getFunding, saveFunding |
contribution-limits | Medium | getAll, create, update, calculateDeposits |
settings | Medium | get, update, backupDatabase |
files | Medium | openCsvDialog, openSaveDialog |
snapshots | High | getAll, getByDate, save, checkImport, importSnapshots |
events | Low | onDrop, onUpdateComplete, onSyncComplete |
network | High | request |
secrets | High | set, get, use, delete |
Baseline capabilities are implicit. ui (sidebar/router/navigation), query, toast, logger, and storage are granted to every addon and are not declared in permissions. Only the data domains above plus files, network, secrets, events, snapshots, and settings need a declaration.
3. User Approval
During installation, users see both declared and detected permissions, plus declared network hosts. They can approve or reject the addon installation. Updates that add permissions require reinstall approval.
Available APIs
The addon context provides access to host APIs through a sandbox bridge:
interface AddonContext {
ui: {
root: HTMLElement;
};
sidebar: SidebarAPI;
router: RouterAPI;
onDisable: (callback: () => void) => void;
api: {
accounts: AccountsAPI;
portfolio: PortfolioAPI;
activities: ActivitiesAPI;
market: MarketAPI;
assets: AssetsAPI;
quotes: QuotesAPI;
performance: PerformanceAPI;
exchangeRates: ExchangeRatesAPI;
goals: GoalsAPI;
contributionLimits: ContributionLimitsAPI;
settings: SettingsAPI;
files: FilesAPI;
snapshots: SnapshotsAPI;
events: EventsAPI;
secrets: SecretsAPI;
storage: StorageAPI;
network: NetworkAPI;
navigation: NavigationAPI;
query: QueryAPI;
toast: ToastAPI;
logger: LoggerAPI;
};
}Development Setup
Required Packages
npm install @wealthfolio/addon-sdk @wealthfolio/ui react react-dom
npm install -D @wealthfolio/addon-dev-tools typescript viteCore Dependencies
- @wealthfolio/addon-sdk: TypeScript types and API definitions
- @wealthfolio/ui: UI components based on shadcn/ui and Tailwind CSS
- @wealthfolio/addon-dev-tools: CLI and development server
Development Server
The development tools include a hot-reload server:
# Start development server
npm run dev:server
# Available on localhost:3001-3003
# Auto-discovered by WealthfolioDevelopment Server Structure:
├─ /health # Health check
├─ /status # Build status
├─ /manifest.json # Addon manifest
└─ /addon.js # Built addon codeProject Structure
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 # DocumentationManifest File
{
"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", "path": "/addons/my-addon" }],
"links": {
"sidebar": [
{
"id": "my-addon",
"route": "my-addon",
"label": "My Addon",
"icon": "squares-four",
"order": 100
}
]
}
},
"permissions": [],
"hostDependencies": {
"@wealthfolio/addon-sdk": "^3.6.1",
"@wealthfolio/ui": "^3.6.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}A route (contributes.routes) is a durable addon page the host can render before the addon boots; a link (contributes.links, only the "sidebar" slot is consumed today) places a declared route in a host slot. The runtime router.add({ id }) must use the same id as its declared route. Navigation and the baseline ui capability need no permissions entry.
Lifecycle Management
Installation Process
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │ │ │ │
│ ZIP File │───▶│ Extract │───▶│ Validate │───▶│ Analyze │
│ │ │ │ │ │ │ Permissions │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ │ │ │
│ Running │◀───│ Enable │◀───│ Load │◀─────────────┘
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘- Extract: Unzip addon package and read files
- Validate: Check manifest.json structure and compatibility
- Analyze Permissions: Scan code for API usage patterns
- Approve: User reviews permissions and requested network hosts
- Load: Create sandbox iframe with scoped APIs
- Enable: Call addon’s enable function
- Running: Addon functionality is active
Lazy Activation
Addons that declare contributes.routes boot lazily: the host reads the manifest and builds the sidebar and route table without executing any addon code, then calls enable(ctx) only when one of the addon’s routes is first visited. Because navigation comes from the manifest, a route survives reloads and deep links even before the addon has run.
Sandbox failures degrade gracefully rather than crashing the host — blocked storage, an unknown API call, or an unavailable route surface as a toast plus an inline panel in the addon’s frame, and errors in one addon never affect another.
Context Isolation
Each addon runs in its own sandboxed iframe and receives an isolated context. Host APIs are exposed through a broker; addons do not receive host React globals, the raw host context, or another addon’s context.
// Addon "my-addon" accessing its own secrets
await ctx.api.secrets.set('api-key', 'value');
const apiKey = await ctx.api.secrets.get('api-key');Secrets are stored in the OS keyring under the addon’s own scope. Other addons cannot read them.
UI Components
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:
- All Radix UI components
- Financial components (
components/financial) for amounts, gains, and currency inputs - Lucide React and Phosphor icons — for rendering inside your own page (any icon you like)
- Tailwind CSS utilities
- Recharts for data visualization
- React Query for data fetching
- date-fns for date manipulation
The sidebar icon is different: it’s named by a string from a curated set (typed as
AddonIconName), not a component you import. See Sidebar
icons for the full list.
Build and Distribution
Build Configuration
Use the addon dev tools template when possible. It configures host dependencies so common libraries are provided by Wealthfolio instead of bundled into every addon.
// vite.config.ts
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/addon.tsx',
fileName: () => 'addon.js',
formats: ['es'],
},
rollupOptions: {
external: [
'react',
'react-dom',
'react-dom/client',
'@wealthfolio/addon-sdk',
'@wealthfolio/ui',
],
},
},
});Package Scripts
{
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"dev:server": "wealthfolio dev",
"clean": "rm -rf dist",
"package": "zip -r $npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md -x \"*.map\"",
"bundle": "pnpm clean && pnpm build && pnpm package",
"lint": "tsc --noEmit",
"type-check": "tsc --noEmit"
}
}Error Handling
Addon Failures
- Errors are logged but don’t affect other addons
- Host application continues normally
- Users see error notifications
Permission Violations
PermissionErrorthrown for unauthorized API calls- API calls are blocked
- Errors are logged for debugging
Security Model
- Each addon runs in a sandboxed iframe with a brokered API bridge
- Routes are limited to the addon’s own
/addon/<namespace>or/addons/<namespace>path - Manifest permissions are enforced at runtime
- Static analysis detects additional API usage during installation
- Network access is brokered by the backend and limited to approved HTTPS hosts
- Secrets are stored in the OS keyring and scoped by addon ID
- Bearer authorization headers are injected by the backend from add-on-scoped secrets
- Addon network requests are audited locally
Publishing
Users can install addons directly from ZIP files. To publish your addon in the Wealthfolio Store, contact wealthfolio@teymz.com.