Raxx · internal docs

internal · gated ↑ index

ADR 0014 — Alpaca scope: server-side market-data account + Pro+ live-broker handoff only

Status: Proposed Date: 2026-04-22 Deciders: product owner (user), software-architect Related: ADR 0002 (no stored credentials), ADR 0013 (MBT), docs/architecture/multi-tenant-alpaca.md, docs/architecture/mbt-paper-trading-engine.md Supersedes (in part): ADR 0008 (OAuth integration mode) — now narrower scope; ADR 0009 (OAuth token posture) — now narrower scope Parent epic: #183

Context

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

Surface 2: Live-broker handoff — per-user OAuth, Pro+ opt-in only

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

Four guardrails (carried forward from ADR 0009, still required)

  1. 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).
  2. Encryption at rest. Envelope encryption with KMS-backed wrapping key. DB has no plaintext column.
  3. Rotation + short-lived-by-default. No life extension beyond Alpaca's issuance; 90-day re-consent cap regardless.
  4. 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

Negative

Neutral

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

Revisit when