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:
-
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.
-
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.
| Option | Pros | Cons |
|---|---|---|
A — New ctrader-service microservice ✅ | Clean separation; independently deployable; can own OAuth2 lifecycle and trading without touching mi-casa or ohlc-service; follows proven ohlc-service scaffold | Additional service to deploy and operate |
| B — Extend ohlc-service | No new deployment unit | ohlc-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 backend | Single deployment | Main 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_tickshypertable — raw bid/ask ticks stored in TimescaleDB with a 7-day retention policy./ws/ticksWebSocket server — broadcastsspot:tickevents 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:
- User authenticates with Spotware via mi-casa’s OAuth2 callback.
- mi-casa stores the user’s cTrader access and refresh tokens in its main Postgres database.
- 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.
Related
- #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
- #252 —
ctrader_tickshypertable and 7-day retention policy - #253 — Tick ingestion pipeline (market data WebSocket → DB → broadcast)
- #254 —
/ws/ticksWebSocket server andspot:tickbroadcast - #255 — ohlc-service integration (consume
spot:tickfrom 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