Raxx · internal docs

internal · gated ↑ index

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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_SECRET credential shape and by keeping ALPACA_AUTH_MODE=trading with paper URL. See §5.
  6. Audit trail. Every state change that touches identity, session, or money writes an 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.
  7. 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 .env files 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:

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):

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.

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:

  1. At v1.0 launch, the operator is the only user.
  2. Multi-tenant credential isolation is the subject of #183 / #495, which are post-v1.0.
  3. 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
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.

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:

  1. Log in to Cloudflare dashboard → Zero Trust → Access → Applications.
  2. Find the application named raxx.app (or Antlers depending on how it was registered).
  3. Click Edit → scroll to Policies section.
  4. 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).
  5. Save.
  6. Confirm: curl -I https://raxx.app returns 200 without a CF Access login redirect.

For api.raxx.app:

  1. Same path: Zero Trust → Access → Applications → find api.raxx.app.
  2. Remove the gating policy.
  3. Retain the Application entry (zero-policy CF Access = no gate, but the entry is recoverable for re-gating).
  4. Confirm: 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.


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:

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


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.

  1. (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.

  2. (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.

  3. (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.

  4. (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.

  5. (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.