Wealthfolio logo Wealthfolio
Download
Docs
Getting Started

Getting Started

Create your first Wealthfolio addon in 5 minutes.


Prerequisites

# 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)

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

# 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

# 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:

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)
├── 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:

{
  "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.6.1",
  "minWealthfolioVersion": "3.6.1",
  "enabled": true,
  "contributes": {
    "routes": [{ "id": "hello-world", "path": "/addons/hello-world" }],
    "links": {
      "sidebar": [
        {
          "id": "hello-world",
          "route": "hello-world",
          "label": "Hello World",
          "icon": "puzzle-piece",
          "order": 100
        }
      ]
    }
  },
  "permissions": [],
  "hostDependencies": {
    "@wealthfolio/addon-sdk": "^3.6.1",
    "@wealthfolio/ui": "^3.6.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, 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:

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',
    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 names, typed as AddonIconName. See the full list in the API reference.

Start Development

# Start development server (recommended)
pnpm dev:server

Output:

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

Available Commands

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.

First, install TanStack Query in your addon:

pnpm add @tanstack/react-query@^5.62.7

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

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.totalValue?.toLocaleString() || 'N/A'}
                        </div>
                        <div className="text-sm text-muted-foreground">Total Value</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
// shared QueryClientProvider) to the page. The QueryClientProvider shares one
// cache across route navigations.
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',
    component: HelloWorldRoute,
  });

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

export default enable;

Update Permissions

Update manifest.json to include account access:

{
  "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.6.1",
  "minWealthfolioVersion": "3.6.1",
  "enabled": true,
  "contributes": {
    "routes": [{ "id": "hello-world", "path": "/addons/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.6.1",
    "@wealthfolio/ui": "^3.6.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

# 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:

// 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

export default function enable(ctx: AddonContext) {
  try {
    // Your addon code
  } catch (error) {
    ctx.api.logger.error('Addon error:', 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

IDE Setup

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:

{
  "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

# Type checking
pnpm type-check

# Linting
pnpm lint

# Formatting
pnpm format

Configuration Files

Package.json Scripts

{
  "scripts": {
    "dev:server": "wealthfolio 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/ -x \"*.map\""
  }
}

TypeScript Configuration

{
  "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

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

export default defineConfig({
  plugins: [react()],
  build: {
    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

Continue with: