Skip to content

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, apply setGlobalPrefix('api') + enableCors({ origin: CORS_ALLOWLIST }), set Swagger at api/docs — and assert: GET /api/health → 200; GET /api/docs → 200 text/html; a GET carrying Origin: http://localhost:5173 returns access-control-allow-origin: http://localhost:5173; a preflight OPTIONS /api/legendaries with that Origin is answered (not 404). Watch it fail against today's /api-docs, no-prefix app.
  • [ ] GREEN: add apps/api/src/config/cors.ts exporting CORS_ALLOWLIST (readonly string[] — http://localhost:5173, https://gw2priory-l21x-7m5b.onrender.com). In main.ts add app.setGlobalPrefix('api'), app.enableCors({ origin: CORS_ALLOWLIST }), and change SwaggerModule.setup('api-docs', …) → SwaggerModule.setup('api/docs', …). Do not edit generate-openapi.ts.
  • [ ] REFACTOR: only with the test green. Confirm pnpm --filter @gw2priory/api generate:openapi still emits a byte-identical openapi.json (paths stay /health, /legendaries, /recipe-graph/{itemId}) — the existing generate-openapi.test.ts guards this.
  • [ ] Confirm the test has teeth — remove setGlobalPrefix (health 404s) and the enableCors line (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) and resolveApiOrigin('staging') each throw. Watch it fail (no module yet).
  • [ ] GREEN: add apps/web/src/api/apiBase.ts — a typed { dev, prod } const map and resolveApiOrigin(env: string | undefined): string that returns the mapped origin or throws Error(\Invalid VITE_APP_ENV: ${env}`); export API_ORIGIN = resolveApiOrigin(import.meta.env.VITE_APP_ENV). Add apps/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 — stub globalThis.fetch; call apiFetch with /api/legendaries and assert fetch receives http://localhost:3000/api/legendaries (mock API_ORIGIN to the dev origin). Watch it fail (no module).
  • [ ] GREEN: add apps/web/src/api/apiFetch.ts — prepend API_ORIGIN to the request URL, delegate to fetch, return orval's fetch-client shape. In orval.config.ts add override: { mutator: { path: './src/api/apiFetch.ts', name: 'apiFetch' } } to the api entry (keep baseUrl: '/api'); correct the stale dev-proxy comment.
  • [ ] Regenerate + commit the client: pnpm --filter @gw2priory/web generate:api; the files under apps/web/src/api/generated/** now route through apiFetch. Stage them.
  • [ ] Remove the dev proxy (R5): delete the server.proxy block in apps/web/vite.config.ts and 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.ts fail, restore.
  • [ ] Confirm pnpm verify:contract is 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's routes are only the SPA fallback ([{ type: rewrite, source: /*, destination: /index.html }]), that it has an envVars entry { key: VITE_APP_ENV, value: prod }, and that the API's healthCheckPath is /api/health. Add a block reading specs/015-env-api-url/spec.md's traceability table: no empty cell, every backtick-cited .ts path exists (SC8). Watch it fail against today's render.yaml.
  • [ ] GREEN: in render.yaml — delete the web service's /api/* rewrite route (and its hardcoded destination, 014's F2), keep the /*→/index.html route, add envVars: [{ key: VITE_APP_ENV, value: prod }] to the web service, change the API healthCheckPath /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 by VITE_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.md references 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 typecheck and the full pnpm test (both apps + tests/**) — all green, no any, no skips. Record the actual command output; no success claim without it.
  • [ ] Local smoke: run the API and VITE_APP_ENV=dev web dev server; in the browser, Legendaries and Health populate by calling http://localhost:3000/api/* cross-origin with no CORS/proxy error. curl -H 'Origin: http://localhost:5173' http://localhost:3000/api/legendaries returns the data with a matching Access-Control-Allow-Origin. Record both in deploy.md with today's date (SC1, SC2).
  • [ ] superpowers:requesting-code-review then superpowers:receiving-code-review on 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.md status approved → implemented inside 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 to main.
  • [ ] 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/legendaries returns data with a matching ACAO. Record in deploy.md with 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 Response vs parsed shape) is pinned in T3 by reading the regenerated client — record the actual shape here if it differs from what the plan assumed.