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-BROKERA 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).
- 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. - A redirect login carries a one-time state.
startBrokerConnectstores a random value atusers/{uid}/broker_connect_states/{provider}(server-only, 15 minutes, single use) and sends it through the broker.completeBrokerConnectrefuses anoauth_codewithout the matching value. - A failure body is never an empty portfolio. Angel One answers a dead token with HTTP 200 and
status: false. That now throws —BrokerAuthErrorfor a token code (AG8001,AG8002,AG8003,AB1010), a plain error otherwise. - 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, newbroker-connect-state.ts.packages/brokers:angel-one/client.ts+schema.ts,revokeZerodhaAccessToken,revokeUpstoxAccessToken.packages/shared:broker_provideronPortfolioPositionSchema(optional);stateon theoauth_codecredentials (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.mdT027. - 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.mdT028.