The prior Alpaca integration pattern (ADR 0008) authenticated every user via OAuth and stored an access token for every user (ADR 0009 exception to invariant #1). Under the MBT reframe (ADR 0013), most users no longer touch Alpaca for trading — MBT is the paper engine. Alpaca's role narrows, and the token-storage exception can be narrowed with it.
Decision
Alpaca has exactly two surfaces in the Raxx architecture:
Surface 1: Market Data API — one server-side account, shared across all users
One Alpaca account owned by Raxx (not per-user).
Credentials in secret store: ALPACA_MARKET_DATA_KEY + ALPACA_MARKET_DATA_SECRET. 90-day rotation, rotatable without redeploy.
A single MarketDataHub service (new, in backend_v2/api/services/) holds the long-lived REST + WebSocket client and fans out to in-process pub/sub (v1) or Redis pub/sub (at horizontal scale).
Tier gating enforced at the hub boundary (Free delayed + cached, Pro real-time, Pro+ real-time + archive).
No user OAuth is involved. Users never authorize data access.
Surface 2: Live-broker handoff — per-user OAuth, Pro+ opt-in only
Available to Pro+ users (Pro users likewise per pricing matrix, via the same mechanism — Pro promises "Alpaca live + 1 other broker").
Gated by: (a) paper-profitable-for-N-cycles on MBT, or (b) explicit audited override with step-up WebAuthn.
Alpaca OAuth 2.0 auth-code flow, exactly as in superseded ADR 0008.
Stored in new table alpaca_live_connections (not alpaca_connections — the rename makes the narrower scope visible at the schema layer):
alpaca_live_connections
id TEXT PK (uuid v4)
user_id TEXT FK -> users.id ON DELETE CASCADE
scopes TEXT NOT NULL -- space-separated; 'trading data account:write' typical
access_token_ciphertext BLOB NOT NULL -- envelope-encrypted; NO plaintext column
access_token_iv BLOB NOT NULL
access_token_wrapped_dek BLOB NOT NULL
kms_key_id TEXT NOT NULL
refresh_token_ciphertext BLOB NULL -- if Alpaca issues one; same envelope pattern
refresh_token_iv BLOB NULL
refresh_token_wrapped_dek BLOB NULL
issued_at TIMESTAMP NOT NULL
expires_at TIMESTAMP NOT NULL
last_used_at TIMESTAMP NULL
needs_reauth BOOLEAN NOT NULL DEFAULT 0
revoked_at TIMESTAMP NULL
CHECK (length(access_token_ciphertext) > 0)
UNIQUE (user_id) WHERE revoked_at IS NULL -- one active live connection per user in v1
Step-up WebAuthn on every live order submit in v1.
The exception to invariant #1 (ADR 0002) applies only to rows in this table.
Four guardrails (carried forward from ADR 0009, still required)
Scope + blast-radius caps. Minimum scope per feature; paper-first gate read from MBT state, not from Alpaca paper (Alpaca paper is no longer on our roadmap).
Encryption at rest. Envelope encryption with KMS-backed wrapping key. DB has no plaintext column.
Rotation + short-lived-by-default. No life extension beyond Alpaca's issuance; 90-day re-consent cap regardless.
Audit + revocation. Every mint, refresh, use, revoke writes an audit_log row. Kill switch ALPACA_LIVE_HANDOFF_DISABLED=1. Mass-revoke script narrows to this table only.
Consequences
Positive
Invariant #1 fully satisfied for every user who does not enroll live-handoff. That is the vast majority — free, most Pro, and Pro+ users who stay in paper.
Exception scope is visible. The table name (alpaca_live_connections) and the narrower row count make the exception surface small and auditable.
Broker-API path permanently dropped. Raxx does not become a broker-dealer; no FINRA clearing, no omnibus accounting, no KYC obligation beyond our own account verification.
Market-data cost is fixed, not per-user. One Alpaca data account fee, spread across all users. Economics are predictable.
Live-handoff enrollment is an explicit user choice — no accidental enrollment, no default-on. Matches the regulatory posture (user-directed execution).
Negative
We still have an invariant exception. Narrower, but it exists. ADR 0002 CI-grep list is extended with alpaca_live_connections' forbidden columns (plaintext access_token, plaintext refresh_token).
Live-handoff engineering cost is still real. OAuth flow, token refresh, step-up on every submit, mass-revoke — all needed for the minority that enrolls.
One Alpaca data account is a capacity ceiling at scale. Mitigation: second key / entitlement upgrade. Flagged in multi-tenant-alpaca.md §8.
Alpaca remains a critical dependency for market data even though it is not for trading. A market-data outage degrades MBT (kill-switch falls back to last-known-good cache with user banner).
Neutral
Paper-first gating moves from "Alpaca paper profitability" to "MBT cycle profitability." Semantically identical; implementation is against our DB now.
Second broker for Pro tier (IBKR / tastytrade per pricing matrix) is a future ADR using the same live-handoff pattern. Out of scope for this ADR.
Alternatives considered
Keep per-user OAuth for market data
Rejected. No upside — users don't need their own data entitlement when Raxx can serve everyone from one account at lower cost. Would resurrect the invariant-exception problem that MBT solves.
Multiple Alpaca data accounts, sharded by user
Considered, deferred. At v1 user counts, one account suffices. Revisit at scale; the sharding pattern does not require architecture change at v1.
Broker-partnered live-handoff instead of per-user OAuth
Rejected. A "Raxx has a partnership with Alpaca Broker API" path requires BD status. Permanently off-roadmap per ADR 0013.
No live-handoff at all (MBT-only product)
Considered, rejected. Pro+ users paying $79/month reasonably expect to route real orders somewhere. MBT-only with a "copy to your broker manually" CSV export is product-hostile for that persona. Live-handoff stays.
Compliance checklist
[x] Invariant #1 exception is explicitly scoped and visible (table name + ADR).
[x] CI-grep list extended for alpaca_live_connections forbidden columns.
[x] Envelope encryption, KMS-rotatable without re-enrollment.
[x] Mass-revoke script targets only this table.
[x] Audit trail on every connection lifecycle event + every live order submit.