diff --git a/docs/staging/setup-checklist.md b/docs/staging/setup-checklist.md index e3220dc0..4134426d 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). 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**: + `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..315cae50 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,7 +3,40 @@ 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. +// 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") { + 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 = { // `standalone` is ignored by @opennextjs/cloudflare (it does its own