Wealthfolio logo Wealthfolio
Download
Docs
Wealthfolio Addon Development

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.getAll

2. Permission Categories

CategoryRisk LevelCommon Functions
accountsHighgetAll, create
portfolioHighgetHoldings, getHolding, update, recalculate, valuations
activitiesHighgetAll, search, create, update, saveMany, import
market-dataLowsearchTicker, syncHistory, sync, getProviders, dividends
assetsMediumgetProfile, updateProfile, updateQuoteMode
quotesLowupdate, getHistory
performanceMediumcalculateHistory, calculateSummary, calculateAccountsSimple
currencyLowgetAll, update, add
financial-planningMediumgetAll, create, update, getFunding, saveFunding
contribution-limitsMediumgetAll, create, update, calculateDeposits
settingsMediumget, update, backupDatabase
filesMediumopenCsvDialog, openSaveDialog
snapshotsHighgetAll, getByDate, save, checkImport, importSnapshots
eventsLowonDrop, onUpdateComplete, onSyncComplete
networkHighrequest
secretsHighset, 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 vite

Core 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 Wealthfolio
Development Server Structure:
├─ /health          # Health check
├─ /status          # Build status
├─ /manifest.json   # Addon manifest
└─ /addon.js        # Built addon code

Project 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               # Documentation

Manifest 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     │◀─────────────┘
│             │    │             │    │             │
└─────────────┘    └─────────────┘    └─────────────┘
  1. Extract: Unzip addon package and read files
  2. Validate: Check manifest.json structure and compatibility
  3. Analyze Permissions: Scan code for API usage patterns
  4. Approve: User reviews permissions and requested network hosts
  5. Load: Create sandbox iframe with scoped APIs
  6. Enable: Call addon’s enable function
  7. 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

  • PermissionError thrown 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.

Quick Start

🏃‍♂️ Quick Start

Create your first addon

Get Started →
View

📖 API Reference

Explore available APIs

Browse APIs →
View

💡 Examples

See real addon implementations

Browse Examples →
View

🏪 Addon Store

Explore available addons

Visit Store →
View