Personal wealth management web application for tracking assets, liabilities, and financial goals with real-time price updates and multi-currency support.
- Frontend: React 19 + Tailwind CSS v4 + shadcn/ui
- Landing Page: Astro 5 (static site)
- Routing: TanStack Router (file-based)
- Data Fetching: TanStack Query +
@convex-dev/react-query - Forms: TanStack Form + Effect Schema (Standard Schema v1)
- Charts: Recharts
- Backend & Database: Convex (reactive document database)
- Core Library: Effect (schemas, services, typed errors, branded types)
- Build: Vite 7 (dashboard), Astro (landing)
- Language: TypeScript 5.9 (strict mode)
- Linting & Formatting: Ultracite (Oxlint + Oxfmt)
- Git Hooks: Lefthook (pre-commit lint/format)
# Enter the reproducible environment (installs dependencies on first use)
devenv shell
# Start dashboard, landing page, and Convex together
devenv up
# Start dashboard Vite dev server (port 5173)
pnpm dev:dashboard
# Start landing page Astro dev server (port 4321)
pnpm dev:landing
# Start Convex dev server
pnpm convex:dev
# The original pnpm command remains available too
pnpm dev:all| Command | Description |
|---|---|
pnpm dev:dashboard | Start dashboard Vite dev server |
pnpm dev:landing | Start landing page Astro dev server |
pnpm build:dashboard | TypeScript check + Vite production build |
pnpm build:landing | Build landing page for production |
pnpm convex:dev | Start Convex dev server |
pnpm dev:all | Run dashboard + landing + Convex concurrently |
pnpm convex:deploy | Deploy Convex functions (prompts for creds) |
pnpm convex:auth-keys | Generate JWT keys and optionally set in Convex |
pnpm check | Lint & format check (ultracite) |
pnpm fix | Auto-fix lint & format issues (ultracite) |
wealthflow/
├── apps/
│ ├── dashboard/ # @wealthflow/dashboard (React SPA)
│ │ ├── convex/ # Convex schema & functions
│ │ └── src/ # React app source
│ └── landing/ # @wealthflow/landing (Astro static site)
│ └── src/ # Astro pages, components, styles
├── packages/
│ └── tsconfig/ # @wealthflow/tsconfig (shared TS configs)
├── scripts/ # Deployment & utility scripts
├── devenv.nix # Reproducible Node 24 development environment
├── devenv.yaml # Pinned Nix inputs
└── docs/ # Architecture & roadmap documentation
The stack runs 4 Docker services:
| Service | Internal Port | Role |
|---|---|---|
convex-backend | 3210 / 3211 | Convex backend (API + site) |
convex-dashboard | 6791 | Convex admin dashboard |
dashboard | 8080 | Vite SPA served by serve |
landing | 4321 | Astro static site |
All services communicate over an internal Docker network. No ports are published (expose, not ports) — a reverse proxy or tunnel is expected to expose services publicly.
- Docker & Docker Compose
- Node.js + pnpm (on your local machine, for
npx convexcommands) - A reverse proxy or tunnel to expose services publicly
Create a .env file at the repository root with the following variables:
# Public URL of the Convex backend (must be reachable from the user's browser)VITE_CONVEX_URL=https://convex.example.com# Public URL of the Convex site endpoint (used for auth callbacks)VITE_CONVEX_SITE_URL=https://convex-site.example.com# Same values, used by the Convex backend itselfCONVEX_CLOUD_ORIGIN=https://convex.example.comCONVEX_SITE_ORIGIN=https://convex-site.example.com# Convex admin dashboard — URL to the Convex backendNEXT_PUBLIC_DEPLOYMENT_URL=https://convex.example.com# Instance secret — MUST be exactly 32 bytes (64 hex characters)# Generate with: openssl rand -hex 32# ⚠️ 64 bytes (128 hex chars) will crash the backend:# "Hex-decoded key was 64 bytes, not 32"INSTANCE_SECRET=<64-hex-chars># Landing pageSITE_URL=https://example.comPUBLIC_DASHBOARD_URL=https://app.example.comPUBLIC_CONTACT_EMAIL=hello@example.com# Optional — Plausible analyticsPUBLIC_PLAUSIBLE_DOMAIN=PUBLIC_PLAUSIBLE_SCRIPT_URL=https://plausible.io/js/script.jsImportant:
VITE_CONVEX_URLandVITE_CONVEX_SITE_URLare injected at build time by Vite into the client JS bundle. They must be publicly accessible URLs (not internal Docker hostnames).
docker network create cloudflared-netThis network is declared as
externalincompose.yamland must exist before starting the services. It allows your reverse proxy (e.g., Cloudflare Tunnel, Traefik, Caddy) to reach the containers.
docker compose up -d --builddocker compose exec convex-backend ./generate_admin_key.shThis outputs a key in the format convex-self-hosted|.... Save it — you'll need it for the next steps.
Run this from your local machine (not inside a container):
pnpm convex:deployThe project uses @convex-dev/auth with the Password provider. It requires two environment variables:
JWT_PRIVATE_KEY— RSA 2048 private key in PKCS8 format (signs tokens)JWKS— the corresponding public key as a JSON Web Key Set (verifies tokens)
Generate both keys and optionally set them as Convex environment variables:
pnpm convex:auth-keysThe script generates an RSA 2048 key pair, prints both values, then prompts you to set them directly via convex env set. You can also skip the prompt and set them manually via the Convex dashboard.
Gotchas:
JWT_PRIVATE_KEYmust contain real newlines, not literal\ncharacters.- Ed25519 keys will not work — the error will be
"PrivateKeyInfo algorithm is not rsaEncryption". Use RSA 2048.- Both variables are required. Missing
JWKSwill causeMissing environment variable JWKSat runtime.
See the docs/ folder for detailed documentation:
- Architecture — Data model, tech stack, and project structure
- Roadmap — Feature sprints and progress tracking