ADR-010: cTrader Integration as a Dedicated Microservice

Status: Accepted

Context

Integrating with the cTrader Open API introduces two categories of complexity that are fundamentally different from what mi-casa or ohlc-service currently handle:

  1. Market data via a shared broker connection. cTrader exposes live bid/ask ticks over a proprietary protobuf/WebSocket protocol. The connection uses OAuth2 app-level auth (a single service account approved by Spotware) and must remain alive continuously — reconnecting causes gaps in the tick stream. This is the same operational argument that motivated ADR-008, but the protocol is more specialised.

  2. Per-user live trading operations. Spotware has approved Mi Casa for multi-account (broker/multi-user) access, meaning each user can link their own cTrader account. Order placement, position queries, and reconciliation each require a per-user OAuth2 access token. Tokens are issued by Spotware, stored in mi-casa’s main Postgres database, and must be fetched at request time — they are not environment variables baked into any service.

These two concerns (shared market-data feed + per-user trading) need to live in the same service because they share the cTrader Open API connection model. Adding them to either mi-casa or ohlc-service would force one of those services to own OAuth2 complexity and broker-protocol knowledge that is entirely out of scope for them.

Wire format note: The cTrader Open API uses a 4-byte big-endian length prefix followed by a protobuf-encoded ProtoMessage { payloadType, payload } envelope. The protobufjs library is used with an inline proto2 schema definition. The ctrader-open-api npm package does not exist and is not used.

OptionProsCons
A — New ctrader-service microserviceClean separation; independently deployable; can own OAuth2 lifecycle and trading without touching mi-casa or ohlc-service; follows proven ohlc-service scaffoldAdditional service to deploy and operate
B — Extend ohlc-serviceNo new deployment unitohlc-service takes on OAuth2 complexity and cTrader protocol knowledge it should not own; impossible to extend for live order execution without a major refactor
C — Direct integration in mi-casa backendSingle deploymentMain app must not own persistent WebSocket connections that survive redeploys; violates the separation established by ADR-008

Decision

Build ctrader-service as a new standalone Bun microservice at port 3872, following the same scaffold as ohlc-service (Bun, Drizzle ORM, TimescaleDB, API-key middleware).

The service owns:

  • App-level OAuth2 lifecycle — one shared Spotware service account used for the market data connection; token refresh is managed internally.
  • Single shared market data WebSocket — connects to the cTrader Open API using the protobuf wire format and maintains one persistent connection for all bid/ask tick ingestion.
  • ctrader_ticks hypertable — raw bid/ask ticks stored in TimescaleDB with a 7-day retention policy.
  • /ws/ticks WebSocket server — broadcasts spot:tick events to subscribers (primarily ohlc-service, which uses them as its spot-price feed).
  • Per-user trading operations — order placement, position queries, and reconciliation. At request time the service fetches the requesting user’s OAuth2 token from mi-casa’s main database, opens a per-account connection to cTrader, and executes the operation. Tokens are never cached in ctrader-service env vars.

Authentication between services uses the same static pre-shared API-key pattern as ohlc-service (X-API-Key header for HTTP; ?key= query parameter for WebSocket upgrades).

Per-user token flow:

  1. User authenticates with Spotware via mi-casa’s OAuth2 callback.
  2. mi-casa stores the user’s cTrader access and refresh tokens in its main Postgres database.
  3. When a trading operation is requested, ctrader-service calls a mi-casa internal endpoint to retrieve the relevant token, then opens a per-account WebSocket to cTrader.

Consequences

Positive

  • cTrader protocol knowledge, OAuth2 handling, and tick storage are fully isolated. Neither mi-casa nor ohlc-service needs to understand the Spotware API.
  • ctrader-service can be restarted, scaled, or replaced independently. mi-casa and ohlc-service degrade gracefully when it is unavailable (missing spot prices, trading endpoints return errors).
  • The shared market data connection is stable across mi-casa redeploys — the same guarantee ADR-008 provides for OHLC candles.
  • Adding new trading capabilities (e.g. stop orders, portfolio reconciliation) requires changes only to ctrader-service, not to mi-casa or ohlc-service.
  • Multi-account support is a first-class citizen: the per-user token model is built in from the start rather than retrofitted.

Negative / Tradeoffs

  • A third service joins the compose stack. Developers must run ctrader-service locally to exercise any cTrader-dependent feature.
  • The per-user token fetch adds one internal HTTP round-trip per trading operation. Acceptable at current scale; would need caching if order frequency grows significantly.
  • The static API-key auth pattern (inherited from ohlc-service) has the same limitations noted in ADR-008: no per-consumer identity, no expiry, no rotation workflow.
  • #248 — ADR issue (this decision)
  • #249 — Scaffold ctrader-service (Bun, Drizzle, TimescaleDB, port 3872)
  • #250 — cTrader protobuf client (proto2 schema, WebSocket connection)
  • #251 — App-level OAuth2 and token refresh
  • #252ctrader_ticks hypertable and 7-day retention policy
  • #253 — Tick ingestion pipeline (market data WebSocket → DB → broadcast)
  • #254/ws/ticks WebSocket server and spot:tick broadcast
  • #255 — ohlc-service integration (consume spot:tick from ctrader-service)
  • #256 — mi-casa integration (route trading requests to ctrader-service)
  • #463 — Per-user OAuth2 token storage in mi-casa DB
  • #464 — Per-account WebSocket connection management
  • #465 — Order placement API
  • #466 — Position queries
  • #467 — Reconciliation
  • #468 — Internal mi-casa endpoint for token retrieval
  • #469 — Docker compose integration and local dev setup
  • ADR-008 — established the microservice pattern this decision extends