// Production static-file server for the built SPA, run by Bun (replaces nginx). // Serves apps/frontend/dist and reverse-proxies backend routes so the // session cookie stays first-party. Dev still uses Vite (see vite.config.ts). const PORT = Number(process.env.PORT ?? 8080); const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001"; const DIST = "./dist"; function isProxied(pathname: string): boolean { return ( pathname === "/health" || pathname.startsWith("/auth") || pathname.startsWith("/api") || pathname.startsWith("/internal") ); } // Forward the request to the backend, preserving method/headers/body/cookies. async function proxy(req: Request, url: URL): Promise { const target = new URL(url.pathname + url.search, BACKEND_URL); const headers = new Headers(req.headers); // Let fetch set Host from the target URL (Cloud Run routes by Host). headers.delete("host"); headers.set("x-forwarded-host", url.host); headers.set("x-forwarded-proto", url.protocol.replace(":", "")); const init: RequestInit = { method: req.method, headers, redirect: "manual" }; if (req.method !== "GET" && req.method !== "HEAD") { init.body = await req.arrayBuffer(); } return fetch(target, init); } async function serveStatic(url: URL): Promise { const pathname = url.pathname === "/" ? "/index.html" : url.pathname; const file = Bun.file(`${DIST}${pathname}`); if (await file.exists()) { const headers: Record = pathname.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : { "Cache-Control": "no-cache" }; return new Response(file, { headers }); } // SPA fallback: serve index.html for client-side routes. return new Response(Bun.file(`${DIST}/index.html`), { headers: { "Cache-Control": "no-cache" } }); } Bun.serve({ port: PORT, fetch(req) { const url = new URL(req.url); return isProxied(url.pathname) ? proxy(req, url) : serveStatic(url); }, }); console.log(`cerebrus-frontend serving on :${PORT} (proxy backend routes -> ${BACKEND_URL})`);