← All docs

Broker Platform

Mirrored from docs/*.md in the Aegis repository
Broker Platform

» The single broker integration layer for the entire ecosystem. One OAuth
» callback endpoint for every broker. Applications never implement broker
» OAuth directly. The callback reads the signed OAuth state to determine
» WHICH application initiated the flow, then redirects the user back to that
» app with its workflow restored. Token exchange, refresh, broker sessions,
» WebSocket lifecycle and the capability registry belong exclusively to this
» platform.

**Status: 🟡 Implemented (Aegis migrated; FlipTrade registered, migration documented — prod untouched)**


0. THE SEVEN PLATFORM-OWNED CAPABILITIES

  # | Capability | Owner in this module
  1 | OAuth initiation | `runtime.ts` → `startFlow()`
  2 | Unified callback (one endpoint, all brokers/apps) | `runtime.ts` → `handleCallback()`
  3 | Token exchange | providers (`zerodha.ts` / `groww.ts`) → `exchange()`
  4 | Refresh tokens | providers → `refresh()`; `restoreSession()` (Kite: no refresh token — re-auth, reported honestly)
  5 | Broker sessions | `sessions.ts` (per app+broker, IndexedDB + fallback)
  6 | WebSocket lifecycle | `websocket.ts` → `WebSocketLifecycle` (connect, subscribe, reconnect backoff, staleness)
  7 | Broker capability registry | `capabilities.ts` → `brokerCapabilityRegistry`

Applications only call `startFlow()`, `restoreSession()`, and the capability
registry. Everything else is platform-owned.

---


1. WHY THIS EXISTS

Previously each application implemented broker OAuth itself:

• Aegis: built the Kite login URL + handled `/auth/callback` + exchanged the
  token + stored the session (in `src/plugins/broker-zerodha.ts`,
  `src/app/auth/callback/page.tsx`, `src/platform/broker/*`).
• FlipTrade: did the same in FastAPI (`api.thytrade.xyz/api/v1/auth/zerodha/callback`).

Every future product would have copied this again. That is an architectural
duplication: broker OAuth is a **platform capability**, not an application
capability. This module centralizes it.


2. ARCHITECTURE

┌─────────────┐   ┌─────────────┐   ┌──────────────┐
│   Aegis     │   │  FlipTrade  │   │ Future apps  │   ← applications
└──────┬──────┘   └──────┬──────┘   └──────┬───────┘
       │ startFlow()     │ startFlow()     │ startFlow()
       └─────────────────┴─────────────────┘
                        ▼
        ┌───────────────────────────────────┐
        │   BROKER AUTH PLATFORM            │  src/platform/broker-auth/
        │  · app registry (who may auth)    │
        │  · broker providers (zerodha/groww)│
        │  · signed expiring state          │  state.ts
        │  · startFlow / handleCallback     │  runtime.ts
        │  · session store (per-app)        │  sessions.ts
        │  · reconnect / expiry logic       │
        └──────────────┬────────────────────┘
                       │ login URL + signed state
                       ▼
        ┌─────────────────────────────┐
        │  Broker (Kite / Groww / …)  │
        └──────────────┬──────────────┘
                       │ redirect → THE ONE callback
                       │   (Kite: redirect_params echo;
                       │    OAuth: state param)
                       ▼
        ┌───────────────────────────────────┐
        │  /auth/broker/callback            │  ← one endpoint, all brokers
        │  1. verify state (sig/expiry)     │
        │  2. WHICH app? state.app          │
        │  3. exchange token (provider)     │
        │  4. persist session (platform)    │
        │  5. redirect → app.homeUrl+path   │
        └───────────────────────────────────┘

  Key decisions

  Decision | Why
  State is **signed + expiring** (HMAC-SHA256, 10 min TTL) | Routing to the originating app must not be forgeable (open-redirect protection); stale callbacks must fail
  State carries `{app, broker, redirectTo, nonce, exp, sig}` | The callback knows exactly which app + workflow to restore
  Kite uses **`redirect_params`**, standard OAuth uses **`state`** | Kite Connect has no `state` param; `redirect_params` is its documented echo mechanism. The provider contract abstracts the transport (`stateTransport`)
  Sessions keyed **per app + broker** in one store | Multiple apps share the platform without colliding
  Credentials staged **per app + broker** in localStorage, 30 min TTL | Callback is a separate page load; the exchange needs the secret, but it never travels in the state
  `createPlatformBrokerStorage(appId)` adapts the store to the SDK `BrokerStorage` | The broker runtime (execution layer) reads sessions through the platform — one source of truth
  FlipTrade **registered but prod untouched** | Requirement: existing FlipTrade functionality continues unchanged; migration documented below


3. MODULE LAYOUT

src/platform/broker-auth/
  types.ts            BrokerApp, BrokerAuthProvider, state payload, results
  state.ts            createState / verifyState (HMAC-SHA256, expiry, nonce)
  registry.ts         registerApp / registerProvider + built-ins (aegis, fliptrade; zerodha, groww)
  sessions.ts         per-app session store (IndexedDB + memory fallback) + BrokerStorage adapter
  capabilities.ts     PLATFORM-OWNED capability registry (brokerCapabilityRegistry)
  websocket.ts        PLATFORM-OWNED WebSocket lifecycle (WebSocketLifecycle)
  providers/
    zerodha.ts        Kite: login URL w/ redirect_params, checksum exchange, 06:00 IST expiry
    groww.ts          Standard OAuth: `state` param, code exchange, refresh_token
  runtime.ts          startFlow / handleCallback / manualExchange / restoreSession
  index.ts            public surface: `import { … } from "@/platform/broker-auth"`


4. PUBLIC API (WHAT AN APP CALLS)

import { startFlow, handleCallback, manualExchange, restoreSession } from "@/platform/broker-auth";

// 1. Initiate — app names itself + its workflow-restore path.
const { loginUrl } = await startFlow({
  app: "aegis",
  broker: "zerodha",              // platform broker id (not the runtime adapter id)
  redirectTo: "/app?broker=zerodha",
  credentials: { apiKey, apiSecret },
});

// 2. User authorizes at the broker. Broker redirects to the ONE callback.
//    (Aegis hosts it at /auth/broker/callback — same page for every broker.)

// 3. Callback handler — inside the endpoint only.
const result = await handleCallback({ broker, requestToken, state });
if (result.ok) window.location.replace(result.redirectUrl); // back to app + workflow

// 4. App-side restore — never touch tokens.
const r = await restoreSession("aegis", "zerodha");
// r.status: "connected" | "expired" | "none"

// 5. Paste-token fallback (no redirect) — exchange still platform-owned.
const r2 = await manualExchange({ app: "aegis", broker: "zerodha", credentials, requestToken });

Apps do NOT: build broker URLs, read `request_token`, compute checksums,
store access tokens, handle expiry, or reconnect. The platform does all of it.


5. KITE-SPECIFIC NOTES (VERIFIED AGAINST OFFICIAL DOCS)

• Login endpoint: `https://kite.zerodha.com/connect/login?v=3&api_key=<key
… (truncated — see repository docs/*.md for the full document)
This is a condensed in-app view. The full document lives in the repository at docs/broker_platform.md.