Status: Draft
Owner: software-architect
Date: 2026-05-03
Parent epic: #94 — Production Release v1.0
Related ADRs: 0047, 0048, 0049
Surface class: Class 1 (customer-facing public) per ADR 0031
Customers visiting https://raxx.app hit a React SPA (Antlers) served by Cloudflare Pages (raxx-app CF Pages project). That SPA calls https://api.raxx.app/api/*. The backend (Raptor) runs on Heroku as raxx-api-prod. Before any customer can use the product, four infrastructure connections must be wired correctly and two process gaps must be closed. This document audits the current state, decides the gaps, and produces the implementation sub-cards for feature-developer.
Track B is deliberately narrow: it covers only what is needed for the first customer-capable wiring. It does not include multi-tenant architecture (#183 / #495), onboarding wizard (#469), or MBT paper engine (#256).
The following project-level invariants apply to every sub-card in this track. Feature-developer must treat each as a hard constraint, not a goal:
ALPACA_PAPER_API_KEY / ALPACA_PAPER_API_SECRET credential shape and by keeping ALPACA_AUTH_MODE=trading with paper URL. See §5.audit_log row. The auth design (auth.md §3) already defines this table. Track B only needs to confirm the table migration is in place before B1-level routes are reachable from the internet.FRONTEND_ORIGIN, SECRET_KEY, ALPACA_PAPER_API_KEY, ALPACA_PAPER_API_SECRET, SENTRY_DSN, and the WebAuthn env vars (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, WEBAUTHN_ORIGIN) all sit in Heroku config-vars. Never in .env files tracked by git.| Surface | Host | Mechanism | Gap? |
|---|---|---|---|
raxx.app |
Cloudflare Pages (raxx-app) |
deploy-antlers.yml pushes CF Pages on tag |
Trigger tag pattern mismatch — see §6 |
api.raxx.app |
Heroku raxx-api-prod |
DNS CNAME → Heroku; deploy-heroku.yml on workflow_dispatch production |
CORS not yet set for https://raxx.app — see §4 |
/api/auth/* |
raxx-api-prod |
Blueprint registered, flag-gated | Login (#111) and session (#112) not yet shipped |
/api/trading/* |
raxx-api-prod |
Blueprint registered; reads ALPACA_PAPER_API_KEY from config-vars |
Env vars may not be set on prod; verify during B3 |
CF Access on api.raxx.app |
Cloudflare | Access Application policy gates the origin | Must be removed for customers to reach the API — see §7 |
The blueprint registration in backend_v2/api/__init__.py registers all route groups under /api. The routes that are relevant to v1.0 customer wiring:
/api/auth/register/options — exists, flag-gated by FLAG_WEBAUTHN_REGISTRATION/api/auth/register/verify — exists, flag-gated by FLAG_WEBAUTHN_REGISTRATION/api/auth/login/* — not yet shipped (issue #111)/api/auth/email/verify — not yet shipped (issue #112)/api/trading/* — exists; reads Alpaca creds from env/api/backtest/* — exists/api/historical-data/* — exists/api/market-data/* — exists/health — exists (liveness)Missing for v1.0 end-to-end customer path: login endpoint (#111) and email verification endpoint (#112). Those are pre-existing sub-cards. Track B's B2 sub-card is about making the existing and incoming auth routes reachable from raxx.app, not reimplementing them.
backend_v2/api/__init__.py reads FRONTEND_ORIGIN env var (comma-separated) and passes it to flask_cors.CORS. Default fallback is http://localhost:3000. On production Heroku, if FRONTEND_ORIGIN is not set, CORS will reject every request from https://raxx.app because the browser-sent Origin does not match http://localhost:3000.
FRONTEND_ORIGIN must be set on raxx-api-prod Heroku config-vars to at least https://raxx.app. For staging parity, raxx-api-staging should carry https://raxx-app.pages.dev (the CF Pages staging slot URL that deploy-antlers.yml uses for non-tag pushes).
The code change required is zero — the config reading is already correct. This is a config-var ops task.
# Production (raxx-api-prod)
FRONTEND_ORIGIN=https://raxx.app
# Staging (raxx-api-staging)
FRONTEND_ORIGIN=https://raxx-app.pages.dev
If multiple preview origins are needed in future (PR preview URLs), append comma-separated. The existing CORS init already handles that.
After setting the config-var, a preflight OPTIONS request to https://api.raxx.app/api/system/status with Origin: https://raxx.app must return Access-Control-Allow-Origin: https://raxx.app. See B1 acceptance criteria.
ADR: 0047
Registration routes ship (#110, merged). They are flag-gated by FLAG_WEBAUTHN_REGISTRATION. The WebAuthn config reads these env vars (from webauthn_service.py):
WEBAUTHN_RP_ID — must be raxx.app (matches ADR 0005 / auth.md §4)WEBAUTHN_RP_NAME — display name, e.g. RaxxWEBAUTHN_ORIGIN — must be https://raxx.appThese three env vars must be set on raxx-api-prod before FLAG_WEBAUTHN_REGISTRATION is flipped on.
| Route | Issue | Status |
|---|---|---|
POST /api/auth/login/options |
#111 | Open — not yet shipped |
POST /api/auth/login/verify |
#111 | Open — not yet shipped |
POST /api/auth/email/verify |
#112 | Open — not yet shipped |
POST /api/auth/logout |
#111 (or #112) | Open |
GET /api/auth/session |
#111 or new | Not scoped explicitly |
Track B does not reopen these issues. It creates B2, which wires the already-built and incoming auth endpoints to be reachable from raxx.app (CORS + feature flag + env vars). B2 is a coordination card, not an implementation card for the auth logic itself.
Sessions issued by #111 will set Set-Cookie with SameSite=None; Secure because the SPA origin (raxx.app) and the API origin (api.raxx.app) are different subdomains. supports_credentials=True is already set in the CORS init. The cookie domain must be .raxx.app to span both. This is a detail for #111 but must be confirmed before B2 is accepted.
v1.0 is single-operator, paper-mode only. "Customer enters their own Alpaca paper API keys" is deferred to post-v1.0 (#183). At v1.0, Raptor uses a single set of operator-owned Alpaca paper credentials, shared across all authenticated users. This is acceptable because:
The "user enters their own Alpaca paper API keys" story is preserved as an explicit open question in §9 and tracked by #183.
ADR: 0049
raxx-api-prodALPACA_AUTH_MODE=trading
ALPACA_PAPER_API_KEY=<operator paper key from Infisical /MooseQuest/alpaca/>
ALPACA_PAPER_API_SECRET=<operator paper secret from Infisical /MooseQuest/alpaca/>
ALPACA_PAPER_BASE_URL=https://paper-api.alpaca.markets
ALPACA_DATA_BASE_URL=https://data.alpaca.markets
These must be set via heroku config:set ... >/dev/null 2>&1 (stdout silenced per memory feedback_heroku_config_set_echoes_secrets.md). Values come from Infisical; the console Velvet rotation cycle governs their 90-day refresh.
The following existing trading routes are required for a working paper-mode session:
| Route | Purpose |
|---|---|
GET /api/trading/mode |
Returns current paper/live mode |
GET /api/trading/account |
Alpaca paper account summary |
GET /api/trading/positions |
Open positions |
GET /api/trading/orders |
Open + recent orders |
POST /api/trading/orders |
Submit a paper order |
DELETE /api/trading/orders/{id} |
Cancel a paper order |
GET /api/trading/readiness |
Pre-flight credential check |
All of these already exist in backend_v2/api/routes/trading.py. No new routes are required for v1.0 paper-mode. B3 is a verification + env-var-setting task, not an implementation task.
If the paper API behaves unexpectedly, the kill-switch is heroku config:set ALPACA_AUTH_MODE='' -a raxx-api-prod >/dev/null 2>&1, which causes resolve_trading_credentials to return credentials_present: false and all trading routes to return 503 — credentials not configured. No redeploy required.
Per ADR 0031, raxx.app is a Class 1 surface (customer-facing public). CF Access must not gate customer-facing surfaces. The current state (both raxx.app and api.raxx.app behind CF Access) blocks every unauthenticated visitor.
These are Cloudflare dashboard steps, not code changes. No PR is required unless you use Terraform for CF Access management.
For raxx.app:
raxx.app (or Antlers depending on how it was registered).curl -I https://raxx.app returns 200 without a CF Access login redirect.For api.raxx.app:
api.raxx.app.curl -I https://api.raxx.app/health returns 200.Important: After removing CF Access from api.raxx.app, the FLAG_ENFORCE_CF_ORIGIN feature flag must remain false (its current default) on raxx-api-prod. Setting it to true would cause Raptor to reject requests that don't carry CF-Connecting-IP — but CF Access removal does not remove CF proxying. Cloudflare will still inject CF-Connecting-IP on all requests routed through the CDN. The flag is safe to enable post-launch for defence-in-depth (Phase 2 of the origin guard rollout described in cloudflare_origin_guard.py).
Sub-card B5 tracks this procedure as a runbook document + operator verification step.
deploy-antlers.yml triggers on tags matching v*.*.* (bare semver, e.g. v1.10.1).
release-please-config.json sets package-name: trademaster-api for the frontend/trademaster_ui package. Release-please produces tags in the format trademaster-api-v1.10.1.
These patterns do not intersect. A release-please-generated tag will never trigger deploy-antlers.yml.
Fix the trigger in deploy-antlers.yml to match trademaster-api-v*.*.*. Do not change the release-please config, because the tag format it produces is the correct multi-package convention and other workflows (notably release.yml post-release verification) already expect trademaster-api-v*.*.*.
ADR: 0048
In deploy-antlers.yml:
# Before
tags: ['v*.*.*']
# After
tags: ['trademaster-api-v*.*.*']
The environment URL logic in the deploy job already uses startsWith(github.ref, 'refs/tags/v') for the production environment and the REACT_APP_API_URL toggle. This must also change to startsWith(github.ref, 'refs/tags/trademaster-api-v'). Sub-card B4 handles both.
Additionally, the deploy job publishes with --branch=${{ github.ref_name }}. For a tag trademaster-api-v1.10.1, github.ref_name is trademaster-api-v1.10.1. CF Pages will interpret this as the branch name for the deployment. This is cosmetically non-ideal but functionally correct — CF Pages production deployments are controlled by the --project-name flag and CF Pages' own alias configuration, not by the branch name in the Wrangler command.
sequenceDiagram
participant B as Browser
participant CF as CF Pages (raxx.app)
participant R as Raptor (api.raxx.app)
participant A as Alpaca Paper API
B->>CF: GET https://raxx.app/
CF-->>B: 200 index.html (React SPA)
B->>R: OPTIONS https://api.raxx.app/api/auth/register/options
R-->>B: 200 + CORS headers (Origin: https://raxx.app allowed)
B->>R: POST /api/auth/register/options {email, bootstrap_token}
R-->>B: 200 PublicKeyCredentialCreationOptions
Note over B: Browser invokes platform authenticator
B->>R: POST /api/auth/register/verify {email, credential}
R-->>B: 200 {user_id, credential_id, needs_email_verification: true}
R->>B: (async) Email verification link sent
sequenceDiagram
participant B as Browser
participant R as Raptor (api.raxx.app)
participant A as Alpaca Paper API
B->>R: POST /api/auth/login/verify {credential} — cookie issued
R-->>B: 200 Set-Cookie: session=<token>; Domain=.raxx.app; SameSite=None; Secure
B->>R: GET /api/trading/account (Cookie: session=<token>)
R->>A: GET /v2/account (ALPACA_PAPER_API_KEY)
A-->>R: 200 account JSON
R-->>B: 200 account JSON
B->>R: POST /api/trading/orders {symbol, qty, side, type}
R->>A: POST /v2/orders
A-->>R: 200 order
R-->>B: 200 order (+ audit_log row written)
No schema changes are required in Track B. The auth migrations (#110) create the users, webauthn_credentials, sessions, and audit_log tables. Track B sub-cards are:
If auth migration from #110 has not yet run on raxx-api-prod, B2 acceptance criteria must include verifying the migration applied. Feature-developer running B2 should check heroku run python3 -c "from db import init_persistence; ..." or equivalent migration-status command.
| Phase | What | Gate |
|---|---|---|
| Dark | Set CORS config-var on raxx-api-staging; smoke test CORS from raxx-app.pages.dev |
B1 staging AC |
| Dark | Set WebAuthn env vars + enable flag on staging | B2 staging AC |
| Dark | Verify Alpaca paper env vars on staging | B3 staging AC |
| Dark | Fix deploy-antlers.yml tag trigger on staging | B4 AC |
| Flag → Beta | Remove CF Access from raxx.app and api.raxx.app |
B5 — operator step |
| Beta | Fire trademaster-api-v1.10.x tag; confirm deploy-antlers fires |
B4 verification |
| GA | Remove CF Access policies; confirm /health and SPA are public |
B5 operator verification |
audit_log row. Retained 7 years (financial audit requirement). Redacted fields: no raw session tokens (hashed), no Alpaca keys (never in DB), IP stored as prefix only.webauthn_credentials cannot impersonate the user — the private key never leaves the authenticator. Alpaca paper keys stored in Heroku config-vars are not in the DB, not in source code, not in logs.user.pii_access and session.issue events. Automated breach notification is part of ADR 0003 / #114 scope. Track B activates the flow; it does not change the design.raxx-api-prod are managed by Velvet (ADR 0037–0041). 90-day Alpaca key rotation and 30-day SECRET_KEY rotation are enforced without redeploy.heroku config:set ALPACA_AUTH_MODE='' -a raxx-api-prod >/dev/null 2>&1 — disables all trading routes without redeploy.heroku config:set FLAG_WEBAUTHN_REGISTRATION=false -a raxx-api-prod >/dev/null 2>&1api.raxx.app in the Cloudflare dashboard (seconds, no deploy).These must be answered before the relevant sub-card can be claimed by feature-developer. Cards marked "(B-sub-card)" are blocked until resolved.
(B2 — blocks login) Has issue #111 (WebAuthn login endpoint) shipped to raxx-api-prod? If not, B2 depends on #111 landing first. Track B cannot wire what doesn't exist.
(B3 — blocks trading) Are ALPACA_PAPER_API_KEY and ALPACA_PAPER_API_SECRET already set on raxx-api-prod? If yes, B3 is a verification-only task. If no, they must be fetched from Infisical /MooseQuest/alpaca/ before B3 can be marked done.
(B5 — blocks public traffic) Is raxx.app and/or api.raxx.app currently registered as a CF Access Application? The procedure in §7 assumes yes. If CF Access was never applied to raxx.app, B5 is a no-op for that surface. Operator (Kristerpher) to confirm in B5 comments.
(Post-v1.0 — informational) When does "user enters their own Alpaca paper API keys" become a v1.1 priority? This is the per-customer credential shape question. It is tracked by #183. No sub-card in Track B blocks on this answer.
(Informational) Should raxx-api-staging also remove its CF Access gate before B1 staging smoke tests? Currently staging is gated by https://raxx-api-staging-1a19fb3873b9.herokuapp.com as the Heroku URL — CF Access on the staging custom subdomain (if one exists) would affect B1 staging work. Operator to confirm staging CF Access posture.