Track B — Backend wiring for raxx.app customer-facing launch
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
1. Context
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).
2. Invariants
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:
- No stored credentials. No Alpaca API key, session secret, or any other credential may appear in source files, commit history, or build artifacts. All secrets enter Raptor at runtime via Heroku config-vars backed by Infisical (Velvet, ADR 0037–0041). Violation is not a code-review comment; it is a CI-blocking grep.
- Passkeys / WebAuthn only. The auth endpoints that Track B surfaces to raxx.app are registration (#110) and login (#111). No password path. No SMS. No fallback credential of any kind.
- Email is the single post-auth contact channel. Email verification (#112) is required before any account is considered active. No phone number is collected. No push token.
- GDPR by default. Any PII flowing through the wired endpoints (email, IP prefix, user-agent fragment) is subject to retention limits and DSR (data subject request) tooling designed in auth.md. Track B does not add new PII categories; it exposes existing ones to customers, so the existing GDPR design is activated, not extended.
- Paper-first gating. The Alpaca trading proxy is paper-mode only in v1.0. No live-trading code path is opened. Paper-first gate is enforced server-side via
ALPACA_PAPER_API_KEY/ALPACA_PAPER_API_SECRETcredential shape and by keepingALPACA_AUTH_MODE=tradingwith paper URL. See §5. - Audit trail. Every state change that touches identity, session, or money writes an
audit_logrow. 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. - Secrets into infra.
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.envfiles tracked by git.
3. Topology audit
Current wiring
| 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 |
What Raptor currently serves on prod
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 byFLAG_WEBAUTHN_REGISTRATION/api/auth/register/verify— exists, flag-gated byFLAG_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.
4. CORS
Current state
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.
Required state
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.
Verification
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
5. Auth surface — current gaps
What exists
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 beraxx.app(matches ADR 0005 / auth.md §4)WEBAUTHN_RP_NAME— display name, e.g.RaxxWEBAUTHN_ORIGIN— must behttps://raxx.app
These three env vars must be set on raxx-api-prod before FLAG_WEBAUTHN_REGISTRATION is flipped on.
What is missing for v1.0
| 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.
Session cookie posture
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.
6. Trading proxy — Alpaca paper-mode scope for v1.0
Decision
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:
- At v1.0 launch, the operator is the only user.
- Multi-tenant credential isolation is the subject of #183 / #495, which are post-v1.0.
- The Alpaca paper trading API does not involve real money.
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
Required Heroku config-vars on raxx-api-prod
ALPACA_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.
Endpoint scope for v1.0
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 (live broker when credentials present, mock fallback otherwise) |
POST /api/trading/orders |
Submit a paper order |
DELETE /api/trading/orders/{id} |
Cancel an order (live broker when credentials present, mock cancel otherwise) |
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.
Kill-switch
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.
7. Cloudflare Access posture for raxx.app + api.raxx.app
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.
Policy removal procedure (operator steps)
These are Cloudflare dashboard steps, not code changes. No PR is required unless you use Terraform for CF Access management.
For raxx.app:
- Log in to Cloudflare dashboard → Zero Trust → Access → Applications.
- Find the application named
raxx.app(orAntlersdepending on how it was registered). - Click Edit → scroll to Policies section.
- Remove or disable the policy. Do not delete the Application entry itself — retain it without policies, so it can be re-gated instantly if needed (kill-switch).
- Save.
- Confirm:
curl -I https://raxx.appreturns200without a CF Access login redirect.
For api.raxx.app:
- Same path: Zero Trust → Access → Applications → find
api.raxx.app. - Remove the gating policy.
- Retain the Application entry (zero-policy CF Access = no gate, but the entry is recoverable for re-gating).
- Confirm:
curl -I https://api.raxx.app/healthreturns200.
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.
8. Deploy gating — tag pattern mismatch
The mismatch
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.
Decision
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
Change required
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.
9. Sequences
Customer first visit + registration
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
Authenticated trading session
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)
10. Migrations
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:
- Config-var operations (no schema change)
- Workflow file edits (no schema change)
- Runbook documentation (no schema change)
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.
11. Rollout plan
| 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 |
12. Security considerations
- PII collected: Email address (users table), IP prefix (/24 IPv4, /48 IPv6) in sessions table, user-agent fragment in sessions table. No new PII categories introduced by Track B.
- Retention period: Per auth.md §3 and ADR 0003 — sessions 12h rolling; user records soft-deleted on DSR erasure (deleted_at set, not hard-deleted, for audit continuity). Hard deletion after 30-day quarantine.
- DSR deletion path: auth.md §5.4 defines the erasure flow. DSR tooling (#114) is pre-existing sub-card.
- Audit log: Every auth event, session issuance, and order submission writes an
audit_logrow. Retained 7 years (financial audit requirement). Redacted fields: no raw session tokens (hashed), no Alpaca keys (never in DB), IP stored as prefix only. - Credential replay risk: Zero. WebAuthn public keys stored in
webauthn_credentialscannot 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. - Breach notification: Raptor emits structured audit_log rows for
user.pii_accessandsession.issueevents. Automated breach notification is part of ADR 0003 / #114 scope. Track B activates the flow; it does not change the design. - Secrets rotation: All config-vars on
raxx-api-prodare managed by Velvet (ADR 0037–0041). 90-day Alpaca key rotation and 30-day SECRET_KEY rotation are enforced without redeploy. - Kill-switches:
- Trading:
heroku config:set ALPACA_AUTH_MODE='' -a raxx-api-prod >/dev/null 2>&1— disables all trading routes without redeploy. - Auth registration:
heroku config:set FLAG_WEBAUTHN_REGISTRATION=false -a raxx-api-prod >/dev/null 2>&1 - CF Access re-gate: restore the CF Access policy on
api.raxx.appin the Cloudflare dashboard (seconds, no deploy).
13. Open questions
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_KEYandALPACA_PAPER_API_SECRETalready set onraxx-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.appand/orapi.raxx.appcurrently registered as a CF Access Application? The procedure in §7 assumes yes. If CF Access was never applied toraxx.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-stagingalso remove its CF Access gate before B1 staging smoke tests? Currently staging is gated byhttps://raxx-api-staging-1a19fb3873b9.herokuapp.comas 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.