From 4777f4ca1322d27a2de9840fecedd5b0b5019342 Mon Sep 17 00:00:00 2001 From: AndresL230 <190146319+AndresL230@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:52:25 -0400 Subject: [PATCH 1/2] fix(frontend): fail build when BACKEND_URL unset; document staging footgun A staging frontend build without a build-time BACKEND_URL falls back to http://localhost:5000 in the /api rewrite destination. Next's path-to-regexp then misreads the :5000 port as a route param ('Expected "5000" to be a string'), so every proxied /api/* call 500s at the worker while the backend itself is healthy. wrangler.toml [vars] is runtime-only and does not cover the build-time rewrite. Throw at build time when NODE_ENV=production and BACKEND_URL is unset so a broken worker can never ship silently again, and document the build-time vars (and the SESSION_SECRET parity requirement) in the staging checklist. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/staging/setup-checklist.md | 8 +++++++- frontend/next.config.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/staging/setup-checklist.md b/docs/staging/setup-checklist.md index e3220dc0..d1413616 100644 --- a/docs/staging/setup-checklist.md +++ b/docs/staging/setup-checklist.md @@ -40,8 +40,13 @@ variables, Cloudflare worker secrets, or a local gitignored `backend/.env.stagin ### Step 4 — Cloudflare Workers (the frontend service) - [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker. +- [ ] **Set the build-time env vars** `BACKEND_URL` and `NEXT_PUBLIC_API_URL` (both `https://api.staging.saplinglearn.com`) for the staging build. These are read by `next.config.ts`/client bundles at **build time** — `wrangler.toml [env.staging.vars]` is runtime-only and does NOT cover them. If you deploy via Cloudflare Workers Builds, add them as **Build variables** on the staging build; if you deploy locally, export them before `npm run cf:deploy:staging` (also export `NEXT_PUBLIC_LOCAL_MODE=false` so a stray local `.env.local` can't flip staging into mock mode). Miss `BACKEND_URL` and the `/api` rewrite bakes `http://localhost:5000` → every dashboard API call 500s (`next.config.ts` now fails the build loudly if it is unset). - [ ] Add the worker secrets for the Access hop (Step 5): `wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`. +- [ ] Add `SESSION_SECRET` as a worker secret — **it must equal the Railway `SESSION_SECRET` (Step 3) exactly**: + `wrangler secret put SESSION_SECRET --env staging`. The frontend `/api/auth/session` route HMAC-verifies the + backend's `auth_token` and signs the `sapling_session` cookie with this; if it is missing the route returns + 500 and sign-in fails with "Sign-in failed", if it mismatches the backend it returns 401 (same symptom). ### Step 5 — DNS + Cloudflare Access (the security gate) - [ ] `api.staging.saplinglearn.com` → point at the Railway target from Step 3 (proxied). @@ -62,7 +67,8 @@ variables, Cloudflare worker secrets, or a local gitignored `backend/.env.stagin |---|---|---|---|---| | `SUPABASE_SERVICE_KEY` | ✅ | ❌ | ✅ (for migrate/seed) | ❌ never | | `SUPABASE_DB_URL` | ✅ | ❌ | ✅ | ❌ | -| `ENCRYPTION_KEY` / `SESSION_SECRET` | ✅ | ❌ | ✅ (encryption key only, for seed) | ❌ | +| `ENCRYPTION_KEY` | ✅ | ❌ | ✅ (for seed) | ❌ | +| `SESSION_SECRET` | ✅ | ✅ (`wrangler secret put --env staging`, **same value as Railway**) | optional | ❌ | | `GOOGLE_CLIENT_ID` / `_SECRET` | ✅ | ❌ | ✅ | ❌ | | `CF_ACCESS_CLIENT_ID` / `_SECRET` | ❌ | ✅ (`wrangler secret put`) | ❌ | ❌ | | `BACKEND_URL` / `COOKIE_DOMAIN` (non-secret) | ❌ | ✅ (`wrangler.toml` vars) | ❌ | ✅ (config, not secret) | diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 92a33e76..1817a387 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -5,6 +5,23 @@ import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:5000"; +// Guard against the silent footgun that took staging's dashboard down: a +// deployment build (NODE_ENV=production) MUST set BACKEND_URL explicitly. +// Falling back to http://localhost:5000 bakes a :5000 port into the /api +// rewrite destination, which Next's path-to-regexp then misreads as a route +// param named "5000" ("TypeError: Expected \"5000\" to be a string") so every +// proxied /api/* call 500s at runtime. wrangler.toml [vars] is runtime-only and +// does NOT fix this — the rewrite is baked at build time. Fail the build loudly +// instead of shipping a worker that 500s on every API call. +if (process.env.NODE_ENV === "production" && !process.env.BACKEND_URL) { + throw new Error( + "BACKEND_URL is required for production builds. Set it to the backend origin " + + "(prod: https://api.saplinglearn.com, staging: https://api.staging.saplinglearn.com) " + + "as a build-time env var or Cloudflare Workers Builds variable. Without it the /api " + + "rewrite bakes http://localhost:5000 and every proxied API call 500s.", + ); +} + const nextConfig: NextConfig = { // `standalone` is ignored by @opennextjs/cloudflare (it does its own // packaging), but keeping it lets `next build` alone still produce a From be33f4b26daed6c1ca5525d6c2bd8504805acdb2 Mon Sep 17 00:00:00 2001 From: AndresL230 <190146319+AndresL230@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:06:46 -0400 Subject: [PATCH 2/2] fix(frontend): trim BACKEND_URL and validate its shape at build time A stray leading/trailing space in the Workers Builds BACKEND_URL variable makes the /api rewrite destination start with a space, which Next rejects at build time as 'Invalid rewrite found'. Trim the value so the build tolerates whitespace, and validate it is an absolute http(s) origin with a clear error that points at the variable instead of Next's cryptic message. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/staging/setup-checklist.md | 2 +- frontend/next.config.ts | 32 ++++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/staging/setup-checklist.md b/docs/staging/setup-checklist.md index d1413616..4134426d 100644 --- a/docs/staging/setup-checklist.md +++ b/docs/staging/setup-checklist.md @@ -40,7 +40,7 @@ variables, Cloudflare worker secrets, or a local gitignored `backend/.env.stagin ### Step 4 — Cloudflare Workers (the frontend service) - [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker. -- [ ] **Set the build-time env vars** `BACKEND_URL` and `NEXT_PUBLIC_API_URL` (both `https://api.staging.saplinglearn.com`) for the staging build. These are read by `next.config.ts`/client bundles at **build time** — `wrangler.toml [env.staging.vars]` is runtime-only and does NOT cover them. If you deploy via Cloudflare Workers Builds, add them as **Build variables** on the staging build; if you deploy locally, export them before `npm run cf:deploy:staging` (also export `NEXT_PUBLIC_LOCAL_MODE=false` so a stray local `.env.local` can't flip staging into mock mode). Miss `BACKEND_URL` and the `/api` rewrite bakes `http://localhost:5000` → every dashboard API call 500s (`next.config.ts` now fails the build loudly if it is unset). +- [ ] **Set the build-time env vars** `BACKEND_URL` and `NEXT_PUBLIC_API_URL` (both `https://api.staging.saplinglearn.com`) for the staging build. These are read by `next.config.ts`/client bundles at **build time** — `wrangler.toml [env.staging.vars]` is runtime-only and does NOT cover them. If you deploy via Cloudflare Workers Builds, add them as **Build variables** on the staging build; if you deploy locally, export them before `npm run cf:deploy:staging` (also export `NEXT_PUBLIC_LOCAL_MODE=false` so a stray local `.env.local` can't flip staging into mock mode). Miss `BACKEND_URL` and the `/api` rewrite bakes `http://localhost:5000` → every dashboard API call 500s (`next.config.ts` now fails the build loudly if it is unset). Paste the value with **no leading/trailing whitespace** — a stray space makes Next reject the rewrite at build time (`Invalid rewrite found`); `next.config.ts` trims it defensively but keep the dashboard value clean. - [ ] Add the worker secrets for the Access hop (Step 5): `wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`. - [ ] Add `SESSION_SECRET` as a worker secret — **it must equal the Railway `SESSION_SECRET` (Step 3) exactly**: diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 1817a387..315cae50 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,7 +3,12 @@ import type { NextConfig } from "next"; // against Cloudflare bindings (R2/KV/env vars). Safe no-op in prod builds. import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; -const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:5000"; +// .trim() defends against a stray leading/trailing space in the build +// variable: an untrimmed " https://..." makes the /api rewrite destination +// start with a space, which Next rejects at build time as "Invalid rewrite +// found". Trimming makes the build tolerant; the guard below still rejects a +// genuinely malformed value. +const BACKEND_URL = (process.env.BACKEND_URL ?? "").trim() || "http://localhost:5000"; // Guard against the silent footgun that took staging's dashboard down: a // deployment build (NODE_ENV=production) MUST set BACKEND_URL explicitly. @@ -13,13 +18,24 @@ const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:5000"; // proxied /api/* call 500s at runtime. wrangler.toml [vars] is runtime-only and // does NOT fix this — the rewrite is baked at build time. Fail the build loudly // instead of shipping a worker that 500s on every API call. -if (process.env.NODE_ENV === "production" && !process.env.BACKEND_URL) { - throw new Error( - "BACKEND_URL is required for production builds. Set it to the backend origin " + - "(prod: https://api.saplinglearn.com, staging: https://api.staging.saplinglearn.com) " + - "as a build-time env var or Cloudflare Workers Builds variable. Without it the /api " + - "rewrite bakes http://localhost:5000 and every proxied API call 500s.", - ); +if (process.env.NODE_ENV === "production") { + const configured = (process.env.BACKEND_URL ?? "").trim(); + if (!configured) { + throw new Error( + "BACKEND_URL is required for production builds. Set it to the backend origin " + + "(prod: https://api.saplinglearn.com, staging: https://api.staging.saplinglearn.com) " + + "as a build-time env var or Cloudflare Workers Builds variable. Without it the /api " + + "rewrite bakes http://localhost:5000 and every proxied API call 500s.", + ); + } + if (!/^https?:\/\//.test(configured)) { + throw new Error( + "BACKEND_URL must be an absolute http(s) origin, got " + + JSON.stringify(process.env.BACKEND_URL) + + ". A stray leading/trailing space is the usual cause (Next then rejects the " + + "/api rewrite as 'Invalid rewrite found') — check the Cloudflare Workers Builds variable.", + ); + } } const nextConfig: NextConfig = {