Source: https://wealthfolio.app/docs/addons/getting-started/

# Build Your First Addon

Create your first Wealthfolio addon in 5 minutes.

* * *

Last updated September 10, 2026

## Prerequisites

```bash
# Check Node.js version (requires 20.19+ or 22.12+)
node --version

# Check pnpm
pnpm --version

# Install pnpm if needed
npm install -g pnpm
```

Requirements:

-   Node.js 20.19+ or 22.12+ and pnpm
-   Wealthfolio desktop app (optional but recommended: running in development mode for live reload and testing)
-   Basic TypeScript and React knowledge
-   Code editor (VS Code recommended)

## Start Wealthfolio (Recommended)

For the best development experience with live addon reloading, start Wealthfolio in addon development mode:

```bash
# Clone Wealthfolio repository (if not already done)
git clone https://github.com/wealthfolio/wealthfolio.git
cd wealthfolio

# Install dependencies
pnpm install

# Start in addon development mode (enables live addon reloading)
VITE_ENABLE_ADDON_DEV_MODE=true pnpm tauri dev
```

This enables:

-   Live addon reload when files change
-   Better error messages and debugging
-   Automatic addon discovery
-   Console logging for development

## Create New Addon

```bash
# Navigate to development directory
cd ~/Documents/WealthfolioAddons

# Create addon using CLI
npx @wealthfolio/addon-dev-tools create <addon-name>

# Navigate and install
cd <addon-name>
pnpm install
```

This will scaffold a new addon project with the following structure:

```plaintext
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
├── dist/                   # Built files (generated)
├── assets/                 # Private static assets (optional)
├── 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

`manifest.json` defines metadata and permissions:

```json
{
  "id": "hello-world-addon",
  "name": "Hello World Addon",
  "version": "1.0.0",
  "description": "My first Wealthfolio addon",
  "author": "Your Name",
  "main": "dist/addon.js",
  "sdkVersion": "3.8.0",
  "minWealthfolioVersion": "3.8.0",
  "enabled": true,
  "contributes": {
    "routes": [{ "id": "hello-world" }],
    "links": {
      "sidebar": [
        {
          "id": "hello-world",
          "route": "hello-world",
          "label": "Hello World",
          "icon": "puzzle-piece",
          "order": 100
        }
      ]
    }
  },
  "permissions": [],
  "hostDependencies": {
    "@wealthfolio/addon-sdk": "^3.8.0",
    "@wealthfolio/ui": "^3.8.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}
```

Navigation is **declarative**. A `contributes.routes` entry is a durable addon page — the host can render it (and build the sidebar) before your addon boots, so nothing runs until the route is first visited. A `contributes.links` entry places that route in a host slot (only `"sidebar"` is consumed today) and references a declared route `id`. The runtime `router.add({ id })` you register in `addon.tsx` **must use the same `id`** as its declared route.

Baseline capabilities — `ui`, `navigation`, private packaged `assets`, `query`, `toast`, `logger`, and `storage` — are implicit and never declared in `permissions`. Only data domains and `files`, `network`, `secrets`, `events`, `snapshots`, and `settings` need an entry.

## Main Addon File

`src/addon.tsx` contains the addon logic:

```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';

function HelloWorldPage() {
  return (
    <div className="p-6 max-w-4xl mx-auto">
      <h1 className="text-4xl font-bold mb-4">Hello Wealthfolio</h1>
      <p className="text-xl mb-8">Your first addon is working.</p>

      <div className="border rounded-lg p-6">
        <h2 className="text-lg font-semibold mb-2">Success</h2>
        <p>You've successfully created and loaded your first addon.</p>
      </div>
    </div>
  );
}

// The host owns a single React root per addon and mounts the route `component`
// itself, with no access to the addon context. Capture it 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 HelloWorldRoute = () => (
  <QueryClientProvider client={addonCtx!.api.query.getClient() as QueryClient}>
    <HelloWorldPage />
  </QueryClientProvider>
);

const enable: AddonEnableFunction = (ctx) => {
  addonCtx = ctx;

  // The sidebar item + route are declared in manifest.json (`contributes`), so
  // the host renders navigation without booting the addon. The route `id` MUST
  // match `contributes.routes[].id`.
  ctx.router.add({
    id: 'hello-world',
    path: '/addons/hello-world-addon',
    component: HelloWorldRoute,
  });

  ctx.api.logger.info('Hello World addon loaded');

  // The host owns the React root, so there is nothing to unmount here.
  ctx.onDisable(() => {
    addonCtx = undefined;
    ctx.api.logger.info('Hello World addon disabled');
  });
};

export default enable;
```

Hand the host a `component` and let it own the single React root — do **not** call `createRoot` yourself (a per-route root leaves an orphaned tree whose re-renders never reach the DOM). The component receives the current route as a `{location}` prop; the sandbox has no react-router provider, so `useLocation()` / `useParams()` are unavailable. `render` remains as a legacy imperative escape hatch, but `component` is preferred.

The sidebar `icon` (declared in `contributes.links`) is one of a curated set of [Phosphor](https://phosphoricons.com/) names, typed as `AddonIconName`. See the full list in the [API reference](https://wealthfolio.app/docs/addons/api-reference/#sidebar-icons).

## Add Packaged Assets

Non-JavaScript/CSS files below `assets/**` and generated files below `dist/assets/**` are indexed automatically in Wealthfolio 3.7. JavaScript and CSS in those roots remain runtime modules and styles. You do not add an `assets` field or permission to the manifest.

```typescript
export async function loadAddonResources(ctx: AddonContext) {
  const logoUrl = await ctx.assets.getUrl('assets/logo.png');
  const configBlob = await ctx.assets.getBlob('assets/config.json');
  const config = JSON.parse(await configBlob.text());

  return { config, logoUrl };
}
```

-   `list()` returns public path, MIME type, and size metadata.
-   `has(path)` checks whether a logical package path exists.
-   `getBlob(path)` lazily loads verified bytes.
-   `getUrl(path)` creates one sandbox-local Blob URL and automatically revokes it when the addon stops or reloads.

Local CSS `url(...)` references are rewritten relative to the CSS file. Remote CSS URLs and `@import` are rejected; JavaScript and JSX asset strings are not rewritten, so use `getUrl()`. Addons using this API must set `sdkVersion` and `minWealthfolioVersion` to `3.7.0`.

`ctx.assets` is the package asset registry. `ctx.api.assets` is the separate financial-instrument API. See [v3.7 compatibility and assets](https://wealthfolio.app/docs/addons/v3-7-assets/) for limits and migration guidance.

## Start Development

```bash
# Start development server (recommended)
pnpm dev:server
```

Output:

```plaintext
Wealthfolio Addon Development Server
Addon: hello-world-addon
Server: http://localhost:3001
Watching for changes...
```

### Hot Reload Features

-   File watching in `src/` directory
-   Fast rebuilds with Vite
-   Hot Module Replacement for component updates
-   Auto-discovery by Wealthfolio
-   Error recovery with overlay messages
-   Coherent `/runtime-package` generations for code, CSS, and private assets

Wealthfolio 3.7 requires `@wealthfolio/addon-dev-tools` 3.7 or newer. A 404 or 405 for `/runtime-package` means the development server is too old; upgrade it and restart `pnpm dev:server`.

### Available Commands

```bash
pnpm dev:server   # Start development server (recommended)
pnpm build        # Production build
pnpm type-check   # Run TypeScript checks
pnpm lint         # Run ESLint
pnpm format       # Run Prettier
pnpm bundle       # Bundle addon for distribution
```

Verify in Wealthfolio:

1.  Open Wealthfolio (preferably in development mode with `pnpm tauri dev`)
2.  Check sidebar for “Hello World”
3.  Click to load addon page
4.  Check console for log message

## Add Data Access

For data access, it’s recommended to use [TanStack Query](https://tanstack.com/query/latest).

First, install TanStack Query in your addon:

```bash
pnpm add @tanstack/react-query@^5.90.0
```

Update `src/addon.tsx` to access portfolio data using TanStack Query:

```typescript
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import type { AddonContext, AddonEnableFunction, Account } from '@wealthfolio/addon-sdk';

function HelloWorldPage({ ctx }: { ctx: AddonContext }) {
  const {
    data: accounts = [],
    isLoading,
    isError,
    error,
    refetch,
  } = useQuery<Account[]>({
    queryKey: ['accounts'],
    queryFn: () => ctx.api.accounts.getAll(),
    staleTime: 5 * 60 * 1000, // 5 minutes
    refetchOnWindowFocus: false,
  });

  return (
    <div className="p-6 max-w-4xl mx-auto">
      <h1 className="text-3xl font-bold mb-6">Hello Wealthfolio</h1>

      <div className="border rounded-lg p-6 mb-8">
        <div className="flex justify-between items-center mb-4">
          <h2 className="text-lg font-semibold">Portfolio Summary</h2>
          <button
            onClick={() => refetch()}
            disabled={isLoading}
            className="px-3 py-1 text-sm border rounded hover:bg-gray-50 disabled:opacity-50"
          >
            {isLoading ? 'Loading...' : 'Refresh'}
          </button>
        </div>

        {isLoading ? (
          <div className="flex items-center space-x-2">
            <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900"></div>
            <span>Loading accounts...</span>
          </div>
        ) : isError ? (
          <div className="text-red-600">
            <p>Failed to load accounts: {error?.message}</p>
            <button
              onClick={() => refetch()}
              className="mt-2 px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200"
            >
              Try Again
            </button>
          </div>
        ) : (
          <div>
            <p className="mb-4">
              You have {accounts.length} account{accounts.length !== 1 ? 's' : ''}:
            </p>

            {accounts.length > 0 ? (
              <div className="grid gap-3">
                {accounts.map((account) => (
                  <div key={account.id} className="border rounded-lg p-4">
                    <div className="flex justify-between items-center">
                      <div>
                        <h3 className="font-semibold">{account.name}</h3>
                        <p className="text-sm text-muted-foreground">
                          {account.currency} • {account.isActive ? 'Active' : 'Inactive'}
                        </p>
                      </div>
                      <div className="text-right">
                        <div className="text-lg font-semibold">
                          {account.balance.toLocaleString()}
                        </div>
                        <div className="text-sm text-muted-foreground">Balance</div>
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <p className="text-muted-foreground">
                No accounts found. Add an account in Wealthfolio to see data.
              </p>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

// Capture the context at enable time so the route wrapper can supply it (and a
// QueryClientProvider) to the page. The QueryClient is scoped to this addon and
// reused across route navigations; invalidations/refetches are bridged to the host.
let addonCtx: AddonContext | undefined;

const HelloWorldRoute = () => (
  <QueryClientProvider client={addonCtx!.api.query.getClient() as QueryClient}>
    <HelloWorldPage ctx={addonCtx!} />
  </QueryClientProvider>
);

const enable: AddonEnableFunction = (ctx) => {
  addonCtx = ctx;

  ctx.router.add({
    id: 'hello-world',
    path: '/addons/hello-world-addon',
    component: HelloWorldRoute,
  });

  ctx.onDisable(() => {
    addonCtx = undefined;
  });
};

export default enable;
```

## Update Permissions

Update `manifest.json` to include account access:

```json
{
  "id": "hello-world-addon",
  "name": "Hello World Addon",
  "version": "1.0.0",
  "description": "My first Wealthfolio addon",
  "author": "Your Name",
  "main": "dist/addon.js",
  "sdkVersion": "3.8.0",
  "minWealthfolioVersion": "3.8.0",
  "enabled": true,
  "contributes": {
    "routes": [{ "id": "hello-world" }],
    "links": {
      "sidebar": [
        {
          "id": "hello-world",
          "route": "hello-world",
          "label": "Hello World",
          "icon": "puzzle-piece",
          "order": 100
        }
      ]
    }
  },
  "permissions": [
    {
      "category": "accounts",
      "functions": ["getAll"],
      "purpose": "Display account summary"
    }
  ],
  "hostDependencies": {
    "@tanstack/react-query": "^5.90.0",
    "@wealthfolio/addon-sdk": "^3.8.0",
    "@wealthfolio/ui": "^3.8.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}
```

Only the `accounts` data domain needs declaring — navigation now lives in `contributes`, and the `ui` capability it used to require is baseline (implicit).

## Build and Package

```bash
# Build for production
pnpm build

# Package for distribution
pnpm bundle
```

Creates `dist/hello-world-addon.zip` for installation.

Source maps are useful during development, but they are not required in install bundles. Exclude `*.map` files from release ZIPs to keep packages small.

## Debugging and Development Tools

### Browser Developer Tools

Access full debugging capabilities:

```typescript
// Use console for debugging
ctx.api.logger.info('Debug message');
ctx.api.logger.error('Error message');

// Access React DevTools
// Components will show up in React DevTools extension
```

### Error Handling

```typescript
export default function enable(ctx: AddonContext) {
  try {
    // Your addon code
  } catch (error) {
    ctx.api.logger.error(`Addon error: ${String(error)}`);
    // Handle gracefully
  }
}
```

### Development Server Features

-   Port: `http://localhost:3001`
-   CORS configured for Wealthfolio
-   Source maps for debugging
-   Real-time TypeScript checking
-   Hot Module Replacement
-   Coherent runtime package and lazy asset generations

## IDE Setup

### VS Code (Recommended)

Recommended extensions:

-   TypeScript and JavaScript Language Features
-   ES7+ React/Redux/React-Native snippets
-   Tailwind CSS IntelliSense
-   Auto Rename Tag
-   Error Lens

Create `.vscode/settings.json`:

```json
{
  "typescript.preferences.importModuleSpecifier": "relative",
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}
```

## Code Quality and Testing

### Manual Testing

1.  Start development server
2.  Open Wealthfolio
3.  Navigate to your addon
4.  Test all features
5.  Check console for errors

### Code Quality Commands

```bash
# Type checking
pnpm type-check

# Linting
pnpm lint

# Formatting
pnpm format
```

## Configuration Files

### Package.json Scripts

```json
{
  "scripts": {
    "dev:server": "wealthfolio-addon dev",
    "build": "vite build",
    "type-check": "tsc --noEmit",
    "lint": "eslint src --ext .ts,.tsx",
    "format": "prettier --write \"src/**/*.{ts,tsx}\"",
    "bundle": "pnpm build && zip -r addon.zip manifest.json dist/ assets/ -x \"*.map\""
  }
}
```

### TypeScript Configuration

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```

### Vite Build Configuration

```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
    lib: {
      entry: 'src/addon.tsx',
      formats: ['es'],
      fileName: () => 'addon.js',
    },
    rollupOptions: {
      external: [
        'react',
        'react-dom',
        'react-dom/client',
        '@wealthfolio/addon-sdk',
        '@wealthfolio/ui',
        '@tanstack/react-query',
      ],
    },
  },
});
```

## Next Steps

You now understand:

-   Project structure and development workflow
-   Permission system and security model
-   Hot reload development
-   API integration for portfolio data
-   UI integration with navigation
-   Private packaged assets and their sandbox lifecycle

Continue with:

-   [API Reference](https://wealthfolio.app/docs/addons/api-reference/) - All available APIs
-   [v3.7 Compatibility & Assets](https://wealthfolio.app/docs/addons/v3-7-assets/) - Migration and packaging rules
-   [Examples](https://github.com/wealthfolio/wealthfolio-addons/tree/main/official) - Real addon implementations

* * *
