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 pnpmRequirements:
- 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:
# 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 devThis 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 installThis 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 # DocumentationManifest 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:serverOutput:
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 distributionVerify in Wealthfolio:
- Open Wealthfolio (preferably in development mode with
pnpm tauri dev) - Check sidebar for “Hello World”
- Click to load addon page
- 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.7Update 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 bundleCreates 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 extensionError 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
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:
{
"typescript.preferences.importModuleSpecifier": "relative",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}Code Quality and Testing
Manual Testing
- Start development server
- Open Wealthfolio
- Navigate to your addon
- Test all features
- Check console for errors
Code Quality Commands
# Type checking
pnpm type-check
# Linting
pnpm lint
# Formatting
pnpm formatConfiguration 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:
- API Reference - All available APIs
- Examples - Real addon implementations