diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d24078a --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Root compose defaults (safe for local self-host / dev) +# Copy to .env to override. + +# --- app ports (host) --- +FRONTEND_PORT=3000 +BACKEND_PORT=8000 + +# --- database --- +POSTGRES_DB=openclaw_agency +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +# --- redis --- +REDIS_PORT=6379 + +# --- backend settings (see backend/.env.example for full list) --- +CORS_ORIGINS=http://localhost:3000 +DB_AUTO_MIGRATE=true + +# --- frontend settings --- +# Public URL used by the browser to reach the API +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7401f9e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + # Allow maintainers to manually kick CI when GitHub doesn't create a run for a new head SHA. + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: python -m pip install --upgrade pip uv + + - name: Cache uv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + backend/.venv + key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: make setup + + - name: Run checks + env: + # Keep CI builds deterministic and secretless. + NEXT_TELEMETRY_DISABLED: "1" + run: make check diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..df5a587 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,44 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# System deps (keep minimal) +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install uv (https://github.com/astral-sh/uv) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:${PATH}" + +# --- deps layer --- +FROM base AS deps + +# Copy only dependency metadata first for better build caching +COPY pyproject.toml uv.lock ./ + +# Create venv and sync deps (including runtime) +RUN uv sync --frozen --no-dev + +# --- runtime --- +FROM base AS runtime + +# Copy virtual environment from deps stage +COPY --from=deps /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:${PATH}" + +# Copy app source +COPY alembic ./alembic +COPY alembic.ini ./alembic.ini +COPY app ./app + +# Default API port +EXPOSE 8000 + +# Run the API +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f6a43a9 --- /dev/null +++ b/compose.yml @@ -0,0 +1,67 @@ +name: openclaw-mission-control + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-openclaw_agency} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "${POSTGRES_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 3s + retries: 20 + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + ports: + - "${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 20 + + backend: + build: + context: ./backend + env_file: + - ./backend/.env.example + environment: + # Override localhost defaults for container networking + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-openclaw_agency} + REDIS_URL: redis://redis:6379/0 + CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000} + DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "${BACKEND_PORT:-8000}:8000" + + frontend: + build: + context: ./frontend + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + env_file: + - ./frontend/.env.example + environment: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + depends_on: + - backend + ports: + - "${FRONTEND_PORT:-3000}:3000" + +volumes: + postgres_data: + redis_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..4a87dcd --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-alpine AS builder +WORKDIR /app + +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 +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# 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 + +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/next.config.ts ./next.config.ts + +EXPOSE 3000 + +CMD ["npm", "run", "start"] diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 377a775..408079c 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index 29706e3..76f624d 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -1,10 +1,12 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 92fec06..3d08640 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index ea5cb45..3a09a95 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -1,10 +1,12 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, diff --git a/frontend/src/app/boards/[boardId]/approvals/page.tsx b/frontend/src/app/boards/[boardId]/approvals/page.tsx index a362a64..531ca13 100644 --- a/frontend/src/app/boards/[boardId]/approvals/page.tsx +++ b/frontend/src/app/boards/[boardId]/approvals/page.tsx @@ -1,8 +1,10 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useParams } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk"; import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index a3f9813..70b9b37 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { X } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 1784834..1725560 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Activity, ArrowUpRight, diff --git a/frontend/src/app/boards/new/page.tsx b/frontend/src/app/boards/new/page.tsx index c9fb9ae..5b188ec 100644 --- a/frontend/src/app/boards/new/page.tsx +++ b/frontend/src/app/boards/new/page.tsx @@ -1,10 +1,12 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { useCreateBoardApiV1BoardsPost } from "@/api/generated/boards/boards"; diff --git a/frontend/src/app/boards/page.tsx b/frontend/src/app/boards/page.tsx index 2234ef8..7e979e8 100644 --- a/frontend/src/app/boards/page.tsx +++ b/frontend/src/app/boards/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, flexRender, diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index be3ef59..b21b5f1 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -1,8 +1,10 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo } from "react"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Area, AreaChart, diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 9cd78a8..8f1233b 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/gateways/[gatewayId]/page.tsx b/frontend/src/app/gateways/[gatewayId]/page.tsx index a8f4133..726d4a1 100644 --- a/frontend/src/app/gateways/[gatewayId]/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index f310fd7..1c99568 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx index ca5ac18..5ede20a 100644 --- a/frontend/src/app/gateways/page.tsx +++ b/frontend/src/app/gateways/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 146d8a1..905dfae 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,9 +3,9 @@ import "./globals.css"; import type { Metadata } from "next"; import type { ReactNode } from "react"; -import { ClerkProvider } from "@clerk/nextjs"; import { IBM_Plex_Sans, Sora } from "next/font/google"; +import { AuthProvider } from "@/components/providers/AuthProvider"; import { QueryProvider } from "@/components/providers/QueryProvider"; export const metadata: Metadata = { @@ -29,14 +29,14 @@ const headingFont = Sora({ export default function RootLayout({ children }: { children: ReactNode }) { return ( - - - + + + {children} - - - + + + ); } diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index 21bd936..66613ef 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -1,9 +1,11 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@/auth/clerk"; import { Globe, Info, RotateCcw, Save, User } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx new file mode 100644 index 0000000..691b3a0 --- /dev/null +++ b/frontend/src/auth/clerk.tsx @@ -0,0 +1,70 @@ +"use client"; + +import type { ReactNode } from "react"; + +// NOTE: We intentionally keep this file very small and dependency-free. +// It provides CI/secretless-build safe fallbacks for Clerk hooks/components. + +import { + ClerkProvider, + SignedIn as ClerkSignedIn, + SignedOut as ClerkSignedOut, + SignInButton as ClerkSignInButton, + SignOutButton as ClerkSignOutButton, + useAuth as clerkUseAuth, + useUser as clerkUseUser, +} from "@clerk/nextjs"; + +import type { ComponentProps } from "react"; + +export function isClerkEnabled(): boolean { + // Invariant: Clerk is disabled ONLY when the publishable key is absent. + // If a key is present, we assume Clerk is intended to be enabled and we let + // Clerk fail fast if the key is invalid/misconfigured. + return Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY); +} + +export function SignedIn(props: { children: ReactNode }) { + if (!isClerkEnabled()) return null; + return {props.children}; +} + +export function SignedOut(props: { children: ReactNode }) { + if (!isClerkEnabled()) return <>{props.children}; + return {props.children}; +} + +// Keep the same prop surface as Clerk components so call sites don't need edits. +export function SignInButton(props: ComponentProps) { + if (!isClerkEnabled()) return null; + return ; +} + +export function SignOutButton(props: ComponentProps) { + if (!isClerkEnabled()) return null; + return ; +} + +export function useUser() { + if (!isClerkEnabled()) { + return { isLoaded: true, isSignedIn: false, user: null } as const; + } + return clerkUseUser(); +} + +export function useAuth() { + if (!isClerkEnabled()) { + return { + isLoaded: true, + isSignedIn: false, + userId: null, + sessionId: null, + getToken: async () => null, + } as const; + } + return clerkUseAuth(); +} + +// Re-export ClerkProvider for places that want to mount it, but strongly prefer +// gating via isClerkEnabled() at call sites. +export { ClerkProvider }; diff --git a/frontend/src/components/BoardApprovalsPanel.tsx b/frontend/src/components/BoardApprovalsPanel.tsx index 0ed470d..264d73c 100644 --- a/frontend/src/components/BoardApprovalsPanel.tsx +++ b/frontend/src/components/BoardApprovalsPanel.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; -import { useAuth } from "@clerk/nextjs"; +import { useAuth } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; import { Clock } from "lucide-react"; diff --git a/frontend/src/components/organisms/LandingHero.tsx b/frontend/src/components/organisms/LandingHero.tsx index 38db84f..349bd48 100644 --- a/frontend/src/components/organisms/LandingHero.tsx +++ b/frontend/src/components/organisms/LandingHero.tsx @@ -1,6 +1,6 @@ "use client"; -import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk"; import { HeroCopy } from "@/components/molecules/HeroCopy"; import { Button } from "@/components/ui/button"; diff --git a/frontend/src/components/organisms/UserMenu.tsx b/frontend/src/components/organisms/UserMenu.tsx index 0918924..661fbea 100644 --- a/frontend/src/components/organisms/UserMenu.tsx +++ b/frontend/src/components/organisms/UserMenu.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import { SignOutButton, useUser } from "@clerk/nextjs"; +import { SignOutButton, useUser } from "@/auth/clerk"; import { LogOut } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; diff --git a/frontend/src/components/providers/AuthProvider.tsx b/frontend/src/components/providers/AuthProvider.tsx new file mode 100644 index 0000000..4da743e --- /dev/null +++ b/frontend/src/components/providers/AuthProvider.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { ClerkProvider } from "@clerk/nextjs"; +import type { ReactNode } from "react"; + +function isLikelyValidClerkPublishableKey(key: string | undefined): key is string { + if (!key) return false; + // Clerk publishable keys look like: pk_test_... or pk_live_... + // In CI we want builds to stay secretless; if the key isn't present/valid, + // we skip Clerk entirely so `next build` can prerender. + // + // Note: Clerk appears to validate key *contents*, not just shape. We therefore + // use a conservative heuristic to avoid treating obvious placeholders as valid. + const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); + if (!m) return false; + const body = m[2]; + if (body.length < 16) return false; + if (/^0+$/.test(body)) return false; + return true; +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + + if (!isLikelyValidClerkPublishableKey(publishableKey)) { + return <>{children}; + } + + return {children}; +} diff --git a/frontend/src/components/templates/DashboardShell.tsx b/frontend/src/components/templates/DashboardShell.tsx index ace048a..04b516a 100644 --- a/frontend/src/components/templates/DashboardShell.tsx +++ b/frontend/src/components/templates/DashboardShell.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; -import { SignedIn, useUser } from "@clerk/nextjs"; +import { SignedIn, useUser } from "@/auth/clerk"; import { BrandMark } from "@/components/atoms/BrandMark"; import { UserMenu } from "@/components/organisms/UserMenu"; diff --git a/frontend/src/components/templates/LandingShell.tsx b/frontend/src/components/templates/LandingShell.tsx index 59760fc..32d6af7 100644 --- a/frontend/src/components/templates/LandingShell.tsx +++ b/frontend/src/components/templates/LandingShell.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; -import { SignedIn } from "@clerk/nextjs"; +import { SignedIn } from "@/auth/clerk"; import { BrandMark } from "@/components/atoms/BrandMark"; import { UserMenu } from "@/components/organisms/UserMenu"; diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts index 7543980..75b3086 100644 --- a/frontend/src/proxy.ts +++ b/frontend/src/proxy.ts @@ -1,6 +1,9 @@ +import { NextResponse } from "next/server"; import { clerkMiddleware } from "@clerk/nextjs/server"; -export default clerkMiddleware(); +const isClerkEnabled = () => Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY); + +export default isClerkEnabled() ? clerkMiddleware() : () => NextResponse.next(); export const config = { matcher: [