diff --git a/CLAUDE.md b/CLAUDE.md
index 2a48c65..bd30031 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -32,7 +32,7 @@ app/
│ ├── home, calls, usage, keys, settings # auth-gated
│ ├── device, waitlist # device-approval + early-access
│ └── error.tsx
-├── (auth)/ # /login, /signup — no sidebar
+├── (auth)/ # /login, /signup — branded pages; hand off to Auth0; no sidebar
├── api/
│ ├── pymthouse/ # BFF: account-usage, keys, plans, subscribe, wallet, invoices
│ ├── mcp/ # Streamable HTTP MCP resource server
diff --git a/README.md b/README.md
index d4f14a0..8a6477b 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ lib/
| `/apps/[id]` | public | App detail + playground |
| `/orgs/[slug]` | public | Organization's published apps |
| `/network` | public | Network stats (sidebar: "Stats") |
-| `/login` | public | Sign in |
-| `/signup` | public | Sign up |
+| `/login` | public | Sign in (hands off to Auth0) |
+| `/signup` | public | Sign up (hands off to Auth0) |
See `CLAUDE.md` for console conventions (KPI rows, tables, motion tokens, color rules).
diff --git a/app/(app)/install/page.tsx b/app/(app)/install/page.tsx
index 5a8ef47..811c1d7 100644
--- a/app/(app)/install/page.tsx
+++ b/app/(app)/install/page.tsx
@@ -9,8 +9,8 @@ import {
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from "react";
-import { useRouter } from "next/navigation";
import { useAuth } from "@/components/console/AuthContext";
+import { AUTH_SIGNIN_HREF } from "@/lib/console/auth-login";
import CopyButton from "@/components/console/CopyButton";
import HarnessLogo from "@/components/console/HarnessLogo";
import SectionHeader from "@/components/console/SectionHeader";
@@ -545,16 +545,15 @@ function McpServerUrl() {
export default function InstallPage() {
const { isConnected, isLoading } = useAuth();
- const router = useRouter();
// Middleware already sends signed-out requests to /login before this page
// is served (see middleware.ts). This client-side fallback only fires if
// the session lapses while the console is open.
useEffect(() => {
if (!isLoading && !isConnected) {
- router.replace("/login");
+ window.location.replace(AUTH_SIGNIN_HREF);
}
- }, [isLoading, isConnected, router]);
+ }, [isLoading, isConnected]);
if (isLoading) return null;
diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx
index 06ed226..b9d3374 100644
--- a/app/(app)/page.tsx
+++ b/app/(app)/page.tsx
@@ -1,25 +1,25 @@
"use client";
import { useEffect } from "react";
-import { useRouter } from "next/navigation";
import { useAuth } from "@/components/console/AuthContext";
+import { AUTH_SIGNIN_HREF } from "@/lib/console/auth-login";
// Root `/`:
// - signed in → redirect to /home (the console default)
-// - signed out → redirect to /login (the console default entry point)
-// Explore is still reachable at /explore for anyone who lands there directly;
-// it just isn't the default landing any more.
+// - signed out → /login
export default function RootPage() {
const { isConnected, isLoading, user } = useAuth();
- const router = useRouter();
const signedIn = isConnected && !!user;
useEffect(() => {
if (isLoading) return;
- router.replace(signedIn ? "/home" : "/login");
- }, [isLoading, signedIn, router]);
+ if (signedIn) {
+ window.location.replace("/home");
+ return;
+ }
+ window.location.replace(AUTH_SIGNIN_HREF);
+ }, [isLoading, signedIn]);
- // Nothing renders here — `/` is a pure redirect in both auth states.
return null;
}
diff --git a/app/(app)/waitlist/page.tsx b/app/(app)/waitlist/page.tsx
index 969e838..b02a543 100644
--- a/app/(app)/waitlist/page.tsx
+++ b/app/(app)/waitlist/page.tsx
@@ -1,6 +1,7 @@
import { auth0 } from "@/lib/auth0";
import ConsolePageHeader from "@/components/console/ConsolePageHeader";
import SectionHeader from "@/components/console/SectionHeader";
+import { consoleSignInHref } from "@/lib/console/auth-login";
import { isEmailAllowlisted } from "@/lib/console/email-allowlist";
export const dynamic = "force-dynamic";
@@ -27,7 +28,7 @@ export default async function WaitlistPage() {
{email}
) : (
-
+
Sign in
{" "}
to join the waitlist with your account email.
diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
index 7834ef9..b10eb3b 100644
--- a/app/(auth)/layout.tsx
+++ b/app/(auth)/layout.tsx
@@ -13,7 +13,7 @@ export default function ConsoleAuthLayout({
}) {
return (
-
+
{children}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index 6302b46..9e07de7 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -1,27 +1,37 @@
import { redirect } from "next/navigation";
-
import { auth0 } from "@/lib/auth0";
+import { authLoginHref, safeReturnTo } from "@/lib/console/auth-login";
import LoginPage from "@/components/console/LoginPage";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Sign in — Livepeer Early Access",
+};
+
const MCP_CALLBACK_PATH = "/api/mcp/oauth/callback";
export default async function LoginRoute({
- searchParams
+ searchParams,
}: {
searchParams: Promise<{
mcp_oauth?: string;
+ returnTo?: string;
}>;
}) {
const params = await searchParams;
const mcpOauth = params.mcp_oauth === "1";
+ const returnTo = safeReturnTo(params.returnTo);
const session = await auth0.getSession();
if (session) {
- if (mcpOauth) {
- redirect(MCP_CALLBACK_PATH);
- }
- redirect("/home");
+ redirect(mcpOauth ? MCP_CALLBACK_PATH : returnTo);
+ }
+
+ // MCP flow must go directly to Auth0 — no interactive UI step.
+ if (mcpOauth) {
+ redirect(authLoginHref({ returnTo: MCP_CALLBACK_PATH }));
}
- return
;
+ return
;
}
diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx
index eb72ea9..b1dc73f 100644
--- a/app/(auth)/signup/page.tsx
+++ b/app/(auth)/signup/page.tsx
@@ -1,12 +1,21 @@
+import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { auth0 } from "@/lib/auth0";
+import { safeReturnTo } from "@/lib/console/auth-login";
import LoginPage from "@/components/console/LoginPage";
-export default async function SignupRoute() {
- const session = await auth0.getSession();
- if (session) {
- redirect("/home");
- }
+export const metadata: Metadata = {
+ title: "Sign up — Livepeer Early Access",
+};
- return
;
+export default async function SignupRoute({
+ searchParams,
+}: {
+ searchParams: Promise<{ returnTo?: string }>;
+}) {
+ const params = await searchParams;
+ const returnTo = safeReturnTo(params.returnTo);
+ const session = await auth0.getSession();
+ if (session) redirect(returnTo);
+ return
;
}
diff --git a/app/api/mcp/oauth/callback/route.ts b/app/api/mcp/oauth/callback/route.ts
index af03995..426ce46 100644
--- a/app/api/mcp/oauth/callback/route.ts
+++ b/app/api/mcp/oauth/callback/route.ts
@@ -25,8 +25,8 @@ export async function GET(req: NextRequest) {
const session = await auth0.getSession();
const sub = session?.user?.sub?.trim();
if (!session || !sub) {
- const login = new URL("/login", origin);
- login.searchParams.set("mcp_oauth", "1");
+ const login = new URL("/auth/login", origin);
+ login.searchParams.set("returnTo", "/api/mcp/oauth/callback");
return NextResponse.redirect(login);
}
diff --git a/app/authorize/route.ts b/app/authorize/route.ts
index 0e84e39..3c75be9 100644
--- a/app/authorize/route.ts
+++ b/app/authorize/route.ts
@@ -88,7 +88,7 @@ export async function GET(req: NextRequest) {
return json(req, 503, { error: "temporarily_unavailable" });
}
- const login = consoleLoginUrl(req, nonce);
+ const login = consoleLoginUrl(req);
const response = NextResponse.redirect(login, 302);
response.cookies.set(PKCE_COOKIE, pending, pkceCookieOptions());
for (const [k, v] of Object.entries(corsHeaders(req))) {
diff --git a/branding/auth0/branding.json b/branding/auth0/branding.json
new file mode 100644
index 0000000..9525777
--- /dev/null
+++ b/branding/auth0/branding.json
@@ -0,0 +1,8 @@
+{
+ "logo_url": "https://livepeer-console.vercel.app/icon.svg",
+ "favicon_url": "https://livepeer-console.vercel.app/icon.svg",
+ "colors": {
+ "primary": "#111111",
+ "page_background": "#FFFFFF"
+ }
+}
diff --git a/branding/auth0/text-login-en.json b/branding/auth0/text-login-en.json
new file mode 100644
index 0000000..91fa662
--- /dev/null
+++ b/branding/auth0/text-login-en.json
@@ -0,0 +1,7 @@
+{
+ "login": {
+ "title": "Log in to Livepeer Early Access",
+ "description": " ",
+ "buttonText": "Sign in"
+ }
+}
diff --git a/branding/auth0/text-signup-en.json b/branding/auth0/text-signup-en.json
new file mode 100644
index 0000000..1247d8e
--- /dev/null
+++ b/branding/auth0/text-signup-en.json
@@ -0,0 +1,7 @@
+{
+ "signup": {
+ "title": "Get started with Livepeer",
+ "description": " ",
+ "buttonText": "Sign up"
+ }
+}
diff --git a/branding/auth0/text-signup-id-en.json b/branding/auth0/text-signup-id-en.json
new file mode 100644
index 0000000..a427cc9
--- /dev/null
+++ b/branding/auth0/text-signup-id-en.json
@@ -0,0 +1,6 @@
+{
+ "signup-id": {
+ "title": "Get started with Livepeer",
+ "description": " "
+ }
+}
diff --git a/branding/auth0/text-signup-password-en.json b/branding/auth0/text-signup-password-en.json
new file mode 100644
index 0000000..20e91eb
--- /dev/null
+++ b/branding/auth0/text-signup-password-en.json
@@ -0,0 +1,6 @@
+{
+ "signup-password": {
+ "title": "Get started with Livepeer",
+ "description": " "
+ }
+}
diff --git a/branding/auth0/theme.json b/branding/auth0/theme.json
new file mode 100644
index 0000000..5732ed2
--- /dev/null
+++ b/branding/auth0/theme.json
@@ -0,0 +1,57 @@
+{
+ "displayName": "Livepeer Console",
+ "colors": {
+ "primary_button": "#111111",
+ "primary_button_label": "#FFFFFF",
+ "secondary_button_border": "#E4E4E4",
+ "secondary_button_label": "#111111",
+ "base_focus_color": "#40BF86",
+ "base_hover_color": "#F5F5F5",
+ "links_focused_components": "#40BF86",
+ "header": "#111111",
+ "body_text": "#111111",
+ "widget_background": "#FFFFFF",
+ "widget_border": "#E4E4E4",
+ "input_labels_placeholders": "#9CA3AF",
+ "input_filled_text": "#111111",
+ "input_border": "#E4E4E4",
+ "input_background": "#FFFFFF",
+ "icons": "#9CA3AF",
+ "error": "#EF4444",
+ "success": "#40BF86"
+ },
+ "fonts": {
+ "font_url": "https://rsms.me/inter/font-files/InterVariable.woff2",
+ "links_style": "normal",
+ "reference_text_size": 16,
+ "title": { "size": 112.5, "bold": false },
+ "subtitle": { "size": 87.5, "bold": false },
+ "body_text": { "size": 87.5, "bold": false },
+ "buttons_text": { "size": 87.5, "bold": false },
+ "input_labels": { "size": 87.5, "bold": false },
+ "links": { "size": 75, "bold": false }
+ },
+ "borders": {
+ "button_border_weight": 0,
+ "buttons_style": "rounded",
+ "button_border_radius": 4,
+ "input_border_weight": 1,
+ "inputs_style": "rounded",
+ "input_border_radius": 4,
+ "widget_corner_radius": 4,
+ "widget_border_weight": 1,
+ "show_widget_shadow": true
+ },
+ "widget": {
+ "logo_position": "center",
+ "logo_url": "https://livepeer-console.vercel.app/icon.svg",
+ "logo_height": 36,
+ "header_text_alignment": "center",
+ "social_buttons_layout": "bottom"
+ },
+ "page_background": {
+ "background_color": "#FFFFFF",
+ "background_image_url": "",
+ "page_layout": "center"
+ }
+}
diff --git a/components/console/ConsolePageHeader.tsx b/components/console/ConsolePageHeader.tsx
index 1ba5583..dcd2796 100644
--- a/components/console/ConsolePageHeader.tsx
+++ b/components/console/ConsolePageHeader.tsx
@@ -1,9 +1,13 @@
"use client";
import type { ReactNode } from "react";
-import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "@/components/console/AuthContext";
+import {
+ AUTH_SIGNIN_HREF,
+ AUTH_SIGNUP_HREF,
+ isConsoleAuthPath,
+} from "@/lib/console/auth-login";
import ScopeChip, { type PageScope } from "@/components/console/ScopeChip";
interface ConsolePageHeaderProps {
@@ -34,8 +38,8 @@ interface ConsolePageHeaderProps {
* and right-aligned actions. Information density comes from the content; this
* bar is pure chrome and stays out of the way.
*
- * When the user is signed out (and not already on the `/login`
- * auth route), a `Sign in` / `Sign up` pair is appended to the right
+ * When the user is signed out (and not already on /login or /signup),
+ * a `Sign in` / `Sign up` pair is appended to the right
* actions cluster — mirrors the design prototype's `PageHead` injection
* (`auth.authed === false && !auth.isAuthRoute`). A faint divider sits
* between the page's own actions and the auth CTAs when both exist.
@@ -49,8 +53,7 @@ export default function ConsolePageHeader({
}: ConsolePageHeaderProps) {
const { isConnected, isLoading } = useAuth();
const pathname = usePathname() ?? "";
- const isAuthRoute =
- pathname.startsWith("/login") || pathname.startsWith("/signup");
+ const isAuthRoute = isConsoleAuthPath(pathname);
// Hide auth CTAs while auth state is still resolving (one frame on first
// paint) to avoid flashing them in for connected users.
const showAuthCTAs = !isLoading && !isConnected && !isAuthRoute;
@@ -84,18 +87,18 @@ export default function ConsolePageHeader({
)}
{showAuthCTAs && (
<>
-
Sign in
-
-
+
Sign up
-
+
>
)}
diff --git a/components/console/ConsoleSidebar.tsx b/components/console/ConsoleSidebar.tsx
index ab5839c..6a66a36 100644
--- a/components/console/ConsoleSidebar.tsx
+++ b/components/console/ConsoleSidebar.tsx
@@ -2,11 +2,12 @@
import { useState } from "react";
import Link from "next/link";
-import { usePathname, useRouter } from "next/navigation";
+import { usePathname } from "next/navigation";
import { EllipsisVertical } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { LivepeerLockup } from "@/components/design-system/LivepeerLogo";
import { PORTAL_NAV_ITEMS } from "@/lib/constants";
+import { AUTH_SIGNIN_HREF, AUTH_SIGNUP_HREF } from "@/lib/console/auth-login";
import { useAuth, type ConsoleUser } from "@/components/console/AuthContext";
import Drawer from "@/components/design-system/Drawer";
import NavLink from "@/components/console/NavLink";
@@ -278,7 +279,6 @@ function SidebarNav({
}
function SignedOutSidebarContent({ onNavigate }: { onNavigate?: () => void }) {
- const router = useRouter();
const publicItems = PORTAL_NAV_ITEMS.filter((i) => i.zone === "network");
return (
@@ -319,26 +319,20 @@ function SignedOutSidebarContent({ onNavigate }: { onNavigate?: () => void }) {
No credit card. Spin up in 30 seconds with an API key.
@@ -377,18 +371,12 @@ function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
function MobileSidebarContent({ onNavigate }: { onNavigate: () => void }) {
const { isConnected, isLoading, user, disconnect } = useAuth();
- const router = useRouter();
const pathname = usePathname();
const isSignedOut = !isLoading && !isConnected;
const items = PORTAL_NAV_ITEMS.filter((i) =>
isSignedOut ? i.zone === "network" : i.zone !== "network"
);
- const navigateTo = (href: string) => {
- router.push(href);
- onNavigate();
- };
-
return (