feat(ui): derive Vercel proxy backend from per-project env var (#75)

Generate the /dashboard/api/* proxy destination at build time from
LANGGRAPH_BACKEND_URL via the Vercel Build Output API, instead of hardcoding
it in ui/vercel.json. This lets the dev and main branches stay byte-identical
(required by the fast-forward-only prod promotion) while each Vercel project
proxies to its own LangGraph backend purely via its own env var — so both the
dev and prod dashboards can be git-linked.

- ui/vercel.json: drop the hardcoded rewrites; buildCommand -> bun run vercel-build
- ui/package.json: add vercel-build = vite build && node scripts/build-vercel-output.mjs
- ui/scripts/build-vercel-output.mjs: emit .vercel/output/config.json with
  [ proxy /dashboard/api/* -> $LANGGRAPH_BACKEND_URL, filesystem, SPA fallback ];
  throws if LANGGRAPH_BACKEND_URL is unset so a misconfig fails the build.

Keeps the app same-origin (no CORS/auth change); preserves the _shell.html SPA
fallback. Set LANGGRAPH_BACKEND_URL per Vercel project (All Environments).
This commit is contained in:
Adam Moussa 2026-06-30 11:43:36 -04:00 • committed by GitHub
parent a30ce2ab40
commit 076e20d7e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 49 additions and 12 deletions

View file

@ -5,6 +5,7 @@
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"vercel-build": "vite build && node scripts/build-vercel-output.mjs",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",

View file

@ -0,0 +1,47 @@
// Repackage the static Vite/TanStack-Start build (.output/public) into the Vercel
// Build Output API layout (.vercel/output), generating the /dashboard/api/* proxy
// destination from a per-project env var.
//
// Why this exists: prod deploys via a fast-forward-only promotion, so the `dev` and
// `main` branches are byte-identical and a committed vercel.json CANNOT differ between
// them. The app is same-origin only (no backend CORS), so the per-environment backend
// must be the PROXY DESTINATION, supplied per Vercel project as LANGGRAPH_BACKEND_URL.
// Vercel reads vercel.json BEFORE the build and never interpolates env into it, so the
// routes are emitted here (read AFTER the build) where process.env is available.
import { cp, mkdir, writeFile, rm } from "node:fs/promises"
const backend = process.env.LANGGRAPH_BACKEND_URL
if (!backend) {
throw new Error(
"LANGGRAPH_BACKEND_URL is required at build time (set it per Vercel project: " +
"dev project -> dev langgraph URL, prod project -> prod langgraph URL).",
)
}
const base = backend.replace(/\/$/, "")
await rm(".vercel/output", { recursive: true, force: true })
await mkdir(".vercel/output/static", { recursive: true })
await cp(".output/public", ".vercel/output/static", { recursive: true })
await writeFile(
".vercel/output/config.json",
JSON.stringify(
{
version: 3,
routes: [
// Same-origin proxy to this project's backend (proxy, not redirect, so the
// osw_session cookie stays first-party on the Vercel origin).
{ src: "^/dashboard/api/(.*)$", dest: `${base}/dashboard/api/$1` },
// Serve hashed assets, the PWA manifest/service worker, and _shell.html itself
// BEFORE the SPA catch-all, or static files get swallowed by the fallback.
{ handle: "filesystem" },
// SPA fallback (replaces the old vercel.json /(.*) -> /_shell.html rewrite).
{ src: "^/.*$", dest: "/_shell.html" },
],
},
null,
2,
) + "\n",
)
console.log(`[build-vercel-output] wrote .vercel/output/config.json (backend: ${base})`)

View file

@ -1,15 +1,4 @@
{
"framework": null,
"buildCommand": "bun run build",
"outputDirectory": ".output/public",
"rewrites": [
{
"source": "/dashboard/api/:path*",
"destination": "https://open-swe-v3-f2834ffc0df05a46a10262a9690e8490.us.langgraph.app/dashboard/api/:path*"
},
{
"source": "/(.*)",
"destination": "/_shell.html"
}
]
"buildCommand": "bun run vercel-build"
}