Build Your First Addon
Create your first Wealthfolio addon in 5 minutes.
Last updated
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)
├── 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 # 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.7.0",
"minWealthfolioVersion": "3.7.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.7.0",
"@wealthfolio/ui": "^3.7.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, 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:
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 names, typed as AddonIconName. See the full list in the
API reference.
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.
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 for limits and migration
guidance.
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
- Coherent
/runtime-packagegenerations 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
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
// 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:
{
"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.7.0",
"minWealthfolioVersion": "3.7.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.7.0",
"@wealthfolio/ui": "^3.7.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
- 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:
{
"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-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
{
"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: {
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 - All available APIs
- v3.7 Compatibility & Assets - Migration and packaging rules
- Examples - Real addon implementations