| name | heater-appb-deploy-ops |
| description | Use when deploying HEATER App B, changing Railway or Vercel env vars/service config, diagnosing live 502s/timeouts/blank data after deploy, operating the Yahoo-token or Statcast relays, or driving Railway/Vercel headlessly from a terminal. |
App B Deploy & Ops — Railway + Vercel
The services
| Service | What | Config |
|---|
Railway celebrated-respect (App B) | FastAPI API — THE product backend | service setting railwayConfigFile=railway.api.toml → builds api/Dockerfile, runs sh start-api.sh; healthcheck /healthz; 1 replica (hard invariant: in-process SQLite writer) |
Railway heaterv100-production (App A) | Streamlit fallback | root Dockerfile/railway.toml — leave alone |
Vercel heater-v1-0-1 | Next.js frontend | proxies /api/* server-side → App B |
Deploy = push to master — Railway auto-deploys App B. Each deploy re-runs bootstrap: ~10 min until projections/pool are warm; judge "did the fix work" only after re-bootstrap (the Marcel fix looked dead until the re-bootstrap regenerated projections). Vercel deploys are a separate owner/CLI step.
start-api.sh: first-boot Yahoo token seed → background scheduler loop (HEATER_SCHEDULER_BOOT=1, HEATER_SCHEDULER_IS_OWNER=1, sole DB writer) → exec python -m uvicorn api.main:create_app --factory --port $PORT.
Env inventory (App B unless noted)
- Auth:
CLERK_ISSUER, CLERK_AUDIENCE (setting ISSUER flips the login gate live).
- Billing:
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRO_PRICE_ID — intentionally UNSET on the beta (free friends). Setting them activates the 402 paywall.
- Writes/admin:
HEATER_API_WRITE_TOKEN, HEATER_ADMIN_CLERK_IDS.
- Relays:
HEATER_TOKEN_RELAY_URL + HEATER_RELAY_KEY (Yahoo token pull — Yahoo blocks refresh from Railway IPs, so the owner's PC refreshes and gists it, Task \HEATER Yahoo Relay every 30 min; to switch Yahoo apps: local oob re-auth → run scripts/yahoo_token_relay.py once → App B pulls within ~8 min, NO redeploy); HEATER_STATCAST_RELAY_URL (Savant xwOBA, Task \HEATER Statcast Relay daily — disable before monetization, scraped data is commercial-use-prohibited).
- AI:
HEATER_AI_ADMIN_KEY_<PROVIDER> ×6 (anthropic/openai/gemini/deepseek/xai/openrouter). Key VALUES live only in Railway, gitignored .env, and C:\Users\conno\.heater_ai_keys — never in git or docs.
- Paths/DB:
HEATER_API_DB_PATH (api_state.db), HEATER_DB_PATH, DATABASE_URL (unset = SQLite; Postgres wiring is owner-gated B2.2 — get_connection() raises NotImplementedError on non-SQLite by design).
- Vercel:
HEATER_API_BASE (App B URL — server-side only), NEXT_PUBLIC_HEATER_LIVE=1, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (derivable: pk_<env>_ + base64(<frontend-api-host>$)). HEATER_API_CORS_ORIGINS NOT needed — the proxy makes calls same-origin.
Headless ops
Railway/Vercel CLIs reject the available tokens — drive the APIs directly: Railway GraphQL backboard.railway.app/graphql/v2 with Authorization: Bearer $(cat /c/Users/conno/.railway_token) plus a browser User-Agent (Cloudflare); helper scripts C:\Users\conno\railway_gql.py / vercel_api.py (tokens are ephemeral; IDs are in the token file's notes). me may be unauthorized — query projects.
Failure story — the 502 that was two bugs
2026-06-22: App B returned 502. Fix 1: Railway serviceDomain targetPort 8000→8080 mismatch. Then it "worked" but served the WRONG APP — the service was building the root Streamlit Dockerfile; only adding railway.api.toml + the railwayConfigFile service setting made it run FastAPI. Rule: after any service-config change, verify /healthz AND one real endpoint's shape, not just HTTP 200.
When NOT to apply
Owner-gated flips (Stripe live, Clerk prod/custom domain, Postgres provisioning, leaguemate team assignment — the LAST launch step): surface to owner, don't execute. Code-level debugging → heater-appb-debugging.