openportfolio

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.