diff --git a/.env.example b/.env.example
index 786f925..26fd203 100644
--- a/.env.example
+++ b/.env.example
@@ -29,3 +29,9 @@ LOCAL_AUTH_TOKEN=
# Use `auto` to target the same host currently serving Pipeline on port 8001.
# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com
NEXT_PUBLIC_API_URL=auto
+NEXT_PUBLIC_AUTH_MODE=local
+# Local auth token (used when NEXT_PUBLIC_AUTH_MODE=local).
+# When set: the app loads directly without showing the login screen.
+# When unset: users are prompted to enter their token on first load.
+# Must be at least 50 characters and match LOCAL_AUTH_TOKEN above.
+NEXT_PUBLIC_LOCAL_AUTH_TOKEN=
diff --git a/README.md b/README.md
index ec9993a..dbc594f 100644
--- a/README.md
+++ b/README.md
@@ -149,7 +149,35 @@ Mission Control supports two authentication modes:
- `local`: shared bearer token mode (default for self-hosted use)
- `clerk`: Clerk JWT mode
-Environment templates:
+### Local auth mode
+
+Set `AUTH_MODE=local` (backend) and `NEXT_PUBLIC_AUTH_MODE=local` (frontend).
+
+**Option 1 — Token in environment (recommended for self-hosted)**
+
+Set `NEXT_PUBLIC_LOCAL_AUTH_TOKEN` in the frontend environment. The app loads directly with no login screen. The token is used automatically for every API call.
+
+```env
+NEXT_PUBLIC_AUTH_MODE=local
+NEXT_PUBLIC_LOCAL_AUTH_TOKEN=your-bearer-token-here
+```
+
+**Option 2 — Token entered on load**
+
+Leave `NEXT_PUBLIC_LOCAL_AUTH_TOKEN` unset. On first load, users are presented with a login screen and must paste their bearer token before accessing the app. The token is stored in `sessionStorage` for the duration of the browser session.
+
+```env
+NEXT_PUBLIC_AUTH_MODE=local
+# NEXT_PUBLIC_LOCAL_AUTH_TOKEN is not set — login screen will appear
+```
+
+The bearer token must match `LOCAL_AUTH_TOKEN` set on the backend (minimum 50 characters).
+
+### Clerk mode
+
+Set `AUTH_MODE=clerk` and configure `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`.
+
+### Environment templates
- Root: [`.env.example`](./.env.example)
- Backend: [`backend/.env.example`](./backend/.env.example)
diff --git a/compose.yml b/compose.yml
index a458c7f..ba84529 100644
--- a/compose.yml
+++ b/compose.yml
@@ -58,6 +58,7 @@ services:
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto}
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
+ NEXT_PUBLIC_LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
# Optional, user-managed env file.
# IMPORTANT: do NOT load `.env.example` here because it contains non-empty
# placeholder Clerk keys, which can accidentally flip Clerk "on".
@@ -66,6 +67,7 @@ services:
environment:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto}
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
+ NEXT_PUBLIC_LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
depends_on:
- backend
ports:
diff --git a/frontend/.env.example b/frontend/.env.example
index 3de0593..df40e7b 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -5,9 +5,15 @@ NEXT_PUBLIC_API_URL=auto
# Auth mode: clerk or local.
# - clerk: Clerk sign-in flow
-# - local: shared bearer token entered in UI
+# - local: shared bearer token, entered in UI or pre-set via env
NEXT_PUBLIC_AUTH_MODE=local
+# Local auth token (used when NEXT_PUBLIC_AUTH_MODE=local).
+# When set: the app loads directly without showing the login screen.
+# When unset: users are prompted to enter their token on first load.
+# Must be at least 50 characters and match LOCAL_AUTH_TOKEN on the backend.
+NEXT_PUBLIC_LOCAL_AUTH_TOKEN=
+
# Clerk auth (used when NEXT_PUBLIC_AUTH_MODE=clerk)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/boards
diff --git a/frontend/src/auth/localAuth.ts b/frontend/src/auth/localAuth.ts
index cfe59c9..32e0b10 100644
--- a/frontend/src/auth/localAuth.ts
+++ b/frontend/src/auth/localAuth.ts
@@ -9,6 +9,11 @@ export function isLocalAuthMode(): boolean {
return process.env.NEXT_PUBLIC_AUTH_MODE === AuthMode.Local;
}
+/** Token pre-configured via env — skips the login screen when set. */
+export function getEnvToken(): string | null {
+ return process.env.NEXT_PUBLIC_LOCAL_AUTH_TOKEN?.trim() || null;
+}
+
export function setLocalAuthToken(token: string): void {
localToken = token;
if (typeof window === "undefined") return;
diff --git a/frontend/src/components/organisms/LocalAuthLogin.tsx b/frontend/src/components/organisms/LocalAuthLogin.tsx
index d76216c..da121e0 100644
--- a/frontend/src/components/organisms/LocalAuthLogin.tsx
+++ b/frontend/src/components/organisms/LocalAuthLogin.tsx
@@ -7,39 +7,9 @@ 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