Skip to content

Build Instructions

Finzytrack ships as a standalone desktop app that bundles the FastAPI backend, the Vue 3 frontend, and a native window shell into a single distributable binary. This page covers how to set up the repository, run Finzytrack in development mode, and produce builds for macOS, Windows, and Linux.

Before building, you need:

  • Python 3.12+ (3.13 recommended)
  • Node.js 20+ (22 recommended)
  • npm (comes with Node.js)
  • Git

Node 20 is a hard minimum: Tailwind CSS v4 compiles through a native module (@tailwindcss/oxide) that is published only for Node 20 and above. On Node 18, npm install appears to succeed but silently skips that module, and npm run build then fails with Error: Cannot find native binding. See Node version errors below.

macOS — verified on macOS Tahoe (26.x). Install:

  1. Xcode Command Line Toolsxcode-select --install. Provides git, compilers, iconutil (used to build the .icns icon bundle), and a system python3 (Apple-bundled, version varies with the Xcode release — recent Xcode 16 ships 3.12).
  2. Homebrew — see brew.sh for the install command.
  3. A current Python and Nodebrew install python@3.13 node@22. macOS does not ship Node at all, and the Xcode-bundled Python may be older than our 3.12+ minimum depending on your Xcode version, so installing both via Homebrew is the most reliable way to match CI.

PyWebView on macOS uses Cocoa via pyobjc (pulled in automatically by pip install pywebview) — no extra system libraries are needed beyond Xcode Command Line Tools.

Linux (Ubuntu/Debian) — verified on Debian 13 (Trixie) and Ubuntu 24.04. Earlier Debian (Bookworm and older) ship only WebKit2GTK 4.0 in the main repos, so a build that targets 4.1 will not run without backports.

Ubuntu 22.04 can build Finzytrack, but not with its own runtimes: it ships Python 3.10 and Node 12, below both minimums, so you would need a newer Python and a newer Node before starting. Ubuntu 24.04 ships a suitable Python and needs only the Node upgrade described below.

PyWebView needs GTK + WebKit system libraries, plus the headers required to build PyGObject and pycairo from source. Install:

Terminal window
sudo apt-get update
sudo apt-get install -y \
python3-venv python3-dev python3-pip \
build-essential pkg-config \
libgirepository1.0-dev \
libcairo2-dev \
gir1.2-webkit2-4.1 \
libwebkit2gtk-4.1-0 \
libfuse2t64 # use libfuse2 on Debian 12 / Ubuntu 22.04

On Debian 13, libfuse2 has been renamed libfuse2t64 as part of the time_t 64-bit transition. The classic libfuse2 name still exists as a transitional package, but the t64 variant is what apt will actually install.

The Python-side GTK bindings (pygobject, pycairo) are installed automatically from desktop/requirements.txt. PyGObject is currently pinned <3.51 because newer 3.5x crashes pywebview’s GTK page-load callback; remove the pin once pywebview’s GTK backend is updated upstream.

Node.js on Debian/Ubuntu. Check what you have before building:

Terminal window
node --version

Debian 13 ships Node.js 20 via apt, which meets the minimum. Ubuntu ships older versions that do not — Ubuntu 24.04 provides Node 18, and Ubuntu 22.04 provides Node 12. On those releases the apt nodejs package cannot build the frontend, so install Node 22 from NodeSource (also the version CI builds with):

Terminal window
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # v22.x

This applies only to building from source. The prebuilt AppImage contains an already-compiled frontend and needs no Node.js at all.

Windows — verified on Windows 11. PyWebView uses EdgeChromium (EdgeWebView2), which is included with modern Windows, so no GUI runtime needs to be installed. You do need Python, Node.js, and Git, which a fresh Windows install does not ship.

The fastest way to install all three is via winget from an elevated PowerShell:

Terminal window
winget install --id Git.Git -e
winget install --id Python.Python.3.13 -e
winget install --id OpenJS.NodeJS.LTS -e

Close and reopen PowerShell after installation so the new entries on PATH are picked up. Verify each tool is on PATH:

Terminal window
git --version
python --version # 3.13.x
node --version # v22.x or v24.x — both work
npm --version

PowerShell execution policy. Out of the box, Windows blocks unsigned PowerShell scripts, which prevents both npm (a .ps1 shim) and the Python venv’s Activate.ps1 from running. Relax the policy once for the current user:

Terminal window
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

This is required before either npm --version or .\venv\Scripts\Activate.ps1 will work. The change is permanent for your user account.

Antivirus. Real-time scanning (Windows Defender included) can quarantine PyInstaller’s output or dramatically slow the build. If Finzytrack.exe mysteriously disappears from desktop\dist\Finzytrack\ after a successful build, add the repository folder to Defender’s exclusions via Settings → Privacy & Security → Windows Security → Virus & threat protection → Manage settings → Exclusions.

Short paths. Some PyInstaller hooks and Node tooling struggle with very long Windows paths. Clone the repository at a short path like C:\finzytrack rather than a deep nested location under Documents or OneDrive.

Terminal window
git clone https://github.com/sagarbehere/finzytrack.git
cd finzytrack
Terminal window
python -m venv venv
source venv/bin/activate # macOS / Linux
# .\venv\Scripts\Activate.ps1 # Windows (PowerShell)
# venv\Scripts\activate.bat # Windows (cmd.exe)

On Windows, Activate.ps1 requires the execution policy change noted under the Windows prerequisites above. If you prefer to skip that, use cmd.exe and the .bat activator instead.

Optionally, upgrade pip itself to the latest release before installing dependencies. The python -m pip form is required on Windows (the pip.exe shim can’t overwrite itself while it’s running) and works identically on macOS and Linux:

Terminal window
python -m pip install --upgrade pip

Then install the project dependencies:

Terminal window
# Backend
pip install -r backend/requirements.txt
# Frontend
cd frontend
npm install
cd ..

If npm run build (or python build.py) fails with:

Cannot find native binding. npm has a bug related to optional dependencies
at Object.<anonymous> (.../node_modules/@tailwindcss/oxide/index.js)

your Node.js is older than 20. Tailwind CSS v4 compiles through a native module that is published only for Node 20 and above. Because that module is an optional dependency, npm skips it without failing when the Node version does not match — so npm install looks like it worked, and the problem only appears at build time.

Ignore the message’s advice to delete package-lock.json; that will not help, and it discards the project’s pinned dependency versions. Install Node 20 or newer (see platform-specific prerequisites), then reinstall:

Terminal window
cd frontend
rm -rf node_modules
npm install

Removing node_modules is necessary because npm will not add the skipped module to an existing install.

The demo ledger is a generated artifact, not a committed file, so a fresh clone does not have one. Generate it from the repository root:

Terminal window
python backend/scripts/generate_fake.py

This writes backend/resources/seed_data/ledgers/fake.beancount in well under a second (about 5,400 transactions). It needs no dependencies beyond the standard library, so it works with or without the virtual environment active.

Run it before first launch if you plan to choose demo data in the setup wizard. Without it, demo mode still completes but seeds an empty ledger, and every dashboard renders blank. Choosing your own ledger file instead is unaffected.

The generated transactions are anchored to the current month, which is why the file is not committed: a checked-in ledger would age out and the demo dashboards — which query recent months — would go empty over time. Re-run the command any time you want to re-anchor it to today. The desktop build regenerates it automatically (desktop/finzytrack.spec), and the test suite generates it on demand, so this step is only for running from source.

During development, the backend and frontend run as separate processes. The Vite dev server proxies API requests to the backend, so both need to be running at the same time.

From the backend/ directory, with the virtual environment active:

Terminal window
cd backend
python -m app.main

This starts the FastAPI server on http://127.0.0.1:8001. The backend accepts several options:

Terminal window
# With debug logging
python -m app.main --debug
# Custom port
python -m app.main --server-port 9000
# Specify a ledger file
python -m app.main --ledger-file /path/to/ledger.beancount

On first run, the backend seeds a default configuration in backend/config/ if one doesn’t already exist.

In a separate terminal, from the frontend/ directory:

Terminal window
cd frontend
npm run dev

This starts the Vite dev server on http://127.0.0.1:3000 with hot module replacement. The dev server proxies /api and /health requests to the backend on port 8001, so the frontend and backend work together seamlessly.

Open http://127.0.0.1:3000 in your browser to use Finzytrack in development mode.

Alternatively, you can build the frontend and have the backend serve it directly, without the Vite dev server:

Terminal window
cd frontend
npm run build
cd ../backend
python -m app.main --static-dir ../frontend/dist

This serves the built frontend from the backend on http://127.0.0.1:8001. This is closer to how the packaged desktop app works, but you lose hot module replacement.

The desktop build packages the backend, frontend, and a native window shell into a single distributable binary. You need one additional dependency:

Terminal window
pip install -r desktop/requirements.txt
Terminal window
cd desktop
python build.py

This will:

  1. Build the Vue frontend (npm run build).
  2. Run PyInstaller with finzytrack.spec.
  3. Package the result for the current platform.
FlagEffect
--cleanRemove build/ and dist/ before building
--skip-frontendSkip the frontend build step (use an existing frontend/dist/)

macOS:

desktop/dist/Finzytrack.app

The .app bundle is ad-hoc codesigned and has the quarantine attribute stripped so it launches without Gatekeeper warnings during development.

Linux:

desktop/dist/Finzytrack-x86_64.AppImage

The build script assembles an AppDir from the PyInstaller output, the .desktop file, and app icons, then uses appimagetool to create the AppImage. If appimagetool is not already present in desktop/build/, the script downloads it automatically.

Windows:

desktop/dist/Finzytrack-windows.zip

A zip archive containing the Finzytrack/ directory with Finzytrack.exe and all dependencies.

For instructions on installing and running the packaged app, see the Installation page.

The desktop build lives in the desktop/ directory and uses three key tools:

  • PyInstaller packages the Python backend, all its dependencies, and bundled data files into a single directory of native binaries.
  • PyWebView provides the native window that hosts the frontend. At runtime, the launcher starts the FastAPI server in a background thread and points a native webview at it.
  • build.py is the cross-platform build script that orchestrates the entire process: building the frontend, running PyInstaller, and performing platform-specific packaging.

PyInstaller is configured via desktop/finzytrack.spec. It bundles:

AssetSourceBundle location
Seed config templatebackend/resources/seed_config/backend/seed_config/
Seed data templatebackend/resources/seed_data/backend/seed_data/
Rule and ledger templatesbackend/app/templates/app/templates/
AI prompt templatesbackend/resources/prompts/resources/prompts/
AI reference filesbackend/resources/ai_reference/resources/ai_reference/
JSON Schemasbackend/resources/schemas/resources/schemas/
Frontend SPAfrontend/dist/frontend_dist/
Beancount VERSION filesite-packagesbeancount/
Finzytrack VERSION fileVERSIONbundle root

All Python dependencies (beancount, FastAPI, uvicorn, scikit-learn, etc.) are automatically collected as hidden imports.

Two of those directories are themselves generated rather than committed: resources/ai_reference/ and resources/schemas/ are produced by scripts/sync_ai_reference.py, which build.py runs before PyInstaller. resources/seed_data/ledgers/fake.beancount is regenerated by the spec itself, anchored to the build month, so a fresh install’s demo dashboards aren’t empty.

The build script detects the current OS and runs the appropriate packaging step:

PlatformPyInstaller outputFinal artifact
macOSdist/Finzytrack.app (.app bundle)Ad-hoc codesigned .app
Linuxdist/Finzytrack/ (directory)Finzytrack-x86_64.AppImage
Windowsdist/Finzytrack/ (directory)Finzytrack-windows.zip

desktop/launcher.py is the entry point for the packaged app. When launched, it:

  1. Resolves paths — from sys._MEIPASS when frozen (packaged) or from the source tree in development.
  2. Creates a platform-specific user data directory on first run.
  3. Starts the FastAPI backend in a background thread.
  4. Waits for the backend to respond on /health (up to 180 seconds).
  5. Opens a PyWebView window pointing at the backend URL.
  6. On window close, signals a graceful backend shutdown.

The launcher also supports --headless mode, which skips the GUI window and runs the server only. The backend always serves the frontend on its HTTP port, so you can access Finzytrack from a browser at http://127.0.0.1:8001 whether or not the native window is open.

Since PyInstaller produces native binaries and cannot cross-compile, builds for each platform must run on that platform’s OS. The repository includes a GitHub Actions workflow (.github/workflows/build-desktop.yml) that builds on all three platforms in parallel using GitHub’s hosted runners.

Manual: Go to the Actions tab in GitHub, select Build Desktop App, and click Run workflow.

Automatic: Push a version tag to trigger a build and create a draft release:

Terminal window
git tag v0.2.0
git push --tags

Each platform job:

  1. Checks out the repository.
  2. Sets up Python 3.13 and Node.js 22.
  3. Installs all dependencies (frontend, backend, desktop, and Linux system packages where needed).
  4. Runs python build.py in the desktop/ directory.
  5. Uploads the platform artifact.

When triggered by a version tag (v*), a separate release job runs after all builds complete. It downloads all three artifacts and creates a draft GitHub Release with the binaries attached.

After a successful workflow run, artifacts are available for download from the workflow run page in the Actions tab:

ArtifactContents
Finzytrack-macOSFinzytrack-macOS.zip containing the .app bundle
Finzytrack-LinuxFinzytrack-x86_64.AppImage
Finzytrack-WindowsFinzytrack-windows.zip

App icons are committed artifacts under assets/icons/. The source of truth is assets/icons/master.svg; the per-platform rasters (macOS .iconset, Linux PNGs at multiple sizes, Windows .ico) are pre-rendered and tracked in the repo so the build doesn’t need an SVG renderer.

When you change master.svg, regenerate the rasters and commit them. You need rsvg-convert (from librsvg) installed locally:

Terminal window
# macOS: brew install librsvg
# Linux: sudo apt install librsvg2-bin
cd assets/icons
python generate.py
git add macos/ linux/ windows/
git commit -m "icons: regenerate from master.svg"

The generator also writes favicon, iOS, and Android assets; those are gitignored because they’re not part of the desktop build.

FilePurpose
desktop/build.pyCross-platform build script
desktop/finzytrack.specPyInstaller configuration (data files, hidden imports, icons)
desktop/launcher.pyApp entry point (starts backend, opens window)
desktop/requirements.txtBuild dependencies (PyInstaller, PyWebView)
desktop/linux/AppRunAppImage entry point
desktop/linux/finzytrack.desktopLinux desktop entry file
assets/icons/master.svgSource-of-truth icon design
assets/icons/generate.pyRenders master.svg into platform rasters (manual; not run during builds)
.github/workflows/build-desktop.ymlCI workflow for building on all platforms