Skip to content

Work / Chitragupt / Wiki / Decisions

2026-10-04-broker-sync-is-the-whole-truth-for-its-broker

Decisioncanonicalverified 2026-10-04

DECISION.2026-10-04.BROKER-SYNC-IS-THE-WHOLE-TRUTH-FOR-ITS-BROKER

A broker sync is the whole truth for its broker, and a redirect login is tied to who started it

Decision

Four rules, all found by reading the Broker Connect code against the goal "any user can link a broker and resync whenever they want" (2026-09-07-broker-connect-per-broker-auth, .context/features/0012-broker-connect.md).

  1. A sync removes what the broker no longer reports. Every position a sync writes carries broker_provider. After a pull, any position with that provider whose holding is not in the pull is deleted — in every AY, because the review takes the latest row per holding and an older row would put sold shares back in the total. Rows an uploaded statement or another broker wrote are never touched. The pull artefacts stay, so what was held and when is still on record.
  2. A redirect login carries a one-time state. startBrokerConnect stores a random value at users/{uid}/broker_connect_states/{provider} (server-only, 15 minutes, single use) and sends it through the broker. completeBrokerConnect refuses an oauth_code without the matching value.
  3. A failure body is never an empty portfolio. Angel One answers a dead token with HTTP 200 and status: false. That now throws — BrokerAuthError for a token code (AG8001, AG8002, AG8003, AB1010), a plain error otherwise.
  4. Disconnect ends the token at the broker where the broker allows it (Kite, Upstox). A failed revoke is logged and never blocks the delete.

Also: a sync commits in chunks of 400 writes with the Settings row last, and linked_broker_accounts.status has a collection-group index.

Why

Rule 1: a sync that only adds is wrong the first time a user sells anything, and "resync whenever I want" is exactly when they will notice. Rule 3 is what makes rule 1 safe — without it an expired Angel One token would read as "holds nothing" and the sync would delete the whole account's positions.

Rule 2: the callback page finishes a connect for whoever is signed in, using whatever code is in the URL. With a fixed state, someone could log in to their own broker and send the callback link to a signed-in user, whose Portfolio would then fill with a stranger's holdings.

The chunking: one Firestore batch holds 500 operations and a holding costs two, so an account past about 250 holdings failed outright. The index: dailyBrokerSync queries the collection group by status, which Firestore refuses without one — the scheduled sync would have failed on its first production run.

Impact

  • apps/functions/src/portfolio/broker-sync.ts, broker-connect.ts, new broker-connect-state.ts.
  • packages/brokers: angel-one/client.ts + schema.ts, revokeZerodhaAccessToken, revokeUpstoxAccessToken.
  • packages/shared: broker_provider on PortfolioPositionSchema (optional); state on the oauth_code credentials (required).
  • Website callback reads the state back: settings/brokers/callback/_page-shared.ts.
  • firebase/firestore.indexes.json: the collection-group field override.
  • Dhan copy no longer states how long a token lasts.

Not decided here

  • The same share at two brokers. A position id is <ISIN>__<AY> and the review keeps one row per holding, so RELIANCE held at both Zerodha and Dhan shows only whichever synced last. Fixing it changes the position id and the review engine. tasks.md T027.
  • Dhan for every user. A pasted token works but has to be pasted again each time it runs out. Dhan's consent flow removes that and needs a registered Dhan app. tasks.md T028.