Tasks 015 — Env-selected API base URL (drop the same-origin proxy)
Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.
Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.
Global Constraints in plan.md apply to every task and are not repeated per task.
Ponytail — restate in EVERY implementer dispatch
Ponytail (mode: full) is active for this session but does not reach subagents — the subagent-driven-development controller builds their context fresh. So every dispatch prompt must carry this block verbatim, or the implementer writes code without the ladder's constraints ([[propagate-ponytail-to-subagents]]):
You are a lazy senior developer — lazy means efficient, not careless; the best code is the code never written. Before writing code, stop at the first rung that holds: (1) does it need to exist at all (YAGNI); (2) already in this codebase — reuse the helper/util/pattern; (3) stdlib does it; (4) native platform feature covers it; (5) an already-installed dependency solves it; (6) can it be one line; (7) only then, the minimum code that works. Rules: no unrequested abstractions (no interface with one impl, no config for a value that never changes), no scaffolding "for later", deletion over addition, boring over clever, fewest files, shortest working diff — but only once you understand the problem (trace the real flow first; a small diff in the wrong place is a second bug). Never lazy about: understanding the problem, input validation at trust boundaries, error handling, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind (here: the task's test). Output: code first, then at most three short lines (what was skipped, when to add it); if the explanation is longer than the code, delete the explanation.
The task brief remains the source of requirements; this block only governs how much code answers it.
T1 — API serves every route under /api and allows the web origin
Satisfies: R1, R2, P1 #1, SC2.
The runtime app (main.ts) gains the global prefix and CORS; the doc emitter (generate-openapi.ts) is left untouched so openapi.json stays origin-relative (that isolation is the whole point of research V3 — do not add the prefix there).
- [ ] RED: in
apps/api/src/main.bootstrap.test.ts, update the mirror to the new bootstrap — import the allow-list, applysetGlobalPrefix('api')+enableCors({ origin: CORS_ALLOWLIST }), set Swagger atapi/docs— and assert:GET /api/health→200;GET /api/docs→200text/html; aGETcarryingOrigin: http://localhost:5173returnsaccess-control-allow-origin: http://localhost:5173; a preflightOPTIONS /api/legendarieswith that Origin is answered (not404). Watch it fail against today's/api-docs, no-prefix app. - [ ] GREEN: add
apps/api/src/config/cors.tsexportingCORS_ALLOWLIST(readonly string[]—http://localhost:5173,https://gw2priory-l21x-7m5b.onrender.com). Inmain.tsaddapp.setGlobalPrefix('api'),app.enableCors({ origin: CORS_ALLOWLIST }), and changeSwaggerModule.setup('api-docs', …)→SwaggerModule.setup('api/docs', …). Do not editgenerate-openapi.ts. - [ ] REFACTOR: only with the test green. Confirm
pnpm --filter @gw2priory/api generate:openapistill emits a byte-identicalopenapi.json(paths stay/health,/legendaries,/recipe-graph/{itemId}) — the existinggenerate-openapi.test.tsguards this. - [ ] Confirm the test has teeth — remove
setGlobalPrefix(health 404s) and theenableCorsline (no ACAO), watch each assertion fail, restore. - [ ] Commit.
Verified by: apps/api/src/main.bootstrap.test.ts (prefix, CORS, Swagger path); generate-openapi.test.ts (openapi.json unchanged).
T2 — Web resolves its API origin from VITE_APP_ENV, or fails loudly
Satisfies: R4, P1 #3, SC4.
- [ ] RED: add
apps/web/src/api/__tests__/apiBase.test.ts—resolveApiOrigin('dev')===http://localhost:3000;resolveApiOrigin('prod')===https://gw2priory-api-l21x-lbn5.onrender.com;resolveApiOrigin(undefined)andresolveApiOrigin('staging')each throw. Watch it fail (no module yet). - [ ] GREEN: add
apps/web/src/api/apiBase.ts— a typed{ dev, prod }const map andresolveApiOrigin(env: string | undefined): stringthat returns the mapped origin or throwsError(\Invalid VITE_APP_ENV: ${env}`); exportAPI_ORIGIN = resolveApiOrigin(import.meta.env.VITE_APP_ENV). Addapps/web/src/vite-env.d.tsaugmentingImportMetaEnvwithreadonly VITE_APP_ENV?: 'dev' | 'prod'so the read is typed, notany`. - [ ] REFACTOR: only with the test green.
- [ ] Confirm the test has teeth — make the invalid case silently default instead of throw, watch the throw assertions fail, restore.
- [ ] Commit.
Verified by: apps/web/src/api/__tests__/apiBase.test.ts.
T3 — Web calls the env-selected absolute API directly (mutator in, dev proxy out)
Satisfies: R3, R4, R5, P1 #2, SC3. Depends on T2 (API_ORIGIN).
The generated client keeps emitting /api/* URLs (orval baseUrl); the mutator prepends the origin. Confirm orval 8.23's fetch-mutator contract by regenerating and reading the emitted client — codegen inspection, not a spike; systematic-debugging if the shape surprises.
- [ ] RED: add
apps/web/src/api/__tests__/apiFetch.test.ts— stubglobalThis.fetch; callapiFetchwith/api/legendariesand assertfetchreceiveshttp://localhost:3000/api/legendaries(mockAPI_ORIGINto the dev origin). Watch it fail (no module). - [ ] GREEN: add
apps/web/src/api/apiFetch.ts— prependAPI_ORIGINto the request URL, delegate tofetch, return orval's fetch-client shape. Inorval.config.tsaddoverride: { mutator: { path: './src/api/apiFetch.ts', name: 'apiFetch' } }to theapientry (keepbaseUrl: '/api'); correct the stale dev-proxy comment. - [ ] Regenerate + commit the client:
pnpm --filter @gw2priory/web generate:api; the files underapps/web/src/api/generated/**now route throughapiFetch. Stage them. - [ ] Remove the dev proxy (R5): delete the
server.proxyblock inapps/web/vite.config.tsand its comment. (Trivial deletion — verified by the local smoke, T6, not a unit test.) - [ ] REFACTOR: only with the test green.
- [ ] Confirm teeth — point the mutator at the wrong origin, watch
apiFetch.test.tsfail, restore. - [ ] Confirm
pnpm verify:contractis green (openapi.json byte-identical; regenerated client committed). - [ ] Commit.
Verified by: apps/web/src/api/__tests__/apiFetch.test.ts; pnpm verify:contract (SC3).
T4 — Blueprint drops the /api rewrite and carries VITE_APP_ENV=prod
Satisfies: R6, R7, P2 #1, SC5, SC8.
- [ ] RED: in
tests/deploy/render-blueprint.test.ts, replace the/api/*-proxy test — assert the web service'sroutesare only the SPA fallback ([{ type: rewrite, source: /*, destination: /index.html }]), that it has anenvVarsentry{ key: VITE_APP_ENV, value: prod }, and that the API'shealthCheckPathis/api/health. Add a block readingspecs/015-env-api-url/spec.md's traceability table: no empty cell, every backtick-cited.tspath exists (SC8). Watch it fail against today'srender.yaml. - [ ] GREEN: in
render.yaml— delete the web service's/api/*rewrite route (and its hardcodeddestination, 014's F2), keep the/*→/index.htmlroute, addenvVars: [{ key: VITE_APP_ENV, value: prod }]to the web service, change the APIhealthCheckPath/health→/api/health, and update the now-wrong proxy comments. - [ ] REFACTOR: only with the test green.
- [ ] Confirm teeth — re-add a stray
/api/*route, watch the "only SPA fallback" assertion fail, remove it. - [ ] Commit.
Verified by: tests/deploy/render-blueprint.test.ts.
T5 — deploy.md records the direct-URL + CORS model (and why the proxy died)
Satisfies: R8. Docs — no test; verified by review + the blueprint test's deploy.md regex assertions.
- [ ] Replace the proxy sections in
docs/architecture/deploy.md(the deploy-shape route #1,F1,F2) with the direct-URL + CORS model: the API serves/api/*(runtime prefix), the web app calls its absolute URL chosen byVITE_APP_ENV, CORS allows the web origin; why the proxy was abandoned (Render forwards the rewrite destination path literally — no:splat— measured 404 on/:splat, research V1); no/api/*route in the blueprint (F2 gone). Use the real suffixed URLs. - [ ] Update the verification log for 015's observational rows (SC1 local smoke, SC2 CORS curl, SC6 live cross-origin fetch) — dates filled in T6. Keep the existing
deploy.md↔CLAUDE.mdreferences the blueprint test asserts. - [ ] Commit.
Verified by: review; tests/deploy/render-blueprint.test.ts (deploy.md documented-checks block).
T6 — Verify: suite green + local end-to-end smoke (step 5)
Satisfies: SC1, SC2 (local), and the whole-suite / typecheck gates. superpowers:verification-before-completion.
- [ ] Run
pnpm typecheckand the fullpnpm test(both apps +tests/**) — all green, noany, no skips. Record the actual command output; no success claim without it. - [ ] Local smoke: run the API and
VITE_APP_ENV=devweb dev server; in the browser, Legendaries and Health populate by callinghttp://localhost:3000/api/*cross-origin with no CORS/proxy error.curl -H 'Origin: http://localhost:5173' http://localhost:3000/api/legendariesreturns the data with a matchingAccess-Control-Allow-Origin. Record both indeploy.mdwith today's date (SC1, SC2). - [ ]
superpowers:requesting-code-reviewthensuperpowers:receiving-code-reviewon the branch diff. - [ ] Fill
spec.md's traceability table cells from each task's Verified by, and confirm SC8 (no empty cell) passes.
Verified by: recorded command output; deploy.md local-smoke record; green review.
T7 — Close: status → implemented, merge, post-deploy check (step 6)
Satisfies: DoD; P2 #2, SC6 (post-deploy, observational).
- [ ] The human moves
spec.mdstatusapproved→implementedinside the branch (agent transcribes on instruction); this edit is part of the PR diff, before merge. - [ ]
superpowers:finishing-a-development-branch— PR #17 green, merge tomain. - [ ] After Render deploys (still
checksPass): open the prod web URL — the SPA loads its data from the API with no proxy and no CORS error;curl -H 'Origin: https://gw2priory-l21x-7m5b.onrender.com'the API's/api/legendariesreturns data with a matching ACAO. Record indeploy.mdwith the date (SC6, P2 #2). - [ ] Worktree cleanup per
CLAUDE.md(done from the main tree after merge).
Verified by: deploy.md verification log (dated); merged PR.
Notes
Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.
- Orval 8.23 fetch-mutator contract (return
Responsevs parsed shape) is pinned in T3 by reading the regenerated client — record the actual shape here if it differs from what the plan assumed.