SIGN IN SIGN UP

fix(showcase/shell-dashboard): resolve OPS_BASE_URL at runtime via /api/ops route handler (#5148)

## Root cause: build-time freeze of OPS_BASE_URL

The shell-dashboard ships as a **prebuilt** Docker image
(`ghcr.io/copilotkit/showcase-shell-dashboard:latest`). The Feature
Matrix health overlay calls `/api/ops/probes`, which was served by a
`next.config.ts` `rewrites()` entry proxying `/api/ops/:path*` →
`${process.env.OPS_BASE_URL}/api/:path*`.

Next.js evaluates `rewrites()` at **`next build`** and freezes the
result into the image. To satisfy a throw-if-unset guard, the build was
fed the placeholder `OPS_BASE_URL=http://ops.invalid`, which got
**frozen into the artifact**. So at runtime:

- every `/api/ops/*` call → `ENOTFOUND ops.invalid` → **500**
- the overlay got no probe data → **downgraded every Feature Matrix cell
to amber** (zero green), even though staging PocketBase data was green
- the correct Railway runtime env
`OPS_BASE_URL=https://harness-staging-2ee4.up.railway.app` was
**ignored** because the value was frozen at build

Same freeze bakes the **production** harness URL into the single shared
`:latest` image, so even a correct rebuild would point staging's proxy
at the prod harness.

## Fix: runtime-resolved Route Handler

- New `src/app/api/ops/[...path]/route.ts` proxies at **request time**:
reads `process.env.OPS_BASE_URL` per request (`export const dynamic =
"force-dynamic"`, `revalidate = 0`, `runtime = "nodejs"` — never
statically cached), forwards method + query string + headers + body, and
relays the upstream status + body. GET/POST/PUT/PATCH/DELETE supported.
- Path mapping: `/api/ops/probes` → `${OPS_BASE_URL}/api/probes` (single
`/api`, trailing slashes normalized; never doubled/dropped). Matches the
post-rearch harness, which serves `/api/probes`.
- Missing `OPS_BASE_URL` at runtime → clear **503** (not a build throw).
Unreachable upstream → **502** with the target URL.
- Removed the `/api/ops/*` rewrite **and** the build-time throw-if-unset
guard from `next.config.ts`. The build no longer depends on
`OPS_BASE_URL`.
- The Route Handler reads only the non-public `OPS_BASE_URL` (the
`NEXT_PUBLIC_*` alternate is banned in shell source by the
`copilotkit/no-public-env-shell-read` oxlint rule — a public read is the
exact build-freeze footgun this change removes).
- Updated stale comments in `ops-api.ts`, `runtime-config.ts`, and the
env-switch spike test that referenced the old rewrite.

### Resolves both traps with one image
Each environment now resolves its **own** runtime `OPS_BASE_URL` from
the same shared artifact — staging proxies to the staging harness, prod
to the prod harness, no rebuild required.

## Validation
- **Build without `OPS_BASE_URL`** → succeeds. `/api/ops/[...path]`
reported as `ƒ (Dynamic) server-rendered on demand` (not statically
optimized).
- **Live runtime proxy**: started the production server (built with no
`OPS_BASE_URL`) with
`OPS_BASE_URL=https://harness-staging-2ee4.up.railway.app`; `GET
/api/ops/probes` returned the live staging-harness probes JSON (200,
`cache-control: no-cache`), proving runtime resolution + correct path
mapping.
- New `route.test.ts`: 7 tests (request-time env resolution, path/query
mapping, status/body relay, POST body forwarding, 503-when-unset,
502-on-upstream-failure). Red-green verified.
- Full package unit suite: **686 passed, 1 skipped**. Typecheck clean.
oxlint clean on changed files.

## Deploy note
**Requires the dashboard image to rebuild + redeploy.** Each Railway
environment must have `OPS_BASE_URL` set (staging:
`https://harness-staging-2ee4.up.railway.app`). The shared CI build no
longer needs to bake any harness URL.

## Test plan
- [ ] CI green
- [ ] Rebuild + redeploy `showcase-shell-dashboard` image
- [ ] Confirm staging Feature Matrix overlay shows green cells (probe
data flowing via `/api/ops/probes`)
J
Jordan Ritter committed
2f91e2eb34f9cdbfc165933d1434a4ea70ec4cf4
Committed by GitHub <noreply@github.com> on 6/1/2026, 2:51:41 PM