From 0fe61e3e086ddff4969e40f76258fbd143d803b5 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Tue, 3 Mar 2026 02:40:28 +0530 Subject: [PATCH] refactor(env): update NEXT_PUBLIC_API_URL to use 'auto' for better flexibility --- .env.example | 7 ++--- README.md | 3 ++- backend/.env.example | 1 + compose.yml | 4 +-- frontend/.env.example | 7 ++--- frontend/Dockerfile | 4 +-- frontend/README.md | 11 ++++---- frontend/src/api/mutator.ts | 7 ++--- .../components/organisms/LocalAuthLogin.tsx | 11 ++++---- frontend/src/lib/api-base.test.ts | 27 +++++++++++++++++++ frontend/src/lib/api-base.ts | 25 ++++++++++++----- 11 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 frontend/src/lib/api-base.test.ts diff --git a/.env.example b/.env.example index 6823a9d0..d465ad2f 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ POSTGRES_PASSWORD=postgres POSTGRES_PORT=5432 # --- backend settings (see backend/.env.example for full list) --- +# For remote access, set this to your UI origin (e.g. http://:3000 or https://mc.example.com). CORS_ORIGINS=http://localhost:3000 DB_AUTO_MIGRATE=true LOG_LEVEL=INFO @@ -22,6 +23,6 @@ LOCAL_AUTH_TOKEN= # --- frontend settings --- # REQUIRED: Public URL used by the browser to reach the API. -# If this is missing/blank, frontend API calls (e.g. Activity feed) will break. -# Example (local dev / compose on your machine): -NEXT_PUBLIC_API_URL=http://localhost:8000 +# Use `auto` to target the same host currently serving Mission Control on port 8000. +# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com +NEXT_PUBLIC_API_URL=auto diff --git a/README.md b/README.md index c0ea5dc7..3f953737 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ cp .env.example .env Before startup: - Set `LOCAL_AUTH_TOKEN` to a non-placeholder value (minimum 50 characters) when `AUTH_MODE=local`. -- Ensure `NEXT_PUBLIC_API_URL` is reachable from your browser. +- `NEXT_PUBLIC_API_URL=auto` (default) resolves to `http(s)://:8000`. + - Set an explicit URL when your API is behind a reverse proxy or non-default port. ### 2. Start Mission Control diff --git a/backend/.env.example b/backend/.env.example index 098f36b1..12de11d2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,6 +5,7 @@ LOG_USE_UTC=false REQUEST_LOG_SLOW_MS=1000 REQUEST_LOG_INCLUDE_HEALTH=false DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control +# For remote access, set this to your UI origin (e.g. http://:3000 or https://mc.example.com). CORS_ORIGINS=http://localhost:3000 BASE_URL= # Security response headers (blank values disable each header). diff --git a/compose.yml b/compose.yml index d55a4428..53581af4 100644 --- a/compose.yml +++ b/compose.yml @@ -55,7 +55,7 @@ services: build: context: ./frontend args: - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto} NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE} # Optional, user-managed env file. # IMPORTANT: do NOT load `.env.example` here because it contains non-empty @@ -64,7 +64,7 @@ services: - path: ./frontend/.env required: false environment: - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto} NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE} depends_on: - backend diff --git a/frontend/.env.example b/frontend/.env.example index b1db4249..c5f1ad1d 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,6 +1,7 @@ -# REQUIRED: base URL for frontend -> backend calls (must be set for Activity feed and other API calls). -# Must be reachable from the browser (host). -NEXT_PUBLIC_API_URL=http://localhost:8000 +# Base URL for frontend -> backend calls. +# Use `auto` to target the same host currently serving Mission Control on port 8000. +# Example explicit override: https://mc.example.com +NEXT_PUBLIC_API_URL=auto # Auth mode: clerk or local. # - clerk: Clerk sign-in flow diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a834856f..64c7b179 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,7 +13,7 @@ COPY --from=deps /app/node_modules ./node_modules COPY . ./ # Allows configuring the API URL at build time. -ARG NEXT_PUBLIC_API_URL=http://localhost:8000 +ARG NEXT_PUBLIC_API_URL=auto ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ARG NEXT_PUBLIC_AUTH_MODE ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE} @@ -28,7 +28,7 @@ ARG NEXT_PUBLIC_AUTH_MODE # If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well # (but note some values may be baked at build time). -ENV NEXT_PUBLIC_API_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_URL=auto ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE} COPY --from=builder /app/.next ./.next diff --git a/frontend/README.md b/frontend/README.md index 07d4daf2..a80e0695 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -44,15 +44,15 @@ The frontend reads configuration from standard Next.js env files (`.env.local`, #### `NEXT_PUBLIC_API_URL` -Base URL of the backend API. +Base URL of the backend API (or `auto`). -- Default for local dev: `http://localhost:8000` +- Default: `auto` (resolved in browser as `http(s)://:8000`) - Used by the generated API client and helpers (see `src/lib/api-base.ts` and `src/api/mutator.ts`). Example: ```env -NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_API_URL=auto ``` ### Authentication mode @@ -141,9 +141,10 @@ If you’re working on self-hosting, prefer running compose from the repo root s ## Troubleshooting -### `NEXT_PUBLIC_API_URL is not set` +### `NEXT_PUBLIC_API_URL` and remote hosts -The API client throws if `NEXT_PUBLIC_API_URL` is missing. +If unset or set to `auto`, the client uses `http(s)://:8000`. +If your backend is on a different host/port, set `NEXT_PUBLIC_API_URL` explicitly. Fix: diff --git a/frontend/src/api/mutator.ts b/frontend/src/api/mutator.ts index 610125f3..0c0037cd 100644 --- a/frontend/src/api/mutator.ts +++ b/frontend/src/api/mutator.ts @@ -1,4 +1,5 @@ import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth"; +import { getApiBaseUrl } from "@/lib/api-base"; type ClerkSession = { getToken: () => Promise; @@ -39,11 +40,7 @@ export const customFetch = async ( url: string, options: RequestInit, ): Promise => { - const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; - if (!rawBaseUrl) { - throw new Error("NEXT_PUBLIC_API_URL is not set."); - } - const baseUrl = rawBaseUrl.replace(/\/+$/, ""); + const baseUrl = getApiBaseUrl(); const headers = new Headers(options.headers); const hasBody = options.body !== undefined && options.body !== null; diff --git a/frontend/src/components/organisms/LocalAuthLogin.tsx b/frontend/src/components/organisms/LocalAuthLogin.tsx index fd7ab8c3..a2e7ef36 100644 --- a/frontend/src/components/organisms/LocalAuthLogin.tsx +++ b/frontend/src/components/organisms/LocalAuthLogin.tsx @@ -7,17 +7,18 @@ import { setLocalAuthToken } from "@/auth/localAuth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { getApiBaseUrl } from "@/lib/api-base"; const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50; async function validateLocalToken(token: string): Promise { - const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; - if (!rawBaseUrl) { - return "NEXT_PUBLIC_API_URL is not set."; + let baseUrl: string; + try { + baseUrl = getApiBaseUrl(); + } catch { + return "Unable to resolve backend URL."; } - const baseUrl = rawBaseUrl.replace(/\/+$/, ""); - let response: Response; try { response = await fetch(`${baseUrl}/api/v1/users/me`, { diff --git a/frontend/src/lib/api-base.test.ts b/frontend/src/lib/api-base.test.ts new file mode 100644 index 00000000..3d6176c0 --- /dev/null +++ b/frontend/src/lib/api-base.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { getApiBaseUrl } from "./api-base"; + +describe("getApiBaseUrl", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns normalized explicit URL", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", "https://api.example.com///"); + + expect(getApiBaseUrl()).toBe("https://api.example.com"); + }); + + it("auto-resolves from browser host when set to auto", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", "auto"); + + expect(getApiBaseUrl()).toBe("http://localhost:8000"); + }); + + it("auto-resolves from browser host when unset", () => { + vi.stubEnv("NEXT_PUBLIC_API_URL", ""); + + expect(getApiBaseUrl()).toBe("http://localhost:8000"); + }); +}); diff --git a/frontend/src/lib/api-base.ts b/frontend/src/lib/api-base.ts index a0fbea5b..3ba57223 100644 --- a/frontend/src/lib/api-base.ts +++ b/frontend/src/lib/api-base.ts @@ -1,11 +1,22 @@ export function getApiBaseUrl(): string { - const raw = process.env.NEXT_PUBLIC_API_URL; - if (!raw) { - throw new Error("NEXT_PUBLIC_API_URL is not set."); + const raw = process.env.NEXT_PUBLIC_API_URL?.trim(); + if (raw && raw.toLowerCase() !== "auto") { + const normalized = raw.replace(/\/+$/, ""); + if (!normalized) { + throw new Error("NEXT_PUBLIC_API_URL is invalid."); + } + return normalized; } - const normalized = raw.replace(/\/+$/, ""); - if (!normalized) { - throw new Error("NEXT_PUBLIC_API_URL is invalid."); + + if (typeof window !== "undefined") { + const protocol = window.location.protocol === "https:" ? "https" : "http"; + const host = window.location.hostname; + if (host) { + return `${protocol}://${host}:8000`; + } } - return normalized; + + throw new Error( + "NEXT_PUBLIC_API_URL is not set and cannot be auto-resolved outside the browser.", + ); }