refactor(env): update NEXT_PUBLIC_API_URL to use 'auto' for better flexibility
This commit is contained in:
@@ -12,6 +12,7 @@ POSTGRES_PASSWORD=postgres
|
|||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
# --- backend settings (see backend/.env.example for full list) ---
|
# --- 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
|
CORS_ORIGINS=http://localhost:3000
|
||||||
DB_AUTO_MIGRATE=true
|
DB_AUTO_MIGRATE=true
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
@@ -22,6 +23,6 @@ LOCAL_AUTH_TOKEN=
|
|||||||
|
|
||||||
# --- frontend settings ---
|
# --- frontend settings ---
|
||||||
# REQUIRED: Public URL used by the browser to reach the API.
|
# 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.
|
# Use `auto` to target the same host currently serving Mission Control on port 8000.
|
||||||
# Example (local dev / compose on your machine):
|
# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
NEXT_PUBLIC_API_URL=auto
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ cp .env.example .env
|
|||||||
Before startup:
|
Before startup:
|
||||||
|
|
||||||
- Set `LOCAL_AUTH_TOKEN` to a non-placeholder value (minimum 50 characters) when `AUTH_MODE=local`.
|
- 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
|
### 2. Start Mission Control
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ LOG_USE_UTC=false
|
|||||||
REQUEST_LOG_SLOW_MS=1000
|
REQUEST_LOG_SLOW_MS=1000
|
||||||
REQUEST_LOG_INCLUDE_HEALTH=false
|
REQUEST_LOG_INCLUDE_HEALTH=false
|
||||||
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control
|
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
|
CORS_ORIGINS=http://localhost:3000
|
||||||
BASE_URL=
|
BASE_URL=
|
||||||
# Security response headers (blank values disable each header).
|
# Security response headers (blank values disable each header).
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
args:
|
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}
|
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
|
||||||
# Optional, user-managed env file.
|
# Optional, user-managed env file.
|
||||||
# IMPORTANT: do NOT load `.env.example` here because it contains non-empty
|
# IMPORTANT: do NOT load `.env.example` here because it contains non-empty
|
||||||
@@ -64,7 +64,7 @@ services:
|
|||||||
- path: ./frontend/.env
|
- path: ./frontend/.env
|
||||||
required: false
|
required: false
|
||||||
environment:
|
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}
|
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# REQUIRED: base URL for frontend -> backend calls (must be set for Activity feed and other API calls).
|
# Base URL for frontend -> backend calls.
|
||||||
# Must be reachable from the browser (host).
|
# Use `auto` to target the same host currently serving Mission Control on port 8000.
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
# Example explicit override: https://mc.example.com
|
||||||
|
NEXT_PUBLIC_API_URL=auto
|
||||||
|
|
||||||
# Auth mode: clerk or local.
|
# Auth mode: clerk or local.
|
||||||
# - clerk: Clerk sign-in flow
|
# - clerk: Clerk sign-in flow
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
# Allows configuring the API URL at build time.
|
# 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}
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
ARG NEXT_PUBLIC_AUTH_MODE
|
ARG NEXT_PUBLIC_AUTH_MODE
|
||||||
ENV NEXT_PUBLIC_AUTH_MODE=${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
|
# If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well
|
||||||
# (but note some values may be baked at build time).
|
# (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}
|
ENV NEXT_PUBLIC_AUTH_MODE=${NEXT_PUBLIC_AUTH_MODE}
|
||||||
|
|
||||||
COPY --from=builder /app/.next ./.next
|
COPY --from=builder /app/.next ./.next
|
||||||
|
|||||||
@@ -44,15 +44,15 @@ The frontend reads configuration from standard Next.js env files (`.env.local`,
|
|||||||
|
|
||||||
#### `NEXT_PUBLIC_API_URL`
|
#### `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`).
|
- Used by the generated API client and helpers (see `src/lib/api-base.ts` and `src/api/mutator.ts`).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
NEXT_PUBLIC_API_URL=auto
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication mode
|
### Authentication mode
|
||||||
@@ -141,9 +141,10 @@ If you’re working on self-hosting, prefer running compose from the repo root s
|
|||||||
|
|
||||||
## Troubleshooting
|
## 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:
|
Fix:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth";
|
import { getLocalAuthToken, isLocalAuthMode } from "@/auth/localAuth";
|
||||||
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
|
|
||||||
type ClerkSession = {
|
type ClerkSession = {
|
||||||
getToken: () => Promise<string>;
|
getToken: () => Promise<string>;
|
||||||
@@ -39,11 +40,7 @@ export const customFetch = async <T>(
|
|||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
const baseUrl = getApiBaseUrl();
|
||||||
if (!rawBaseUrl) {
|
|
||||||
throw new Error("NEXT_PUBLIC_API_URL is not set.");
|
|
||||||
}
|
|
||||||
const baseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
||||||
|
|
||||||
const headers = new Headers(options.headers);
|
const headers = new Headers(options.headers);
|
||||||
const hasBody = options.body !== undefined && options.body !== null;
|
const hasBody = options.body !== undefined && options.body !== null;
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import { setLocalAuthToken } from "@/auth/localAuth";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
|
|
||||||
const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50;
|
const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50;
|
||||||
|
|
||||||
async function validateLocalToken(token: string): Promise<string | null> {
|
async function validateLocalToken(token: string): Promise<string | null> {
|
||||||
const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
let baseUrl: string;
|
||||||
if (!rawBaseUrl) {
|
try {
|
||||||
return "NEXT_PUBLIC_API_URL is not set.";
|
baseUrl = getApiBaseUrl();
|
||||||
|
} catch {
|
||||||
|
return "Unable to resolve backend URL.";
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(`${baseUrl}/api/v1/users/me`, {
|
response = await fetch(`${baseUrl}/api/v1/users/me`, {
|
||||||
|
|||||||
27
frontend/src/lib/api-base.test.ts
Normal file
27
frontend/src/lib/api-base.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
export function getApiBaseUrl(): string {
|
export function getApiBaseUrl(): string {
|
||||||
const raw = process.env.NEXT_PUBLIC_API_URL;
|
const raw = process.env.NEXT_PUBLIC_API_URL?.trim();
|
||||||
if (!raw) {
|
if (raw && raw.toLowerCase() !== "auto") {
|
||||||
throw new Error("NEXT_PUBLIC_API_URL is not set.");
|
const normalized = raw.replace(/\/+$/, "");
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error("NEXT_PUBLIC_API_URL is invalid.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
const normalized = raw.replace(/\/+$/, "");
|
|
||||||
if (!normalized) {
|
if (typeof window !== "undefined") {
|
||||||
throw new Error("NEXT_PUBLIC_API_URL is invalid.");
|
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.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user