refactor(env): update NEXT_PUBLIC_API_URL to use 'auto' for better flexibility

This commit is contained in:
Abhimanyu Saharan
2026-03-03 02:40:28 +05:30
parent 2031f8dcd8
commit 0fe61e3e08
11 changed files with 74 additions and 33 deletions

View File

@@ -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://<server-ip>: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

View File

@@ -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)://<current-host>:8000`.
- Set an explicit URL when your API is behind a reverse proxy or non-default port.
### 2. Start Mission Control

View File

@@ -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://<server-ip>:3000 or https://mc.example.com).
CORS_ORIGINS=http://localhost:3000
BASE_URL=
# Security response headers (blank values disable each header).

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)://<current-host>: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 youre 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)://<current-host>:8000`.
If your backend is on a different host/port, set `NEXT_PUBLIC_API_URL` explicitly.
Fix:

View File

@@ -1,4 +1,5 @@
import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth";
import { getApiBaseUrl } from "@/lib/api-base";
type ClerkSession = {
getToken: () => Promise<string>;
@@ -39,11 +40,7 @@ export const customFetch = async <T>(
url: string,
options: RequestInit,
): Promise<T> => {
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;

View File

@@ -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<string | null> {
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`, {

View File

@@ -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");
});
});

View File

@@ -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.",
);
}