ADR-011: UI BASE_URL Contract and Cookie Path Consistency
Status
Accepted
Date
2026-05-29
Context
The server supports a BASE_URL environment variable (e.g., /gateway/no-crd) that prefixes all HTTP routes when deployed behind a reverse proxy or API gateway. Fastify registers all routes under { prefix: basePrefix }, so a route defined as /mcp is served at /gateway/no-crd/mcp.
The UI dashboard runs in the browser and needs to make API calls to the server. It also constructs workspace links and handles OIDC redirect flows. All of these must resolve correctly regardless of whether BASE_URL is set or not.
Two issues were identified during an audit of the UI's HTTP calls:
Hardcoded
localhost:3000fallback: The UI's HTTP transport initialization triedhttp://localhost:3000/mcpas a fallback endpoint. This is unreachable in production or k3d ingress deployments (port 3000 is only accessible inside the cluster), causingERR_CONNECTION_REFUSEDerrors and unnecessary startup delays./logoutmissingbasePath: The logout button fetched/logoutwithout thebasePathprefix, resulting in a 404 whenBASE_URLis configured.
A third concern was raised about cookie Path values: the proxy sets nocr_token cookies with Path=/route/{id}/ (without basePrefix), and the logout handler clears them with the same raw path. The question was whether this mismatch with the actual URL path (which includes basePrefix) would cause cookies to persist after logout.
Decision
1. UI basePath Contract
The server injects window.__NOCR_BASE_URL__ into the HTML page via loadUiHtml(). The UI reads this as basePath:
const basePath = (window as any).__NOCR_BASE_URL__ || "";All UI fetch calls to server endpoints MUST prefix their paths with basePath. This includes:
| Endpoint | Path Pattern |
|---|---|
| MCP transport | ${basePath}/mcp |
| Routing proxy links | ${basePath}/route/${id}/${path} |
| Theme list | ${basePath}/api/themes |
| Theme CSS file | ${basePath}/api/themes/${id}.css |
| Logout | ${basePath}/logout |
OIDC redirect URIs use window.location.origin + window.location.pathname, which implicitly includes the base path since the page itself is served under it.
2. No Hardcoded localhost URLs
The http://localhost:3000/mcp fallback was removed. The relative same-origin path ${basePath}/mcp works in all deployment scenarios:
- Local dev (
bun run dev:bun): origin ishttp://localhost:3000,basePathis"", resolves to/mcp✓ - k3d ingress (
localhost:8080): origin ishttp://localhost:8080,basePathis"", resolves to/mcpvia ingress ✓ - Subpath proxy (
example.com/gateway/no-crd):basePathis/gateway/no-crd, resolves to/gateway/no-crd/mcp✓
3. Cookie Path Consistency
Analysis confirmed that cookie paths are consistent between set and clear operations:
| Operation | Location | Cookie Path |
|---|---|---|
Set nocr_token | proxy.ts onResponse | Path=${basePrefix}/route/${id}/ |
Clear nocr_token | mcp.ts logoutHandler | Path=${basePrefix}/route/${id}/ |
Set nocr_sess | auth.ts preHandler | Path=/ |
Clear nocr_sess | mcp.ts logoutHandler | Path=/ |
Both set and clear use the same paths (with basePrefix for workspace-scoped cookies). This works because:
- Fastify's
{ prefix: basePrefix }affects URL routing, not theSet-Cookieheader value. - The
Pathattribute inSet-Cookieis a literal string sent to the browser. - The browser matches cookies based on the
Pathvalue, not the URL the response came from. - Since both operations write the same
Pathvalue, clearing always matches what was set.
Note: As of ADR-019, workspace-scoped cookies (
nocr_token) include thebasePrefixto ensure correct browser scoping across subpath deployments. The original design used raw paths withoutbasePrefix, but this was corrected when split-network OIDC deployments exposed cookie isolation issues.
Important caveat: If the reverse proxy rewrites cookie paths (some do), this could break. The current design assumes the proxy is transparent to Set-Cookie headers, which is the standard behavior for k8s Ingress controllers and most API gateways.
Alternatives Considered
Prefix cookie paths with basePrefix
- Pros: Cookies would match the actual URL path visible in the browser
- Cons:
Unnecessary— Initially rejected, but later adopted via ADR-019 when subpath deployments exposed cookie scoping issues.
Keep the localhost:3000 fallback behind a dev-mode flag
- Pros: Would still work for the niche case of opening the UI from a different dev server
- Cons: No real use case — in local dev the page is served from
localhost:3000so the relative path works. The flag adds complexity for zero practical benefit. - Rejected: YAGNI. The relative path covers all real scenarios.
Consequences
- All UI API calls are now audited and consistently use
basePath - No hardcoded localhost URLs remain in production code
- Cookie path behavior is documented with inline comments referencing this ADR
- Future developers modifying cookie-setting code should maintain the path consistency documented here
Amendments
| Date | Change |
|---|---|
| 2026-06-13 | Updated cookie path table to reflect that nocr_token now uses ${basePrefix}/route/${id}/ (with basePrefix), as corrected by ADR-019. Updated Alternative A to note it was later adopted. Added cross-reference to ADR-019. |
