Quick start
Node 22+, pnpm, and a Convex account. The free tier is enough.
1. Install
git clone https://github.com/seonglae/openportfolio.git
cd openportfolio
pnpm install
cp .env.example .env.local
npx convex dev --once # creates the deployment
2. Create the first book
On localhost with no identity provider configured, the deployment will only create the tenant named by
OPENPORTFOLIO_DEV_TENANT.
npx convex env set OPENPORTFOLIO_DEV_TENANT home
npx convex run tenants:create '{"slug":"home","name":"Home","baseCurrency":"GBP"}'
3. Start the UI
cp browser/.env.local.example browser/.env.local # set VITE_CONVEX_URL
pnpm --filter openportfolio-browser dev # http://localhost:6101
4. Sync
npx tsx sync-worker.mts --once
With nothing linked it registers the venues it can serve and records a net worth of zero, which is correct.
5. Put something in it
A manual holdings file is how a pension, a property or an unlisted holding gets into the total instead of being left out of it.
// holdings.json
[
{ "accountKey": "isa", "symbol": "VWRL", "assetClass": "etf", "qty": 40, "price": 118.2, "currency": "GBP" },
{ "accountKey": "wallet", "symbol": "BTC", "assetClass": "crypto", "qty": 0.15, "price": 0, "currency": "USD" }
]
export OPENPORTFOLIO_MANUAL_HOLDINGS=$PWD/holdings.json
npx convex run accounts:link '{"accountKey":"isa","venue":"manual","kind":"brokerage","label":"ISA","currency":"GBP"}'
npx convex run accounts:link '{"accountKey":"wallet","venue":"manual","kind":"wallet","label":"Wallet","currency":"USD"}'
npx tsx sync-worker.mts --once
The BTC row is priced at 0 on purpose: the worker re-quotes crypto through the keyless CoinGecko adapter, converts both rows into GBP, and writes one total.
6. Before exposing it
Two things are open on localhost. While OPENPORTFOLIO_DEV_TENANT is set, any
unauthenticated caller is scoped to that tenant. Unset it and configure Clerk before the deployment is
reachable from the internet.
npx convex env set CLERK_ISSUER_URL https://your-app.clerk.accounts.dev
npx convex env unset OPENPORTFOLIO_DEV_TENANT
KEY="$(openssl rand -hex 32)"
npx convex run tenants:issueServiceKey "{\"key\":\"$KEY\",\"label\":\"sync-worker\",\"role\":\"member\"}"
echo "OPENPORTFOLIO_SERVICE_KEY=$KEY" >> .env.local
Keys are stored as hashes. One key maps to exactly one tenant and carries its own role, so a worker that
only reads can be issued a viewer key and will be refused every write.