From 713e9ad6b53a32d086c5f362f4bc20623567067b Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Tue, 31 Mar 2026 22:19:57 +0530
Subject: [PATCH 01/10] feat(*): google integration flow

---
 .env.example                                  |   3 +-
 app/(main)/configurations/page.tsx            |  28 +-
 .../configurations/prompt-editor/page.tsx     |   8 +-
 app/(main)/datasets/page.tsx                  | 134 +----
 app/(main)/document/page.tsx                  |  17 +-
 app/(main)/evaluations/[id]/page.tsx          |  55 +-
 app/(main)/evaluations/page.tsx               | 161 ++---
 app/(main)/knowledge-base/page.tsx            | 565 ++++++++----------
 app/(main)/settings/credentials/page.tsx      |  34 +-
 app/(main)/settings/onboarding/page.tsx       |   4 +-
 app/(main)/speech-to-text/page.tsx            | 110 ++--
 app/(main)/text-to-speech/page.tsx            | 113 ++--
 app/Providers.tsx                             |  20 +
 app/api/auth/google/route.ts                  |  50 ++
 app/api/auth/refresh/route.ts                 |  19 +
 app/components/ConfigModal.tsx                |  53 +-
 app/components/ConfigSelector.tsx             |   2 -
 app/components/Modal.tsx                      |   2 +-
 app/components/PageHeader.tsx                 | 102 ++--
 app/components/Sidebar.tsx                    | 289 ++++++++-
 app/components/SimplifiedConfigEditor.tsx     | 100 ++--
 app/components/auth/FeatureGateModal.tsx      |  31 +
 app/components/auth/LoginModal.tsx            | 129 ++++
 app/components/auth/ProtectedPage.tsx         |  38 ++
 app/components/auth/index.ts                  |   3 +
 app/components/document/DocumentListing.tsx   |  21 +-
 app/components/document/DocumentPreview.tsx   |   6 +-
 app/components/evaluations/DatasetsTab.tsx    |  44 +-
 app/components/evaluations/EvaluationsTab.tsx |  62 +-
 .../speech-to-text/ModelComparisonCard.tsx    |   1 -
 app/hooks/useConfigs.ts                       |  22 +-
 app/hooks/usePaginatedList.ts                 |  16 +-
 app/layout.tsx                                |  10 +-
 app/lib/apiClient.ts                          | 106 +++-
 app/lib/context/AuthContext.tsx               | 111 +++-
 app/lib/types/auth.ts                         |  42 ++
 next.config.ts                                |   9 +-
 package-lock.json                             |  11 +
 package.json                                  |   1 +
 39 files changed, 1435 insertions(+), 1097 deletions(-)
 create mode 100644 app/Providers.tsx
 create mode 100644 app/api/auth/google/route.ts
 create mode 100644 app/api/auth/refresh/route.ts
 create mode 100644 app/components/auth/FeatureGateModal.tsx
 create mode 100644 app/components/auth/LoginModal.tsx
 create mode 100644 app/components/auth/ProtectedPage.tsx
 create mode 100644 app/components/auth/index.ts
 create mode 100644 app/lib/types/auth.ts

diff --git a/.env.example b/.env.example
index ae81a04..3e6995f 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1,2 @@
-BACKEND_URL=http://localhost:8000
\ No newline at end of file
+BACKEND_URL=http://localhost:8000
+NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
\ No newline at end of file
diff --git a/app/(main)/configurations/page.tsx b/app/(main)/configurations/page.tsx
index 1e35c43..d9243f0 100644
--- a/app/(main)/configurations/page.tsx
+++ b/app/(main)/configurations/page.tsx
@@ -45,8 +45,8 @@ export default function ConfigLibraryPage() {
     Record<string, number>
   >({});
   const { sidebarCollapsed } = useApp();
-  const { activeKey } = useAuth();
-  const apiKey = activeKey?.key;
+  const { activeKey, isAuthenticated } = useAuth();
+  const apiKey = activeKey?.key ?? "";
   const [searchInput, setSearchInput] = useState("");
   const [debouncedQuery, setDebouncedQuery] = useState("");
   const [columnCount, setColumnCount] = useState(3);
@@ -100,11 +100,11 @@ export default function ConfigLibraryPage() {
 
   useEffect(() => {
     const fetchEvaluationCounts = async () => {
-      if (!activeKey) return;
+      if (!isAuthenticated) return;
       try {
         const data = await apiFetch<EvalJob[] | { data: EvalJob[] }>(
           "/api/evaluations",
-          activeKey.key,
+          apiKey,
         );
         const jobs: EvalJob[] = Array.isArray(data) ? data : data.data || [];
         const counts: Record<string, number> = {};
@@ -129,7 +129,7 @@ export default function ConfigLibraryPage() {
         await existing;
         return;
       }
-      if (!apiKey) return;
+      if (!isAuthenticated) return;
 
       const loadPromise = (async () => {
         const res = await apiFetch<{
@@ -144,7 +144,7 @@ export default function ConfigLibraryPage() {
       pendingVersionLoads.set(configId, loadPromise);
       await loadPromise;
     },
-    [apiKey],
+    [apiKey, isAuthenticated],
   );
 
   const loadSingleVersion = useCallback(
@@ -152,7 +152,7 @@ export default function ConfigLibraryPage() {
       const key = `${configId}:${version}`;
       const existing = pendingSingleVersionLoads.get(key);
       if (existing) return existing;
-      if (!apiKey) return null;
+      if (!isAuthenticated) return null;
 
       const configPublic =
         configs.find((c) => c.id === configId) ??
@@ -179,7 +179,7 @@ export default function ConfigLibraryPage() {
       pendingSingleVersionLoads.set(key, loadPromise);
       return loadPromise;
     },
-    [apiKey, configs],
+    [apiKey, configs, isAuthenticated],
   );
 
   const handleCreateNew = () => {
@@ -277,17 +277,7 @@ export default function ConfigLibraryPage() {
             ) : error ? (
               <div className="rounded-lg p-6 text-center bg-[#fef2f2] border border-[#fecaca]">
                 <WarningTriangleIcon className="w-12 h-12 mx-auto mb-3 text-[#dc2626]" />
-                <p className="text-sm font-medium text-[#dc2626]">{error}</p>
-                <button
-                  onClick={() => router.push("/keystore")}
-                  className="mt-4 px-4 py-2 rounded-md text-sm font-medium transition-colors"
-                  style={{
-                    backgroundColor: colors.accent.primary,
-                    color: colors.bg.primary,
-                  }}
-                >
-                  Go to Keystore
-                </button>
+                <p className="text-sm font-medium text-status-error">{error}</p>
               </div>
             ) : configs.length === 0 ? (
               <div
diff --git a/app/(main)/configurations/prompt-editor/page.tsx b/app/(main)/configurations/prompt-editor/page.tsx
index 995f8c4..47e4e86 100644
--- a/app/(main)/configurations/prompt-editor/page.tsx
+++ b/app/(main)/configurations/prompt-editor/page.tsx
@@ -47,7 +47,7 @@ function PromptEditorContent() {
   const toast = useToast();
   const searchParams = useSearchParams();
   const { sidebarCollapsed } = useApp();
-  const { activeKey } = useAuth();
+  const { activeKey, isAuthenticated } = useAuth();
   const urlConfigId = searchParams.get("config");
   const urlVersion = searchParams.get("version");
   const showHistory = searchParams.get("history") === "true";
@@ -265,9 +265,9 @@ function PromptEditorContent() {
       return;
     }
 
-    const apiKey = activeKey?.key;
-    if (!apiKey) {
-      toast.error("No API key found. Please add an API key in the Keystore.");
+    const apiKey = activeKey?.key ?? "";
+    if (!isAuthenticated) {
+      toast.error("Please log in to save configurations.");
       return;
     }
 
diff --git a/app/(main)/datasets/page.tsx b/app/(main)/datasets/page.tsx
index ad241bb..737a857 100644
--- a/app/(main)/datasets/page.tsx
+++ b/app/(main)/datasets/page.tsx
@@ -10,6 +10,7 @@ import { useState, useEffect } from "react";
 
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { useApp } from "@/app/lib/context/AppContext";
+import { apiFetch } from "@/app/lib/apiClient";
 import { APIKey } from "@/app/lib/types/credentials";
 import Sidebar from "@/app/components/Sidebar";
 import PageHeader from "@/app/components/PageHeader";
@@ -41,7 +42,7 @@ export default function Datasets() {
   const [isUploading, setIsUploading] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
-  const { activeKey: apiKey } = useAuth();
+  const { activeKey: apiKey, isAuthenticated } = useAuth();
 
   // Pagination state
   const [currentPage, setCurrentPage] = useState(1);
@@ -52,12 +53,11 @@ export default function Datasets() {
     if (apiKey) {
       fetchDatasets();
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [apiKey]);
 
   const fetchDatasets = async () => {
-    if (!apiKey) {
-      setError("No API key found. Please add an API key in the Keystore.");
+    if (!isAuthenticated) {
+      setError("Please log in to continue.");
       return;
     }
 
@@ -65,23 +65,10 @@ export default function Datasets() {
     setError(null);
 
     try {
-      const response = await fetch("/api/evaluations/datasets", {
-        method: "GET",
-        headers: {
-          "X-API-KEY": apiKey.key,
-        },
-      });
-
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Failed to fetch datasets: ${response.status}`,
-        );
-      }
-
-      const data = await response.json();
+      const data = await apiFetch<Dataset[] | { data: Dataset[] }>(
+        "/api/evaluations/datasets",
+        apiKey?.key ?? "",
+      );
       const datasetList = Array.isArray(data) ? data : data.data || [];
       setDatasets(datasetList);
     } catch (err: unknown) {
@@ -119,49 +106,29 @@ export default function Datasets() {
       return;
     }
 
-    if (!apiKey) {
-      toast.error("No API key found. Please add an API key in the Keystore.");
+    if (!isAuthenticated) {
+      toast.error("Please log in to continue.");
       return;
     }
 
     setIsUploading(true);
 
     try {
-      // Prepare FormData for upload
       const formData = new FormData();
       formData.append("file", selectedFile);
       formData.append("dataset_name", datasetName.trim());
 
       formData.append("duplication_factor", duplicationFactor || "1");
 
-      // Upload to backend
-      const response = await fetch("/api/evaluations/datasets", {
+      await apiFetch<Dataset>("/api/evaluations/datasets", apiKey?.key ?? "", {
         method: "POST",
         body: formData,
-        headers: {
-          "X-API-KEY": apiKey.key,
-        },
       });
-
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Upload failed with status ${response.status}`,
-        );
-      }
-
-      await response.json();
-      // Refresh datasets list
       await fetchDatasets();
 
-      // Reset form
       setSelectedFile(null);
       setDatasetName("");
       setDuplicationFactor("1");
-
-      // Close modal
       setIsModalOpen(false);
 
       toast.success("Dataset uploaded successfully!");
@@ -176,8 +143,8 @@ export default function Datasets() {
   };
 
   const handleDeleteDataset = async (datasetId: number) => {
-    if (!apiKey) {
-      toast.error("No API key found");
+    if (!isAuthenticated) {
+      toast.error("Please log in to continue");
       return;
     }
 
@@ -187,23 +154,14 @@ export default function Datasets() {
     }
 
     try {
-      const response = await fetch(`/api/evaluations/datasets/${datasetId}`, {
-        method: "DELETE",
-        headers: {
-          "X-API-KEY": apiKey.key,
+      await apiFetch(
+        `/api/evaluations/datasets/${datasetId}`,
+        apiKey?.key ?? "",
+        {
+          method: "DELETE",
         },
-      });
-
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Delete failed with status ${response.status}`,
-        );
-      }
+      );
 
-      // Refresh datasets list
       await fetchDatasets();
       toast.success("Dataset deleted successfully");
     } catch (error) {
@@ -214,7 +172,6 @@ export default function Datasets() {
     }
   };
 
-  // Pagination calculations
   const indexOfLastItem = currentPage * itemsPerPage;
   const indexOfFirstItem = indexOfLastItem - itemsPerPage;
   const currentDatasets = datasets.slice(indexOfFirstItem, indexOfLastItem);
@@ -223,26 +180,17 @@ export default function Datasets() {
   const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
 
   return (
-    <div
-      className="w-full h-screen flex flex-col"
-      style={{ backgroundColor: "#fafafa" }}
-    >
+    <div className="w-full h-screen flex flex-col bg-bg-secondary">
       <div className="flex flex-1 overflow-hidden">
-        {/* Sidebar */}
         <Sidebar collapsed={sidebarCollapsed} activeRoute="/datasets" />
 
-        {/* Main Content */}
         <div className="flex-1 flex flex-col overflow-hidden">
           <PageHeader
             title="Datasets"
             subtitle="Manage your evaluation datasets"
           />
 
-          {/* Content Area */}
-          <div
-            className="flex-1 overflow-auto p-6"
-            style={{ backgroundColor: "#fafafa" }}
-          >
+          <div className="flex-1 overflow-auto p-6 bg-bg-secondary">
             <div className="max-w-6xl mx-auto space-y-6">
               <DatasetListing
                 datasets={currentDatasets}
@@ -260,7 +208,6 @@ export default function Datasets() {
         </div>
       </div>
 
-      {/* Upload Dataset Modal */}
       {isModalOpen && (
         <UploadDatasetModal
           selectedFile={selectedFile}
@@ -365,42 +312,9 @@ function DatasetListing({
             <p className="text-sm">Loading datasets...</p>
           </div>
         ) : !apiKey ? (
-          <div
-            className="text-center py-12"
-            style={{ color: "hsl(330, 3%, 49%)" }}
-          >
-            <svg
-              className="mx-auto h-12 w-12 mb-4"
-              fill="none"
-              viewBox="0 0 24 24"
-              stroke="currentColor"
-            >
-              <path
-                strokeLinecap="round"
-                strokeLinejoin="round"
-                strokeWidth={2}
-                d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
-              />
-            </svg>
-            <p
-              className="font-medium mb-2"
-              style={{ color: "hsl(330, 3%, 19%)" }}
-            >
-              No API key found
-            </p>
-            <p className="text-sm mb-4">
-              Please add an API key in the Keystore to manage datasets
-            </p>
-            <a
-              href="/keystore"
-              className="inline-block px-6 py-2 rounded-md text-sm font-medium transition-colors"
-              style={{
-                backgroundColor: "#171717",
-                color: "hsl(0, 0%, 100%)",
-              }}
-            >
-              Go to Keystore
-            </a>
+          <div className="text-center py-12 text-text-secondary">
+            <p className="font-medium mb-2 text-text-primary">Login required</p>
+            <p className="text-sm">Please log in to manage datasets</p>
           </div>
         ) : error ? (
           <div
diff --git a/app/(main)/document/page.tsx b/app/(main)/document/page.tsx
index 648691e..18e7497 100644
--- a/app/(main)/document/page.tsx
+++ b/app/(main)/document/page.tsx
@@ -33,7 +33,7 @@ export default function DocumentPage() {
   const [isLoadingDocument, setIsLoadingDocument] = useState(false);
   const [selectedFile, setSelectedFile] = useState<File | null>(null);
   const [isUploading, setIsUploading] = useState(false);
-  const { activeKey: apiKey } = useAuth();
+  const { activeKey: apiKey, isAuthenticated } = useAuth();
 
   const {
     items: documents,
@@ -62,7 +62,7 @@ export default function DocumentPage() {
   };
 
   const handleUpload = async () => {
-    if (!apiKey || !selectedFile) return;
+    if (!isAuthenticated || !selectedFile) return;
 
     setIsUploading(true);
 
@@ -72,7 +72,7 @@ export default function DocumentPage() {
 
       const data = await apiFetch<{ data?: { id: string } }>(
         "/api/document",
-        apiKey.key,
+        apiKey?.key ?? "",
         { method: "POST", body: formData },
       );
       if (selectedFile && data.data?.id) {
@@ -102,8 +102,8 @@ export default function DocumentPage() {
   };
 
   const handleDeleteDocument = async (documentId: string) => {
-    if (!apiKey) {
-      toast.error("No API key found");
+    if (!isAuthenticated) {
+      toast.error("Please log in to continue");
       return;
     }
 
@@ -112,7 +112,7 @@ export default function DocumentPage() {
     }
 
     try {
-      await apiFetch(`/api/document/${documentId}`, apiKey.key, {
+      await apiFetch(`/api/document/${documentId}`, apiKey?.key ?? "", {
         method: "DELETE",
       });
 
@@ -131,13 +131,13 @@ export default function DocumentPage() {
   };
 
   const handleSelectDocument = async (doc: Document) => {
-    if (!apiKey) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingDocument(true);
     try {
       const data = await apiFetch<{ data?: Document }>(
         `/api/document/${doc.id}`,
-        apiKey.key,
+        apiKey?.key ?? "",
       );
       const documentDetails: Document =
         data.data ?? (data as unknown as Document);
@@ -182,7 +182,6 @@ export default function DocumentPage() {
                 isLoading={isLoading}
                 isLoadingMore={isLoadingMore}
                 error={error}
-                apiKey={apiKey}
                 scrollRef={scrollRef}
               />
             </div>
diff --git a/app/(main)/evaluations/[id]/page.tsx b/app/(main)/evaluations/[id]/page.tsx
index a9a5843..808bc98 100644
--- a/app/(main)/evaluations/[id]/page.tsx
+++ b/app/(main)/evaluations/[id]/page.tsx
@@ -48,9 +48,9 @@ export default function EvaluationReport() {
   >(undefined);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
-  const { apiKeys } = useAuth();
+  const { apiKeys, isAuthenticated } = useAuth();
+  const apiKey = apiKeys[0]?.key ?? "";
   const { sidebarCollapsed, setSidebarCollapsed } = useApp();
-  const [selectedKeyId, setSelectedKeyId] = useState<string>("");
   const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
   const [exportFormat, setExportFormat] = useState<"row" | "grouped">("row");
   const [isResyncing, setIsResyncing] = useState(false);
@@ -73,18 +73,9 @@ export default function EvaluationReport() {
     return `"${sanitized}"`;
   };
 
-  useEffect(() => {
-    if (apiKeys.length > 0 && !selectedKeyId) {
-      setSelectedKeyId(apiKeys[0].id);
-    }
-  }, [apiKeys, selectedKeyId]);
-
   // Fetch job details
   const fetchJobDetails = useCallback(async () => {
-    if (!selectedKeyId || !jobId) return;
-
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) return;
+    if (!isAuthenticated || !jobId) return;
 
     setIsLoading(true);
     setError(null);
@@ -93,7 +84,7 @@ export default function EvaluationReport() {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         `/api/evaluations/${jobId}?export_format=${exportFormat}`,
-        selectedKey.key,
+        apiKey,
       );
 
       if (data.success === false && data.error) {
@@ -108,14 +99,10 @@ export default function EvaluationReport() {
       setJob(foundJob);
 
       if (foundJob.assistant_id) {
-        fetchAssistantConfig(foundJob.assistant_id, selectedKey.key);
+        fetchAssistantConfig(foundJob.assistant_id);
       }
       if (foundJob.config_id && foundJob.config_version) {
-        fetchConfigInfo(
-          foundJob.config_id,
-          foundJob.config_version,
-          selectedKey.key,
-        );
+        fetchConfigInfo(foundJob.config_id, foundJob.config_version);
       }
     } catch (err: unknown) {
       setError(
@@ -124,10 +111,9 @@ export default function EvaluationReport() {
     } finally {
       setIsLoading(false);
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [apiKeys, selectedKeyId, jobId, exportFormat]);
+  }, [apiKey, isAuthenticated, jobId, exportFormat]);
 
-  const fetchAssistantConfig = async (assistantId: string, apiKey: string) => {
+  const fetchAssistantConfig = async (assistantId: string) => {
     try {
       const result = await apiFetch<{
         success: boolean;
@@ -142,11 +128,7 @@ export default function EvaluationReport() {
     }
   };
 
-  const fetchConfigInfo = async (
-    configId: string,
-    configVersion: number,
-    apiKey: string,
-  ) => {
+  const fetchConfigInfo = async (configId: string, configVersion: number) => {
     try {
       await apiFetch(`/api/configs/${configId}`, apiKey);
       await apiFetch(
@@ -159,8 +141,8 @@ export default function EvaluationReport() {
   };
 
   useEffect(() => {
-    if (selectedKeyId && jobId) fetchJobDetails();
-  }, [selectedKeyId, jobId, fetchJobDetails]);
+    if (isAuthenticated && jobId) fetchJobDetails();
+  }, [isAuthenticated, jobId, fetchJobDetails]);
 
   // Export grouped format CSV
   const exportGroupedCSV = (traces: GroupedTraceItem[]) => {
@@ -303,16 +285,14 @@ export default function EvaluationReport() {
   };
 
   const handleResync = async () => {
-    if (!selectedKeyId || !jobId) return;
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) return;
+    if (!isAuthenticated || !jobId) return;
 
     setIsResyncing(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         `/api/evaluations/${jobId}?get_trace_info=true&resync_score=true&export_format=${exportFormat}`,
-        selectedKey.key,
+        apiKey,
       );
       const foundJob = data.data || data;
       if (!foundJob) throw new Error("Evaluation job not found");
@@ -325,14 +305,9 @@ export default function EvaluationReport() {
       }
 
       setJob(foundJob);
-      if (foundJob.assistant_id)
-        fetchAssistantConfig(foundJob.assistant_id, selectedKey.key);
+      if (foundJob.assistant_id) fetchAssistantConfig(foundJob.assistant_id);
       if (foundJob.config_id && foundJob.config_version)
-        fetchConfigInfo(
-          foundJob.config_id,
-          foundJob.config_version,
-          selectedKey.key,
-        );
+        fetchConfigInfo(foundJob.config_id, foundJob.config_version);
       toast.success("Metrics resynced successfully");
     } catch (error: unknown) {
       toast.error(
diff --git a/app/(main)/evaluations/page.tsx b/app/(main)/evaluations/page.tsx
index e139f92..f00235c 100644
--- a/app/(main)/evaluations/page.tsx
+++ b/app/(main)/evaluations/page.tsx
@@ -8,6 +8,7 @@
 "use client";
 
 import { useState, useEffect, useCallback, Suspense } from "react";
+import { apiFetch } from "@/app/lib/apiClient";
 import { colors } from "@/app/lib/colors";
 import { useSearchParams } from "next/navigation";
 import { Dataset } from "@/app/(main)/datasets/page";
@@ -17,6 +18,7 @@ import TabNavigation from "@/app/components/TabNavigation";
 import { useToast } from "@/app/components/Toast";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { useApp } from "@/app/lib/context/AppContext";
+import { FeatureGateModal, LoginModal } from "@/app/components/auth";
 import Loader from "@/app/components/Loader";
 import DatasetsTab from "@/app/components/evaluations/DatasetsTab";
 import EvaluationsTab from "@/app/components/evaluations/EvaluationsTab";
@@ -37,8 +39,9 @@ function SimplifiedEvalContent() {
   });
 
   const { sidebarCollapsed } = useApp();
-  const { apiKeys } = useAuth();
-  const [selectedKeyId, setSelectedKeyId] = useState<string>("");
+  const { apiKeys, isAuthenticated } = useAuth();
+  const apiKey = apiKeys[0]?.key ?? "";
+  const [showLoginModal, setShowLoginModal] = useState(false);
   const [mounted, setMounted] = useState(false);
 
   useEffect(() => {
@@ -74,39 +77,26 @@ function SimplifiedEvalContent() {
   );
   const [isEvaluating, setIsEvaluating] = useState(false);
 
-  // Set initial selected key from context
-  useEffect(() => {
-    if (apiKeys.length > 0 && !selectedKeyId) {
-      setSelectedKeyId(apiKeys[0].id);
-    }
-  }, [apiKeys, selectedKeyId]);
-
   // Fetch datasets from backend
   const loadStoredDatasets = useCallback(async () => {
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) {
-      console.error("No selected API key found for loading datasets");
-      return;
-    }
+    if (!isAuthenticated) return;
     setIsDatasetsLoading(true);
     try {
-      const response = await fetch("/api/evaluations/datasets", {
-        method: "GET",
-        headers: { "X-API-KEY": selectedKey.key },
-      });
-      if (!response.ok) return;
-      const data = await response.json();
+      const data = await apiFetch<Dataset[] | { data: Dataset[] }>(
+        "/api/evaluations/datasets",
+        apiKey,
+      );
       setStoredDatasets(Array.isArray(data) ? data : data.data || []);
     } catch (e) {
       console.error("Failed to load datasets:", e);
     } finally {
       setIsDatasetsLoading(false);
     }
-  }, [apiKeys, selectedKeyId]);
+  }, [apiKey, isAuthenticated]);
 
   useEffect(() => {
-    if (apiKeys.length > 0 && selectedKeyId) loadStoredDatasets();
-  }, [apiKeys, selectedKeyId, loadStoredDatasets]);
+    if (isAuthenticated) loadStoredDatasets();
+  }, [isAuthenticated, loadStoredDatasets]);
 
   // File selection handler
   const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -173,9 +163,8 @@ function SimplifiedEvalContent() {
       toast.error("Please enter a dataset name");
       return;
     }
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) {
-      toast.error("No API key selected. Please select one in the Keystore.");
+    if (!isAuthenticated) {
+      toast.error("Please log in to create datasets.");
       return;
     }
 
@@ -191,22 +180,14 @@ function SimplifiedEvalContent() {
         formData.append("duplication_factor", duplicationFactor);
       }
 
-      const response = await fetch("/api/evaluations/datasets", {
-        method: "POST",
-        body: formData,
-        headers: { "X-API-KEY": selectedKey.key },
-      });
-
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Upload failed with status ${response.status}`,
-        );
-      }
-
-      const data = await response.json();
+      const data = await apiFetch<{ dataset_id?: number }>(
+        "/api/evaluations/datasets",
+        apiKey,
+        {
+          method: "POST",
+          body: formData,
+        },
+      );
       await loadStoredDatasets();
 
       if (data.dataset_id) {
@@ -220,7 +201,7 @@ function SimplifiedEvalContent() {
       setDuplicationFactor("1");
 
       toast.success("Dataset created successfully!");
-    } catch (error) {
+    } catch (error: unknown) {
       toast.error(
         `Failed to create dataset: ${error instanceof Error ? error.message : "Unknown error"}`,
       );
@@ -231,8 +212,8 @@ function SimplifiedEvalContent() {
 
   // Run evaluation
   const handleRunEvaluation = async () => {
-    if (!selectedKeyId) {
-      toast.error("Please select an API key first");
+    if (!isAuthenticated) {
+      toast.error("Please log in to run evaluations.");
       return;
     }
     if (!selectedDatasetId) {
@@ -248,12 +229,6 @@ function SimplifiedEvalContent() {
       return;
     }
 
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) {
-      toast.error("Selected API key not found");
-      return;
-    }
-
     setIsEvaluating(true);
     try {
       const payload = {
@@ -263,25 +238,14 @@ function SimplifiedEvalContent() {
         config_version: selectedConfigVersion,
       };
 
-      const response = await fetch("/api/evaluations", {
+      const data = await apiFetch<{
+        id?: string;
+        data?: { id?: string };
+        eval_id?: string;
+      }>("/api/evaluations", apiKey, {
         method: "POST",
-        headers: {
-          "X-API-KEY": selectedKey.key,
-          "Content-Type": "application/json",
-        },
         body: JSON.stringify(payload),
       });
-
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Evaluation failed with status ${response.status}`,
-        );
-      }
-
-      const data = await response.json();
       const evalId = data.id || data.data?.id || data.eval_id || "unknown";
 
       setIsEvaluating(false);
@@ -323,51 +287,18 @@ function SimplifiedEvalContent() {
           />
 
           {/* Tab Content */}
-          {!mounted || apiKeys.length === 0 ? (
-            <div
-              className="flex-1 flex items-center justify-center"
-              style={{ backgroundColor: colors.bg.secondary }}
-            >
-              <div className="text-center">
-                <svg
-                  className="mx-auto h-12 w-12 mb-4"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  style={{ color: colors.border }}
-                >
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    strokeWidth={1.5}
-                    d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
-                  />
-                </svg>
-                <p
-                  className="text-sm font-medium mb-1"
-                  style={{ color: colors.text.primary }}
-                >
-                  API key required
-                </p>
-                <p
-                  className="text-xs mb-4"
-                  style={{ color: colors.text.secondary }}
-                >
-                  Add an API key in the Keystore to start creating datasets and
-                  running evaluations
-                </p>
-                <a
-                  href="/keystore"
-                  className="inline-block px-4 py-2 rounded-md text-sm font-medium"
-                  style={{
-                    backgroundColor: colors.accent.primary,
-                    color: "#ffffff",
-                  }}
-                >
-                  Go to Keystore
-                </a>
-              </div>
-            </div>
+          {!mounted || !isAuthenticated ? (
+            <>
+              <FeatureGateModal
+                feature="Text Evaluation"
+                description="Log in to compare model response quality on your datasets across different configs."
+                onLogin={() => setShowLoginModal(true)}
+              />
+              <LoginModal
+                open={showLoginModal}
+                onClose={() => setShowLoginModal(false)}
+              />
+            </>
           ) : activeTab === "datasets" ? (
             <DatasetsTab
               leftPanelWidth={leftPanelWidth}
@@ -390,16 +321,14 @@ function SimplifiedEvalContent() {
               }}
               storedDatasets={storedDatasets}
               isDatasetsLoading={isDatasetsLoading}
-              apiKeys={apiKeys}
-              selectedKeyId={selectedKeyId}
+              apiKey={apiKey}
               loadStoredDatasets={loadStoredDatasets}
               toast={toast}
             />
           ) : (
             <EvaluationsTab
               leftPanelWidth={leftPanelWidth}
-              apiKeys={apiKeys}
-              selectedKeyId={selectedKeyId}
+              apiKey={apiKey}
               storedDatasets={storedDatasets}
               selectedDatasetId={selectedDatasetId}
               setSelectedDatasetId={setSelectedDatasetId}
diff --git a/app/(main)/knowledge-base/page.tsx b/app/(main)/knowledge-base/page.tsx
index 3467d5d..a27c52d 100644
--- a/app/(main)/knowledge-base/page.tsx
+++ b/app/(main)/knowledge-base/page.tsx
@@ -8,6 +8,7 @@ import PageHeader from "@/app/components/PageHeader";
 import Modal from "@/app/components/Modal";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { useApp } from "@/app/lib/context/AppContext";
+import { apiFetch } from "@/app/lib/apiClient";
 
 export interface Document {
   id: string;
@@ -31,6 +32,32 @@ export interface Collection {
   documents?: Document[];
 }
 
+interface JobStatusData {
+  status?: string | null;
+  collection?: { id?: string; knowledge_base_id?: string };
+  collection_id?: string | null;
+}
+
+interface CollectionResponse {
+  data?: Collection[] | Collection;
+}
+
+interface DocumentResponse {
+  data?: Document[];
+}
+
+interface CreateCollectionResponse {
+  data?: { job_id?: string };
+}
+
+interface DeleteCollectionResponse {
+  data?: { job_id?: string };
+}
+
+interface DocumentDetailResponse {
+  data?: Document;
+}
+
 export default function KnowledgeBasePage() {
   const { sidebarCollapsed } = useApp();
   const [collections, setCollections] = useState<Collection[]>([]);
@@ -47,7 +74,7 @@ export default function KnowledgeBasePage() {
   );
   const [showDocPreviewModal, setShowDocPreviewModal] = useState(false);
   const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
-  const { activeKey: apiKey } = useAuth();
+  const { activeKey: apiKey, isAuthenticated } = useAuth();
   const [showAllDocs, setShowAllDocs] = useState(false);
 
   // Polling refs — persist across renders, no stale closures
@@ -227,7 +254,7 @@ export default function KnowledgeBasePage() {
   ): Promise<
     Map<string, { status: string | null; collectionId: string | null }>
   > => {
-    if (!apiKey) return new Map();
+    if (!isAuthenticated) return new Map();
 
     const cache = JSON.parse(localStorage.getItem(CACHE_KEY) || "{}");
 
@@ -261,22 +288,18 @@ export default function KnowledgeBasePage() {
     const results = await Promise.all(
       Array.from(jobIdsToFetch).map(async (jobId) => {
         try {
-          const response = await fetch(`/api/collections/jobs/${jobId}`, {
-            headers: { "X-API-KEY": apiKey.key },
-          });
-
-          if (response.ok) {
-            const result = await response.json();
-            const jobData = result.data || result;
-            const collectionId =
-              jobData.collection?.id || jobData.collection_id || null;
-
-            return {
-              jobId,
-              status: jobData.status || null,
-              collectionId: collectionId,
-            };
-          }
+          const result = await apiFetch<
+            { data?: JobStatusData } & JobStatusData
+          >(`/api/collections/jobs/${jobId}`, apiKey?.key ?? "");
+          const jobData = result.data || result;
+          const collectionId =
+            jobData.collection?.id || jobData.collection_id || null;
+
+          return {
+            jobId,
+            status: jobData.status || null,
+            collectionId: collectionId,
+          };
         } catch (error) {
           console.error("Error fetching job status:", error);
         }
@@ -294,80 +317,74 @@ export default function KnowledgeBasePage() {
   };
 
   // Fetch collections
-  // eslint-disable-next-line react-hooks/exhaustive-deps
+
   const fetchCollections = async () => {
-    if (!apiKey) return;
+    if (!isAuthenticated) return;
 
     setIsLoading(true);
     try {
-      const response = await fetch("/api/collections", {
-        headers: { "X-API-KEY": apiKey.key },
-      });
-
-      if (response.ok) {
-        const result = await response.json();
-        const collections = result.data || [];
+      const result = await apiFetch<CollectionResponse>(
+        "/api/collections",
+        apiKey?.key ?? "",
+      );
+      const collections = (
+        Array.isArray(result.data) ? result.data : []
+      ) as Collection[];
+
+      // Pre-fetch job statuses only for collections that need it
+      const jobStatusMap = await preFetchJobStatuses(collections);
+
+      // Enrich collections with cached names and live status
+      const enrichedCollections = await Promise.all(
+        collections.map((collection: Collection) =>
+          enrichCollectionWithCache(collection, jobStatusMap),
+        ),
+      );
 
-        // Pre-fetch job statuses only for collections that need it
-        const jobStatusMap = await preFetchJobStatuses(collections);
+      // Remove cache entries whose collection no longer exists on the backend
+      const liveIds = new Set<string>(
+        enrichedCollections.map((c: Collection) => c.id),
+      );
+      pruneStaleCache(liveIds);
 
-        // Enrich collections with cached names and live status
-        const enrichedCollections = await Promise.all(
-          collections.map((collection: Collection) =>
-            enrichCollectionWithCache(collection, jobStatusMap),
-          ),
+      // Preserve optimistic entries not yet replaced by a real collection
+      setCollections((prev) => {
+        const fetchedJobIds = new Set(
+          enrichedCollections.map((c: Collection) => c.job_id).filter(Boolean),
         );
-
-        // Remove cache entries whose collection no longer exists on the backend
-        const liveIds = new Set<string>(
-          enrichedCollections.map((c: Collection) => c.id),
+        const activeOptimistic = prev.filter(
+          (c) =>
+            c.id.startsWith("optimistic-") &&
+            (!c.job_id || !fetchedJobIds.has(c.job_id)),
         );
-        pruneStaleCache(liveIds);
-
-        // Preserve optimistic entries not yet replaced by a real collection
-        setCollections((prev) => {
-          const fetchedJobIds = new Set(
-            enrichedCollections
-              .map((c: Collection) => c.job_id)
-              .filter(Boolean),
-          );
-          const activeOptimistic = prev.filter(
-            (c) =>
-              c.id.startsWith("optimistic-") &&
-              (!c.job_id || !fetchedJobIds.has(c.job_id)),
-          );
-          // Sort by inserted_at in descending order (latest first)
-          const combined = [...activeOptimistic, ...enrichedCollections];
-          return combined.sort(
-            (a, b) =>
-              new Date(b.inserted_at).getTime() -
-              new Date(a.inserted_at).getTime(),
+        // Sort by inserted_at in descending order (latest first)
+        const combined = [...activeOptimistic, ...enrichedCollections];
+        return combined.sort(
+          (a, b) =>
+            new Date(b.inserted_at).getTime() -
+            new Date(a.inserted_at).getTime(),
+        );
+      });
+
+      // If selectedCollection is optimistic and the real one just arrived, fetch full details
+      // Extract the logic outside the updater to avoid side effects
+      let replacementId: string | null = null;
+      setSelectedCollection((prev) => {
+        if (prev?.id.startsWith("optimistic-") && prev.job_id) {
+          const replacement = enrichedCollections.find(
+            (c: Collection) => c.job_id === prev.job_id,
           );
-        });
-
-        // If selectedCollection is optimistic and the real one just arrived, fetch full details
-        // Extract the logic outside the updater to avoid side effects
-        let replacementId: string | null = null;
-        setSelectedCollection((prev) => {
-          if (prev?.id.startsWith("optimistic-") && prev.job_id) {
-            const replacement = enrichedCollections.find(
-              (c: Collection) => c.job_id === prev.job_id,
-            );
-            if (replacement) {
-              replacementId = replacement.id;
-              // Don't set the replacement yet - let fetchCollectionDetails do it with full data
-            }
+          if (replacement) {
+            replacementId = replacement.id;
+            // Don't set the replacement yet - let fetchCollectionDetails do it with full data
           }
-          return prev;
-        });
-
-        // Fetch full details (including documents) for the replacement
-        if (replacementId) {
-          fetchCollectionDetails(replacementId);
         }
-      } else {
-        const error = await response.json().catch(() => ({}));
-        console.error("Failed to fetch collections:", response.status, error);
+        return prev;
+      });
+
+      // Fetch full details (including documents) for the replacement
+      if (replacementId) {
+        fetchCollectionDetails(replacementId);
       }
     } catch (error) {
       console.error("Error fetching collections:", error);
@@ -378,31 +395,27 @@ export default function KnowledgeBasePage() {
 
   // Fetch available documents
   const fetchDocuments = async () => {
-    if (!apiKey) return;
+    if (!isAuthenticated) return;
 
     try {
-      const response = await fetch("/api/document", {
-        headers: { "X-API-KEY": apiKey.key },
-      });
-
-      if (response.ok) {
-        const result = await response.json();
+      const result = await apiFetch<Document[] | DocumentResponse>(
+        "/api/document",
+        apiKey?.key ?? "",
+      );
 
-        // Handle both direct array and wrapped response
-        const documentList = Array.isArray(result) ? result : result.data || [];
+      // Handle both direct array and wrapped response
+      const documentList = Array.isArray(result)
+        ? result
+        : (result as DocumentResponse).data || [];
 
-        // Sort by inserted_at in descending order (latest first)
-        const sortedDocuments = documentList.sort(
-          (a: Document, b: Document) =>
-            new Date(b.inserted_at || 0).getTime() -
-            new Date(a.inserted_at || 0).getTime(),
-        );
+      // Sort by inserted_at in descending order (latest first)
+      const sortedDocuments = documentList.sort(
+        (a: Document, b: Document) =>
+          new Date(b.inserted_at || 0).getTime() -
+          new Date(a.inserted_at || 0).getTime(),
+      );
 
-        setAvailableDocuments(sortedDocuments);
-      } else {
-        const error = await response.json().catch(() => ({}));
-        console.error("Failed to fetch documents:", response.status, error);
-      }
+      setAvailableDocuments(sortedDocuments);
     } catch (error) {
       console.error("Error fetching documents:", error);
     }
@@ -410,7 +423,7 @@ export default function KnowledgeBasePage() {
 
   // Fetch collection details with documents
   const fetchCollectionDetails = async (collectionId: string) => {
-    if (!apiKey) return;
+    if (!isAuthenticated) return;
 
     // Don't fetch optimistic collections from the server
     if (collectionId.startsWith("optimistic-")) {
@@ -425,53 +438,44 @@ export default function KnowledgeBasePage() {
 
     setIsLoading(true);
     try {
-      const response = await fetch(`/api/collections/${collectionId}`, {
-        headers: { "X-API-KEY": apiKey.key },
-      });
-
-      if (response.ok) {
-        const result = await response.json();
+      const result = await apiFetch<CollectionResponse & Collection>(
+        `/api/collections/${collectionId}`,
+        apiKey?.key ?? "",
+      );
 
-        // Handle different response formats
-        const collectionData = result.data || result;
+      // Handle different response formats
+      const collectionData = (result.data as Collection) || result;
 
-        // Get cached data to find the job_id
-        const cached = getCollectionDataByCollectionId(collectionId);
+      // Get cached data to find the job_id
+      const cached = getCollectionDataByCollectionId(collectionId);
 
-        // If we have a job_id, fetch its status
-        let status = undefined;
-        if (cached.job_id) {
-          try {
-            const jobResponse = await fetch(
-              `/api/collections/jobs/${cached.job_id}`,
-              {
-                headers: { "X-API-KEY": apiKey.key },
-              },
-            );
-            if (jobResponse.ok) {
-              const jobResult = await jobResponse.json();
-              const jobData = jobResult.data || jobResult;
-              status = jobData.status || undefined;
-            }
-          } catch (error) {
-            console.error(
-              "Error fetching job status for collection details:",
-              error,
-            );
-          }
+      // If we have a job_id, fetch its status
+      let status = undefined;
+      if (cached.job_id) {
+        try {
+          const jobResult = await apiFetch<
+            { data?: JobStatusData } & JobStatusData
+          >(`/api/collections/jobs/${cached.job_id}`, apiKey?.key ?? "");
+          const jobData = jobResult.data || jobResult;
+          status = jobData.status || undefined;
+        } catch (error) {
+          console.error(
+            "Error fetching job status for collection details:",
+            error,
+          );
         }
+      }
 
-        // Enrich the collection with cached name/description and live status
-        const enrichedCollection = {
-          ...collectionData,
-          name: cached.name || collectionData.name || "Untitled Collection",
-          description: cached.description || collectionData.description || "",
-          status: status,
-          job_id: cached.job_id,
-        };
+      // Enrich the collection with cached name/description and live status
+      const enrichedCollection = {
+        ...collectionData,
+        name: cached.name || collectionData.name || "Untitled Collection",
+        description: cached.description || collectionData.description || "",
+        status: status,
+        job_id: cached.job_id,
+      };
 
-        setSelectedCollection(enrichedCollection);
-      }
+      setSelectedCollection(enrichedCollection);
     } catch (error) {
       console.error("Error fetching collection details:", error);
     } finally {
@@ -499,12 +503,9 @@ export default function KnowledgeBasePage() {
 
       for (const [collectionId, jobId] of Array.from(jobs)) {
         try {
-          const response = await fetch(`/api/collections/jobs/${jobId}`, {
-            headers: { "X-API-KEY": currentApiKey.key },
-          });
-          if (!response.ok) continue;
-
-          const result = await response.json();
+          const result = await apiFetch<
+            { data?: JobStatusData } & JobStatusData
+          >(`/api/collections/jobs/${jobId}`, currentApiKey.key);
           const jobData = result.data || result;
           const status = jobData.status || null;
           const realCollectionId =
@@ -577,8 +578,8 @@ export default function KnowledgeBasePage() {
 
   // Create knowledge base
   const handleCreateClick = async () => {
-    if (!apiKey) {
-      alert("No API key found");
+    if (!isAuthenticated) {
+      alert("Please log in to continue");
       return;
     }
 
@@ -622,60 +623,51 @@ export default function KnowledgeBasePage() {
     setSelectedCollection(optimisticCollection);
 
     try {
-      const response = await fetch("/api/collections", {
-        method: "POST",
-        headers: {
-          "X-API-KEY": apiKey.key,
-          "Content-Type": "application/json",
+      const result = await apiFetch<CreateCollectionResponse>(
+        "/api/collections",
+        apiKey?.key ?? "",
+        {
+          method: "POST",
+          body: JSON.stringify({
+            name: nameAtCreation,
+            description: descriptionAtCreation,
+            documents: docsAtCreation,
+            provider: "openai",
+          }),
         },
-        body: JSON.stringify({
-          name: nameAtCreation,
-          description: descriptionAtCreation,
-          documents: docsAtCreation,
-          provider: "openai",
-        }),
-      });
+      );
 
-      if (response.ok) {
-        const result = await response.json();
-        const jobId = result.data?.job_id;
+      const jobId = result.data?.job_id;
 
-        if (jobId) {
-          saveCollectionData(jobId, nameAtCreation, descriptionAtCreation);
+      if (jobId) {
+        saveCollectionData(jobId, nameAtCreation, descriptionAtCreation);
 
-          // Attach job_id to the optimistic entry so polling picks it up
-          setCollections((prev) =>
-            prev.map((c) =>
-              c.id === optimisticId ? { ...c, job_id: jobId } : c,
-            ),
-          );
-          setSelectedCollection((prev) =>
-            prev?.id === optimisticId ? { ...prev, job_id: jobId } : prev,
-          );
-
-          // Register for polling immediately — don't wait for the next collections render
-          activeJobsRef.current.set(optimisticId, jobId);
-          startPolling();
-        } else {
-          console.error(
-            "No job ID found in response - cannot save name to cache",
-          );
-        }
+        // Attach job_id to the optimistic entry so polling picks it up
+        setCollections((prev) =>
+          prev.map((c) =>
+            c.id === optimisticId ? { ...c, job_id: jobId } : c,
+          ),
+        );
+        setSelectedCollection((prev) =>
+          prev?.id === optimisticId ? { ...prev, job_id: jobId } : prev,
+        );
 
-        // Refresh the real list from the backend (replaces the optimistic entry once the backend knows about it)
-        await fetchCollections();
+        // Register for polling immediately — don't wait for the next collections render
+        activeJobsRef.current.set(optimisticId, jobId);
+        startPolling();
       } else {
-        const error = await response.json().catch(() => ({}));
-        alert(
-          `Failed to create knowledge base: ${error.error || "Unknown error"}`,
+        console.error(
+          "No job ID found in response - cannot save name to cache",
         );
-        // Remove the optimistic entry on failure
-        setCollections((prev) => prev.filter((c) => c.id !== optimisticId));
-        setSelectedCollection(null);
       }
+
+      // Refresh the real list from the backend (replaces the optimistic entry once the backend knows about it)
+      await fetchCollections();
     } catch (error) {
       console.error("Error creating knowledge base:", error);
-      alert("Failed to create knowledge base");
+      alert(
+        `Failed to create knowledge base: ${error instanceof Error ? error.message : "Unknown error"}`,
+      );
       setCollections((prev) => prev.filter((c) => c.id !== optimisticId));
       setSelectedCollection(null);
     } finally {
@@ -685,14 +677,14 @@ export default function KnowledgeBasePage() {
 
   // Delete collection - show confirmation modal
   const handleDeleteCollection = (collectionId: string) => {
-    if (!apiKey) return;
+    if (!isAuthenticated) return;
     setCollectionToDelete(collectionId);
     setShowConfirmDelete(true);
   };
 
   // Confirm and execute delete
   const handleConfirmDelete = async () => {
-    if (!collectionToDelete || !apiKey) return;
+    if (!collectionToDelete || !isAuthenticated) return;
 
     setShowConfirmDelete(false);
     const collectionId = collectionToDelete;
@@ -712,76 +704,38 @@ export default function KnowledgeBasePage() {
     );
 
     try {
-      const response = await fetch(`/api/collections/${collectionId}`, {
-        method: "DELETE",
-        headers: { "X-API-KEY": apiKey.key },
-      });
+      const result = await apiFetch<DeleteCollectionResponse>(
+        `/api/collections/${collectionId}`,
+        apiKey?.key ?? "",
+        { method: "DELETE" },
+      );
 
-      if (response.ok) {
-        const result = await response.json();
-        const jobId = result.data?.job_id;
-
-        if (jobId) {
-          // Poll the delete job status
-          const pollDeleteStatus = async () => {
-            const currentApiKey = apiKeyRef.current;
-            if (!currentApiKey) return;
-
-            try {
-              const jobResponse = await fetch(
-                `/api/collections/jobs/${jobId}`,
-                {
-                  headers: { "X-API-KEY": currentApiKey.key },
-                },
-              );
+      const jobId = result.data?.job_id;
 
-              if (jobResponse.ok) {
-                const jobResult = await jobResponse.json();
-                const jobData = jobResult.data || jobResult;
-                const status = jobData.status;
-                const statusLower = status?.toLowerCase();
-
-                if (statusLower === "successful") {
-                  // Job completed successfully - remove from UI and clean up cache
-                  deleteCollectionFromCache(collectionId);
-                  setCollections((prev) =>
-                    prev.filter((c) => c.id !== collectionId),
-                  );
-                  setSelectedCollection(null);
-                } else if (statusLower === "failed") {
-                  // Job failed - restore original collection
-                  alert("Failed to delete collection");
-                  if (originalCollection) {
-                    setCollections((prev) =>
-                      prev.map((c) =>
-                        c.id === collectionId ? originalCollection : c,
-                      ),
-                    );
-                    setSelectedCollection((prev) =>
-                      prev?.id === collectionId ? originalCollection : prev,
-                    );
-                  }
-                } else {
-                  // Still processing - keep status as "deleting" and poll again
-                  setTimeout(pollDeleteStatus, 2000); // Poll every 2 seconds
-                }
-              } else {
-                // Failed to get job status
-                alert("Failed to check delete status");
-                if (originalCollection) {
-                  setCollections((prev) =>
-                    prev.map((c) =>
-                      c.id === collectionId ? originalCollection : c,
-                    ),
-                  );
-                  setSelectedCollection((prev) =>
-                    prev?.id === collectionId ? originalCollection : prev,
-                  );
-                }
-              }
-            } catch (error) {
-              console.error("Error polling delete status:", error);
-              alert("Failed to check delete status");
+      if (jobId) {
+        // Poll the delete job status
+        const pollDeleteStatus = async () => {
+          const currentApiKey = apiKeyRef.current;
+          if (!currentApiKey) return;
+
+          try {
+            const jobResult = await apiFetch<
+              { data?: JobStatusData } & JobStatusData
+            >(`/api/collections/jobs/${jobId}`, currentApiKey.key);
+            const jobData = jobResult.data || jobResult;
+            const status = jobData.status;
+            const statusLower = status?.toLowerCase();
+
+            if (statusLower === "successful") {
+              // Job completed successfully - remove from UI and clean up cache
+              deleteCollectionFromCache(collectionId);
+              setCollections((prev) =>
+                prev.filter((c) => c.id !== collectionId),
+              );
+              setSelectedCollection(null);
+            } else if (statusLower === "failed") {
+              // Job failed - restore original collection
+              alert("Failed to delete collection");
               if (originalCollection) {
                 setCollections((prev) =>
                   prev.map((c) =>
@@ -792,28 +746,33 @@ export default function KnowledgeBasePage() {
                   prev?.id === collectionId ? originalCollection : prev,
                 );
               }
+            } else {
+              // Still processing - keep status as "deleting" and poll again
+              setTimeout(pollDeleteStatus, 2000); // Poll every 2 seconds
             }
-          };
+          } catch (error) {
+            console.error("Error polling delete status:", error);
+            alert("Failed to check delete status");
+            if (originalCollection) {
+              setCollections((prev) =>
+                prev.map((c) =>
+                  c.id === collectionId ? originalCollection : c,
+                ),
+              );
+              setSelectedCollection((prev) =>
+                prev?.id === collectionId ? originalCollection : prev,
+              );
+            }
+          }
+        };
 
-          // Start polling
-          pollDeleteStatus();
-        } else {
-          // No job_id returned, assume immediate success
-          deleteCollectionFromCache(collectionId);
-          setCollections((prev) => prev.filter((c) => c.id !== collectionId));
-          setSelectedCollection(null);
-        }
+        // Start polling
+        pollDeleteStatus();
       } else {
-        alert("Failed to delete collection");
-        // Restore the original collection on failure
-        if (originalCollection) {
-          setCollections((prev) =>
-            prev.map((c) => (c.id === collectionId ? originalCollection : c)),
-          );
-          setSelectedCollection((prev) =>
-            prev?.id === collectionId ? originalCollection : prev,
-          );
-        }
+        // No job_id returned, assume immediate success
+        deleteCollectionFromCache(collectionId);
+        setCollections((prev) => prev.filter((c) => c.id !== collectionId));
+        setSelectedCollection(null);
       }
     } catch (error) {
       console.error("Error deleting collection:", error);
@@ -846,7 +805,6 @@ export default function KnowledgeBasePage() {
       fetchCollections();
       fetchDocuments();
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [apiKey]);
 
   // Keep apiKeyRef in sync so polling always has the current key
@@ -1385,21 +1343,15 @@ export default function KnowledgeBasePage() {
 
                             if (apiKey) {
                               try {
-                                const response = await fetch(
+                                const data = await apiFetch<
+                                  DocumentDetailResponse & Document
+                                >(
                                   `/api/document/${firstDoc.id}`,
-                                  {
-                                    method: "GET",
-                                    headers: {
-                                      "X-API-KEY": apiKey.key,
-                                    },
-                                  },
+                                  apiKey?.key ?? "",
                                 );
-
-                                if (response.ok) {
-                                  const data = await response.json();
-                                  const documentDetails = data.data || data;
-                                  setPreviewDoc(documentDetails);
-                                }
+                                const documentDetails = (data.data ||
+                                  data) as Document;
+                                setPreviewDoc(documentDetails);
                               } catch (err) {
                                 console.error(
                                   "Failed to fetch document details for preview:",
@@ -1702,18 +1654,11 @@ export default function KnowledgeBasePage() {
 
                   if (apiKey) {
                     try {
-                      const response = await fetch(`/api/document/${doc.id}`, {
-                        method: "GET",
-                        headers: {
-                          "X-API-KEY": apiKey.key,
-                        },
-                      });
-
-                      if (response.ok) {
-                        const data = await response.json();
-                        const documentDetails = data.data || data;
-                        setPreviewDoc(documentDetails);
-                      }
+                      const data = await apiFetch<
+                        DocumentDetailResponse & Document
+                      >(`/api/document/${doc.id}`, apiKey?.key ?? "");
+                      const documentDetails = (data.data || data) as Document;
+                      setPreviewDoc(documentDetails);
                     } catch (err) {
                       console.error(
                         "Failed to fetch document details for preview:",
diff --git a/app/(main)/settings/credentials/page.tsx b/app/(main)/settings/credentials/page.tsx
index 35ce665..0888c78 100644
--- a/app/(main)/settings/credentials/page.tsx
+++ b/app/(main)/settings/credentials/page.tsx
@@ -22,12 +22,11 @@ import { getExistingForProvider } from "@/app/lib/utils";
 import ProviderList from "@/app/components/settings/credentials/ProviderList";
 import CredentialForm from "@/app/components/settings/credentials/CredentialForm";
 import { apiFetch } from "@/app/lib/apiClient";
-import Link from "next/link";
 
 export default function CredentialsPage() {
   const toast = useToast();
   const { sidebarCollapsed, setSidebarCollapsed } = useApp();
-  const { apiKeys } = useAuth();
+  const { apiKeys, isAuthenticated } = useAuth();
   const [selectedProvider, setSelectedProvider] = useState<ProviderDef>(
     PROVIDERS[0],
   );
@@ -43,9 +42,8 @@ export default function CredentialsPage() {
 
   // Load credentials once we have an API key
   useEffect(() => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
     loadCredentials();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [apiKeys]);
 
   // Re-populate form when provider or credentials change
@@ -76,11 +74,11 @@ export default function CredentialsPage() {
     try {
       const data = await apiFetch<{ data?: Credential[] } | Credential[]>(
         "/api/credentials",
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
       setCredentials(Array.isArray(data) ? data : data.data || []);
-    } catch (err) {
-      console.error("Failed to load credentials:", err);
+    } catch {
+      // Silently ignore — credentials may not exist yet or auth may be cookie-only
     } finally {
       setIsLoading(false);
     }
@@ -101,7 +99,7 @@ export default function CredentialsPage() {
   };
 
   const handleSave = async () => {
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -116,13 +114,13 @@ export default function CredentialsPage() {
     setIsSaving(true);
     try {
       if (existingCredential) {
-        await apiFetch("/api/credentials", apiKeys[0].key, {
+        await apiFetch("/api/credentials", apiKeys[0]?.key ?? "", {
           method: "PATCH",
           body: JSON.stringify(buildCredentialBody(true)),
         });
         toast.success(`${selectedProvider.name} credentials updated`);
       } else {
-        await apiFetch("/api/credentials", apiKeys[0].key, {
+        await apiFetch("/api/credentials", apiKeys[0]?.key ?? "", {
           method: "POST",
           body: JSON.stringify(buildCredentialBody(false)),
         });
@@ -159,12 +157,12 @@ export default function CredentialsPage() {
   };
 
   const handleDelete = async () => {
-    if (!existingCredential || apiKeys.length === 0) return;
+    if (!existingCredential || !isAuthenticated) return;
     setIsDeleting(true);
     try {
       await apiFetch(
         `/api/credentials/${selectedProvider.credentialKey}`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
         { method: "DELETE" },
       );
       toast.success(`${selectedProvider.name} credentials removed`);
@@ -260,7 +258,7 @@ export default function CredentialsPage() {
             />
 
             <div className="flex-1 overflow-y-auto p-8">
-              {apiKeys.length === 0 ? (
+              {!isAuthenticated ? (
                 <div
                   className="max-w-lg rounded-lg border p-6 text-sm"
                   style={{
@@ -269,15 +267,7 @@ export default function CredentialsPage() {
                     color: colors.text.secondary,
                   }}
                 >
-                  No API key found. Please add one in{" "}
-                  <Link
-                    href="/keystore"
-                    className="underline"
-                    style={{ color: colors.text.primary }}
-                  >
-                    Keystore
-                  </Link>{" "}
-                  first.
+                  Please log in to manage credentials.
                 </div>
               ) : (
                 <CredentialForm
diff --git a/app/(main)/settings/onboarding/page.tsx b/app/(main)/settings/onboarding/page.tsx
index 40abfca..ea01344 100644
--- a/app/(main)/settings/onboarding/page.tsx
+++ b/app/(main)/settings/onboarding/page.tsx
@@ -61,7 +61,7 @@ function OrganizationListSkeleton() {
 export default function OnboardingPage() {
   const router = useRouter();
   const { sidebarCollapsed } = useApp();
-  const { activeKey, currentUser, isHydrated } = useAuth();
+  const { activeKey, currentUser, isHydrated, isAuthenticated } = useAuth();
   const [view, setView] = useState<View>("loading");
   const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
   const [projects, setProjects] = useState<Project[]>([]);
@@ -100,7 +100,7 @@ export default function OnboardingPage() {
   // Redirect if no API key or not a superuser
   useEffect(() => {
     if (!isHydrated) return;
-    if (!activeKey) {
+    if (!isAuthenticated) {
       router.replace("/");
       return;
     }
diff --git a/app/(main)/speech-to-text/page.tsx b/app/(main)/speech-to-text/page.tsx
index 7b76edf..115d4a1 100644
--- a/app/(main)/speech-to-text/page.tsx
+++ b/app/(main)/speech-to-text/page.tsx
@@ -340,7 +340,7 @@ export default function SpeechToTextPage() {
   const [activeTab, setActiveTab] = useState<Tab>("datasets");
   const { sidebarCollapsed } = useApp();
   const [leftPanelWidth] = useState(450);
-  const { apiKeys } = useAuth();
+  const { apiKeys, isAuthenticated } = useAuth();
   const [languages, setLanguages] = useState<Language[]>([]);
   const [datasetName, setDatasetName] = useState("");
   const [datasetDescription, setDatasetDescription] = useState("");
@@ -376,11 +376,11 @@ export default function SpeechToTextPage() {
 
   // Load languages
   const loadLanguages = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const data = await apiFetch<any>("/api/languages", apiKeys[0].key);
+      const data = await apiFetch<any>("/api/languages", apiKeys[0]?.key ?? "");
 
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       let rawList: any[] = [];
@@ -421,14 +421,14 @@ export default function SpeechToTextPage() {
 
   // Load datasets
   const loadDatasets = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingDatasets(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         "/api/evaluations/stt/datasets",
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       let datasetsList = [];
@@ -452,14 +452,14 @@ export default function SpeechToTextPage() {
 
   // Load evaluation runs
   const loadRuns = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingRuns(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         "/api/evaluations/stt/runs",
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       let runsList = [];
@@ -487,7 +487,6 @@ export default function SpeechToTextPage() {
     if (activeTab === "evaluations") {
       loadRuns();
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [apiKeys, activeTab]);
 
   // Handle audio file selection and upload
@@ -498,7 +497,7 @@ export default function SpeechToTextPage() {
 
     if (!files || files.length === 0) return;
 
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -548,7 +547,7 @@ export default function SpeechToTextPage() {
           file_id?: string;
           id?: string;
           data?: { file_id?: string; id?: string };
-        }>("/api/evaluations/stt/files", apiKeys[0].key, {
+        }>("/api/evaluations/stt/files", apiKeys[0]?.key ?? "", {
           method: "POST",
           body: formData,
         });
@@ -619,7 +618,7 @@ export default function SpeechToTextPage() {
       return;
     }
 
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -648,10 +647,14 @@ export default function SpeechToTextPage() {
         samples: samples,
       };
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      await apiFetch<any>("/api/evaluations/stt/datasets", apiKeys[0].key, {
-        method: "POST",
-        body: JSON.stringify(payload),
-      });
+      await apiFetch<any>(
+        "/api/evaluations/stt/datasets",
+        apiKeys[0]?.key ?? "",
+        {
+          method: "POST",
+          body: JSON.stringify(payload),
+        },
+      );
 
       toast.success(`Dataset "${datasetName}" created successfully!`);
 
@@ -672,7 +675,7 @@ export default function SpeechToTextPage() {
   };
 
   const handleRunEvaluation = async () => {
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -691,7 +694,7 @@ export default function SpeechToTextPage() {
 
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      await apiFetch<any>("/api/evaluations/stt/runs", apiKeys[0].key, {
+      await apiFetch<any>("/api/evaluations/stt/runs", apiKeys[0]?.key ?? "", {
         method: "POST",
         body: JSON.stringify({
           run_name: evaluationName.trim(),
@@ -728,7 +731,7 @@ export default function SpeechToTextPage() {
 
   // Load results for a specific run
   const loadResults = async (runId: number) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingResults(true);
     try {
@@ -736,7 +739,7 @@ export default function SpeechToTextPage() {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const runData = await apiFetch<any>(
         `/api/evaluations/stt/runs/${runId}?include_results=true&include_signed_url=true`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       // Extract results
@@ -822,49 +825,25 @@ export default function SpeechToTextPage() {
           />
 
           {/* Tab Content */}
-          {apiKeys.length === 0 ? (
+          {!isAuthenticated ? (
             <div
               className="flex-1 flex items-center justify-center"
               style={{ backgroundColor: colors.bg.secondary }}
             >
               <div className="text-center">
-                <svg
-                  className="mx-auto h-12 w-12 mb-4"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  style={{ color: colors.border }}
-                >
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    strokeWidth={1.5}
-                    d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
-                  />
-                </svg>
                 <p
                   className="text-sm font-medium mb-1"
                   style={{ color: colors.text.primary }}
                 >
-                  API key required
+                  Authentication required
                 </p>
                 <p
                   className="text-xs mb-4"
                   style={{ color: colors.text.secondary }}
                 >
-                  Add an API key in the Keystore to start creating datasets and
-                  running evaluations
+                  Please sign in to start creating datasets and running
+                  evaluations
                 </p>
-                <a
-                  href="/keystore"
-                  className="inline-block px-4 py-2 rounded-md text-sm font-medium"
-                  style={{
-                    backgroundColor: colors.accent.primary,
-                    color: "#ffffff",
-                  }}
-                >
-                  Go to Keystore
-                </a>
               </div>
             </div>
           ) : activeTab === "datasets" ? (
@@ -1024,6 +1003,7 @@ function DatasetsTab({
   languages,
   toast,
 }: DatasetsTabProps) {
+  const { isAuthenticated } = useAuth();
   const [showLanguageInfo, setShowLanguageInfo] = useState(false);
   const [languageInfoPos, setLanguageInfoPos] = useState({ top: 0, left: 0 });
   const [viewingId, setViewingId] = useState<number | null>(null);
@@ -1044,7 +1024,7 @@ function DatasetsTab({
   const [savingSampleId, setSavingSampleId] = useState<number | null>(null);
 
   const handleViewDataset = async (datasetId: number, datasetName: string) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
     setViewingId(datasetId);
     try {
       const data = await apiFetch<{
@@ -1073,7 +1053,7 @@ function DatasetsTab({
         }[];
       }>(
         `/api/evaluations/stt/datasets/${datasetId}?include_samples=true&include_signed_url=true&sample_limit=100&sample_offset=0`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
       const samples = data?.data?.samples || data?.samples || [];
       if (samples.length === 0) {
@@ -1095,13 +1075,13 @@ function DatasetsTab({
     field: "ground_truth" | "language_id",
     value: string | number,
   ) => {
-    if (!viewModalData || apiKeys.length === 0) return;
+    if (!viewModalData || !isAuthenticated) return;
     setSavingSampleId(sampleId);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       await apiFetch<any>(
         `/api/evaluations/stt/samples/${sampleId}`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
         {
           method: "PATCH",
           body: JSON.stringify({ [field]: value }),
@@ -1320,20 +1300,20 @@ function DatasetsTab({
 
             {audioFiles.length === 0 ? (
               <div
-                onClick={apiKeys.length > 0 ? triggerAudioUpload : undefined}
+                onClick={isAuthenticated ? triggerAudioUpload : undefined}
                 className="border-2 border-dashed rounded-lg p-6 text-center transition-colors"
                 style={{
                   borderColor: colors.border,
                   backgroundColor: colors.bg.primary,
-                  cursor: apiKeys.length > 0 ? "pointer" : "not-allowed",
-                  opacity: apiKeys.length > 0 ? 1 : 0.5,
+                  cursor: isAuthenticated ? "pointer" : "not-allowed",
+                  opacity: isAuthenticated ? 1 : 0.5,
                 }}
                 onMouseEnter={(e) =>
-                  apiKeys.length > 0 &&
+                  isAuthenticated &&
                   (e.currentTarget.style.backgroundColor = colors.bg.secondary)
                 }
                 onMouseLeave={(e) =>
-                  apiKeys.length > 0 &&
+                  isAuthenticated &&
                   (e.currentTarget.style.backgroundColor = colors.bg.primary)
                 }
               >
@@ -1355,7 +1335,7 @@ function DatasetsTab({
                   className="text-xs font-medium mb-1"
                   style={{ color: colors.text.primary }}
                 >
-                  {apiKeys.length > 0
+                  {isAuthenticated
                     ? "Click to upload audio samples"
                     : "Add an API key to upload"}
                 </p>
@@ -1541,14 +1521,13 @@ function DatasetsTab({
 
                 {/* Upload more - below scrollable area */}
                 <button
-                  onClick={apiKeys.length > 0 ? triggerAudioUpload : undefined}
+                  onClick={isAuthenticated ? triggerAudioUpload : undefined}
                   className="flex items-center gap-1 text-xs font-medium mt-2"
                   style={{
-                    color:
-                      apiKeys.length > 0
-                        ? colors.accent.primary
-                        : colors.text.secondary,
-                    cursor: apiKeys.length > 0 ? "pointer" : "not-allowed",
+                    color: isAuthenticated
+                      ? colors.accent.primary
+                      : colors.text.secondary,
+                    cursor: isAuthenticated ? "pointer" : "not-allowed",
                   }}
                 >
                   <svg
@@ -2029,6 +2008,7 @@ function EvaluationsTab({
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   setActiveTab,
 }: EvaluationsTabProps) {
+  const { isAuthenticated } = useAuth();
   const [statusFilter, setStatusFilter] = useState<string>("all");
   const [expandedTranscriptions, setExpandedTranscriptions] = useState<
     Set<number>
@@ -2066,7 +2046,7 @@ function EvaluationsTab({
     isCorrect: boolean | null,
     comment?: string,
   ) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     try {
       const payload: { is_correct?: boolean | null; comment?: string } = {};
@@ -2076,7 +2056,7 @@ function EvaluationsTab({
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       await apiFetch<any>(
         `/api/evaluations/stt/results/${resultId}`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
         {
           method: "PATCH",
           body: JSON.stringify(payload),
diff --git a/app/(main)/text-to-speech/page.tsx b/app/(main)/text-to-speech/page.tsx
index 0127b11..a8af34d 100644
--- a/app/(main)/text-to-speech/page.tsx
+++ b/app/(main)/text-to-speech/page.tsx
@@ -257,7 +257,7 @@ export default function TextToSpeechPage() {
   const leftPanelWidth = 450;
 
   // API Keys
-  const { apiKeys } = useAuth();
+  const { apiKeys, isAuthenticated } = useAuth();
 
   // Languages
   const [languages, setLanguages] = useState<Language[]>([]);
@@ -298,11 +298,11 @@ export default function TextToSpeechPage() {
 
   // Load languages
   const loadLanguages = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const data = await apiFetch<any>("/api/languages", apiKeys[0].key);
+      const data = await apiFetch<any>("/api/languages", apiKeys[0]?.key ?? "");
 
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       let rawList: any[] = [];
@@ -342,14 +342,14 @@ export default function TextToSpeechPage() {
 
   // Load datasets
   const loadDatasets = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingDatasets(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         "/api/evaluations/tts/datasets",
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       let datasetsList = [];
@@ -373,14 +373,14 @@ export default function TextToSpeechPage() {
 
   // Load evaluation runs
   const loadRuns = async () => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingRuns(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         "/api/evaluations/tts/runs",
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       let runsList = [];
@@ -408,7 +408,6 @@ export default function TextToSpeechPage() {
     if (activeTab === "evaluations") {
       loadRuns();
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [apiKeys, activeTab]);
 
   // Add a new text sample
@@ -447,7 +446,7 @@ export default function TextToSpeechPage() {
       return;
     }
 
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -460,15 +459,19 @@ export default function TextToSpeechPage() {
       }));
 
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      await apiFetch<any>("/api/evaluations/tts/datasets", apiKeys[0].key, {
-        method: "POST",
-        body: JSON.stringify({
-          name: datasetName.trim(),
-          description: datasetDescription.trim() || undefined,
-          language_id: datasetLanguageId,
-          samples,
-        }),
-      });
+      await apiFetch<any>(
+        "/api/evaluations/tts/datasets",
+        apiKeys[0]?.key ?? "",
+        {
+          method: "POST",
+          body: JSON.stringify({
+            name: datasetName.trim(),
+            description: datasetDescription.trim() || undefined,
+            language_id: datasetLanguageId,
+            samples,
+          }),
+        },
+      );
 
       toast.success(`Dataset "${datasetName}" created successfully!`);
 
@@ -489,7 +492,7 @@ export default function TextToSpeechPage() {
 
   // Run evaluation
   const handleRunEvaluation = async () => {
-    if (apiKeys.length === 0) {
+    if (!isAuthenticated) {
       toast.error("Please add an API key in Keystore first");
       return;
     }
@@ -508,7 +511,7 @@ export default function TextToSpeechPage() {
 
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      await apiFetch<any>("/api/evaluations/tts/runs", apiKeys[0].key, {
+      await apiFetch<any>("/api/evaluations/tts/runs", apiKeys[0]?.key ?? "", {
         method: "POST",
         body: JSON.stringify({
           run_name: evaluationName.trim(),
@@ -536,14 +539,14 @@ export default function TextToSpeechPage() {
 
   // Load results for a specific run
   const loadResults = async (runId: number) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     setIsLoadingResults(true);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const runData = await apiFetch<any>(
         `/api/evaluations/tts/runs/${runId}?include_results=true&include_signed_url=true`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
 
       let resultsList = [];
@@ -609,50 +612,15 @@ export default function TextToSpeechPage() {
           />
 
           {/* Tab Content */}
-          {apiKeys.length === 0 ? (
+          {!isAuthenticated ? (
             <div
               className="flex-1 flex items-center justify-center"
               style={{ backgroundColor: colors.bg.secondary }}
             >
-              <div className="text-center">
-                <svg
-                  className="mx-auto h-12 w-12 mb-4"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  style={{ color: colors.border }}
-                >
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    strokeWidth={1.5}
-                    d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
-                  />
-                </svg>
-                <p
-                  className="text-sm font-medium mb-1"
-                  style={{ color: colors.text.primary }}
-                >
-                  API key required
-                </p>
-                <p
-                  className="text-xs mb-4"
-                  style={{ color: colors.text.secondary }}
-                >
-                  Add an API key in the Keystore to start creating datasets and
-                  running evaluations
-                </p>
-                <a
-                  href="/keystore"
-                  className="inline-block px-4 py-2 rounded-md text-sm font-medium"
-                  style={{
-                    backgroundColor: colors.accent.primary,
-                    color: "#ffffff",
-                  }}
-                >
-                  Go to Keystore
-                </a>
-              </div>
+              <p className="text-sm" style={{ color: colors.text.secondary }}>
+                Please sign in to start creating datasets and running
+                evaluations.
+              </p>
             </div>
           ) : activeTab === "datasets" ? (
             <DatasetsTab
@@ -796,6 +764,7 @@ function DatasetsTab({
   datasets,
   toast,
 }: DatasetsTabProps) {
+  const { isAuthenticated } = useAuth();
   const [viewingId, setViewingId] = useState<number | null>(null);
   const [viewModalData, setViewModalData] = useState<{
     name: string;
@@ -818,13 +787,13 @@ function DatasetsTab({
   }, [textSamples.length]);
 
   const handleViewDataset = async (datasetId: number, datasetName: string) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
     setViewingId(datasetId);
     try {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const data = await apiFetch<any>(
         `/api/evaluations/tts/datasets/${datasetId}?include_signed_url=true&fetch_content=true`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
       );
       const csvText = data?.csv_content;
       if (!csvText) {
@@ -1085,14 +1054,13 @@ function DatasetsTab({
             )}
 
             <button
-              onClick={apiKeys.length > 0 ? addTextSample : undefined}
+              onClick={isAuthenticated ? addTextSample : undefined}
               className="flex items-center gap-1 text-xs font-medium mt-2"
               style={{
-                color:
-                  apiKeys.length > 0
-                    ? colors.accent.primary
-                    : colors.text.secondary,
-                cursor: apiKeys.length > 0 ? "pointer" : "not-allowed",
+                color: isAuthenticated
+                  ? colors.accent.primary
+                  : colors.text.secondary,
+                cursor: isAuthenticated ? "pointer" : "not-allowed",
               }}
             >
               <svg
@@ -1482,6 +1450,7 @@ function EvaluationsTab({
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   setActiveTab,
 }: EvaluationsTabProps) {
+  const { isAuthenticated } = useAuth();
   const [statusFilter, setStatusFilter] = useState<string>("all");
   const [playingResultId, setPlayingResultId] = useState<number | null>(null);
   const [openScoreInfo, setOpenScoreInfo] = useState<string | null>(null);
@@ -1511,7 +1480,7 @@ function EvaluationsTab({
       [key: string]: any;
     },
   ) => {
-    if (apiKeys.length === 0) return;
+    if (!isAuthenticated) return;
 
     try {
       const payload: {
@@ -1529,7 +1498,7 @@ function EvaluationsTab({
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       await apiFetch<any>(
         `/api/evaluations/tts/results/${resultId}`,
-        apiKeys[0].key,
+        apiKeys[0]?.key ?? "",
         {
           method: "PATCH",
           body: JSON.stringify(payload),
diff --git a/app/Providers.tsx b/app/Providers.tsx
new file mode 100644
index 0000000..da3507a
--- /dev/null
+++ b/app/Providers.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { GoogleOAuthProvider } from "@react-oauth/google";
+import { ToastProvider } from "@/app/components/Toast";
+import { AuthProvider } from "@/app/lib/context/AuthContext";
+import { AppProvider } from "@/app/lib/context/AppContext";
+
+const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "";
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+  return (
+    <GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
+      <ToastProvider>
+        <AuthProvider>
+          <AppProvider>{children}</AppProvider>
+        </AuthProvider>
+      </ToastProvider>
+    </GoogleOAuthProvider>
+  );
+}
diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts
new file mode 100644
index 0000000..c65bfd6
--- /dev/null
+++ b/app/api/auth/google/route.ts
@@ -0,0 +1,50 @@
+import { NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
+
+/**
+ * POST /api/auth/google
+ * Receives the Google credential token from the frontend (@react-oauth/google)
+ * and sends it to the backend for verification and user creation/login.
+ *
+ * Request body: { token: string }
+ * Backend sets a Set-Cookie with the access_token (HttpOnly).
+ * We forward that cookie to the browser.
+ */
+export async function POST(request: Request) {
+  try {
+    const body = await request.json();
+
+    if (!body.token) {
+      return NextResponse.json(
+        { success: false, error: "Missing Google credential token" },
+        { status: 400 },
+      );
+    }
+
+    const {
+      status,
+      data,
+      headers: backendHeaders,
+    } = await apiClient(request, "/api/v1/auth/google", {
+      method: "POST",
+      body: JSON.stringify(body),
+    });
+
+    const response = NextResponse.json(data, { status });
+
+    // Forward Set-Cookie from backend so the browser stores the auth cookie
+    const setCookie = backendHeaders.getSetCookie?.();
+    if (setCookie) {
+      setCookie.forEach((cookie: string) => {
+        response.headers.append("Set-Cookie", cookie);
+      });
+    }
+
+    return response;
+  } catch {
+    return NextResponse.json(
+      { success: false, error: "Failed to connect to backend" },
+      { status: 500 },
+    );
+  }
+}
diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts
new file mode 100644
index 0000000..7750261
--- /dev/null
+++ b/app/api/auth/refresh/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest, NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
+
+export async function POST(request: NextRequest) {
+  const { status, data, headers } = await apiClient(
+    request,
+    "/api/v1/auth/refresh",
+    { method: "POST" },
+  );
+
+  const res = NextResponse.json(data, { status });
+
+  const setCookies = headers.getSetCookie?.() ?? [];
+  for (const cookie of setCookies) {
+    res.headers.append("Set-Cookie", cookie);
+  }
+
+  return res;
+}
diff --git a/app/components/ConfigModal.tsx b/app/components/ConfigModal.tsx
index b081300..817b24f 100644
--- a/app/components/ConfigModal.tsx
+++ b/app/components/ConfigModal.tsx
@@ -9,7 +9,12 @@ import React, { useState, useEffect } from "react";
 import { colors } from "@/app/lib/colors";
 import { EvalJob, AssistantConfig } from "./types";
 import { useAuth } from "@/app/lib/context/AuthContext";
-import { Tool } from "@/app/lib/types/configs";
+import { apiFetch } from "@/app/lib/apiClient";
+import {
+  Tool,
+  ConfigVersionPublic,
+  CompletionParams,
+} from "@/app/lib/types/configs";
 
 interface ConfigModalProps {
   isOpen: boolean;
@@ -24,7 +29,7 @@ interface ConfigVersionInfo {
   model?: string;
   instructions?: string;
   temperature?: number;
-  tools?: { type: string; [key: string]: unknown }[];
+  tools?: Tool[];
   provider?: string;
   type?: "text" | "stt" | "tts";
   knowledge_base_ids?: string[];
@@ -36,7 +41,7 @@ export default function ConfigModal({
   job,
   assistantConfig,
 }: ConfigModalProps) {
-  const { activeKey } = useAuth();
+  const { activeKey, isAuthenticated } = useAuth();
   const [configVersionInfo, setConfigVersionInfo] =
     useState<ConfigVersionInfo | null>(null);
   const [isLoadingConfig, setIsLoadingConfig] = useState(false);
@@ -49,45 +54,31 @@ export default function ConfigModal({
     }
 
     const fetchConfigVersionInfo = async () => {
+      if (!isAuthenticated) return;
+      const apiKey = activeKey?.key ?? "";
+
       setIsLoadingConfig(true);
       try {
-        const apiKey = activeKey?.key;
-        if (!apiKey) {
-          console.error("No API key found");
-          return;
-        }
-
         // Fetch config name first
-        const configResponse = await fetch(`/api/configs/${job.config_id}`, {
-          headers: { "X-API-KEY": apiKey },
-        });
-
-        if (!configResponse.ok) {
-          console.error("Failed to fetch config info");
-          return;
-        }
-
-        const configData = await configResponse.json();
+        const configData = await apiFetch<{
+          success: boolean;
+          data?: { name: string };
+        }>(`/api/configs/${job.config_id}`, apiKey);
         const configName =
           configData.success && configData.data ? configData.data.name : null;
 
         // Fetch full version details including config_blob
-        const versionResponse = await fetch(
+        const versionData = await apiFetch<{
+          success: boolean;
+          data?: ConfigVersionPublic;
+        }>(
           `/api/configs/${job.config_id}/versions/${job.config_version}`,
-          {
-            headers: { "X-API-KEY": apiKey },
-          },
+          apiKey,
         );
-
-        if (!versionResponse.ok) {
-          console.error("Failed to fetch version details");
-          return;
-        }
-
-        const versionData = await versionResponse.json();
         if (versionData.success && versionData.data) {
           const blob = versionData.data.config_blob;
-          const params = blob?.completion?.params || {};
+          const params: CompletionParams =
+            blob?.completion?.params || ({} as CompletionParams);
 
           // Extract knowledge base IDs from multiple sources
           const knowledgeBaseIds: string[] = [];
diff --git a/app/components/ConfigSelector.tsx b/app/components/ConfigSelector.tsx
index 0b39733..61d1621 100644
--- a/app/components/ConfigSelector.tsx
+++ b/app/components/ConfigSelector.tsx
@@ -69,7 +69,6 @@ export default function ConfigSelector({
     items.forEach((item) => {
       loadSingleVersion(item.config_id, item.version);
     });
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [expandedConfigId, versionItemsMap]);
 
   // Reset expanded state and recheck overflow whenever selected config changes.
@@ -112,7 +111,6 @@ export default function ConfigSelector({
         if (!result) setIsLoadingPreview(false);
       })
       .catch(() => setIsLoadingPreview(false));
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [selectedConfigId, selectedVersion, configs]);
 
   // Dropdown display list: all configs from the lightweight allConfigMeta,
diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx
index 38091d4..bb767e1 100644
--- a/app/components/Modal.tsx
+++ b/app/components/Modal.tsx
@@ -47,7 +47,7 @@ export default function Modal({
         onClick={(e) => e.stopPropagation()}
       >
         {(title || showClose) && (
-          <div className="flex items-center justify-between px-6 py-4 border-b border-border shrink-0">
+          <div className="flex items-center justify-between px-6 py-4 shrink-0">
             {title ? (
               <h2 className="text-lg font-semibold text-text-primary">
                 {title}
diff --git a/app/components/PageHeader.tsx b/app/components/PageHeader.tsx
index 2482308..1f997e4 100644
--- a/app/components/PageHeader.tsx
+++ b/app/components/PageHeader.tsx
@@ -1,15 +1,23 @@
 "use client";
 
-import { ReactNode } from "react";
+import { ReactNode, useState } from "react";
 import { colors } from "@/app/lib/colors";
 import { useApp } from "@/app/lib/context/AppContext";
+import { useAuth } from "@/app/lib/context/AuthContext";
 import { MenuIcon } from "@/app/components/icons";
+import { LoginModal } from "@/app/components/auth";
+import { Button } from "@/app/components";
 
 interface PageHeaderProps {
+  /** Page title displayed next to the sidebar toggle. Ignored when `children` is provided. */
   title?: string;
+  /** Subtitle shown below the title. Ignored when `children` is provided. */
   subtitle?: string;
+  /** Custom left-side content that replaces the default title/subtitle. */
   children?: ReactNode;
+  /** Right-side action buttons / controls. */
   actions?: ReactNode;
+  /** When true the header is not rendered at all. */
   hidden?: boolean;
 }
 
@@ -21,48 +29,64 @@ export default function PageHeader({
   hidden = false,
 }: PageHeaderProps) {
   const { sidebarCollapsed, setSidebarCollapsed } = useApp();
+  const { isAuthenticated } = useAuth();
+  const [showLoginModal, setShowLoginModal] = useState(false);
 
   if (hidden) return null;
 
   return (
-    <div
-      className="border-b px-4 py-3 flex items-center justify-between shrink-0"
-      style={{
-        backgroundColor: colors.bg.primary,
-        borderColor: colors.border,
-      }}
-    >
-      <div className="flex items-center gap-3">
-        <button
-          onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
-          className="p-1.5 rounded-md"
-          style={{ color: colors.text.secondary }}
-          aria-label="Toggle sidebar"
-        >
-          <MenuIcon className="w-5 h-5" />
-        </button>
-        {children ?? (
-          <div>
-            {title && (
-              <h1
-                className="text-base font-semibold"
-                style={{
-                  color: colors.text.primary,
-                  letterSpacing: "-0.01em",
-                }}
-              >
-                {title}
-              </h1>
-            )}
-            {subtitle && (
-              <p className="text-xs" style={{ color: colors.text.secondary }}>
-                {subtitle}
-              </p>
-            )}
-          </div>
-        )}
+    <>
+      <div
+        className="border-b px-4 py-3 flex items-center justify-between shrink-0"
+        style={{
+          backgroundColor: colors.bg.primary,
+          borderColor: colors.border,
+        }}
+      >
+        <div className="flex items-center gap-3">
+          <button
+            onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
+            className="p-1.5 rounded-md"
+            style={{ color: colors.text.secondary }}
+            aria-label="Toggle sidebar"
+          >
+            <MenuIcon className="w-5 h-5" />
+          </button>
+          {children ?? (
+            <div>
+              {title && (
+                <h1
+                  className="text-base font-semibold"
+                  style={{
+                    color: colors.text.primary,
+                    letterSpacing: "-0.01em",
+                  }}
+                >
+                  {title}
+                </h1>
+              )}
+              {subtitle && (
+                <p className="text-xs" style={{ color: colors.text.secondary }}>
+                  {subtitle}
+                </p>
+              )}
+            </div>
+          )}
+        </div>
+        <div className="flex items-center gap-2">
+          {actions}
+          {!isAuthenticated && (
+            <Button size="sm" onClick={() => setShowLoginModal(true)}>
+              Log in
+            </Button>
+          )}
+        </div>
       </div>
-      {actions && <div className="flex items-center gap-2">{actions}</div>}
-    </div>
+
+      <LoginModal
+        open={showLoginModal}
+        onClose={() => setShowLoginModal(false)}
+      />
+    </>
   );
 }
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
index 359c320..dcec97c 100644
--- a/app/components/Sidebar.tsx
+++ b/app/components/Sidebar.tsx
@@ -5,8 +5,10 @@
 
 "use client";
 
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef, useCallback } from "react";
+import { createPortal } from "react-dom";
 import { useRouter } from "next/navigation";
+import Image from "next/image";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import {
   ClipboardIcon,
@@ -17,6 +19,8 @@ import {
   KeyIcon,
   ChevronRightIcon,
 } from "@/app/components/icons";
+import { LoginModal } from "@/app/components/auth";
+import { Button } from "@/app/components";
 
 interface SubMenuItem {
   name: string;
@@ -30,6 +34,7 @@ interface MenuItem {
   route?: string;
   icon: React.ReactNode;
   submenu?: SubMenuItem[];
+  gateDescription?: string;
 }
 
 interface SidebarProps {
@@ -37,16 +42,85 @@ interface SidebarProps {
   activeRoute?: string;
 }
 
+/** Routes that are always accessible without auth */
+const PUBLIC_ROUTES = new Set(["/evaluations", "/keystore"]);
+
+// ---- Gate Popover (rendered via portal) ----
+
+function GatePopover({
+  name,
+  description,
+  anchorRect,
+  onMouseEnter,
+  onMouseLeave,
+  onLogin,
+}: {
+  name: string;
+  description: string;
+  anchorRect: DOMRect;
+  onMouseEnter: () => void;
+  onMouseLeave: () => void;
+  onLogin: () => void;
+}) {
+  return createPortal(
+    <div
+      className="fixed z-9999 w-72"
+      style={{
+        top: anchorRect.top - 40,
+        left: anchorRect.right + 10,
+      }}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+    >
+      <div className="bg-white rounded-2xl shadow-2xl border border-border overflow-hidden">
+        {/* Gradient banner */}
+        <div
+          className="h-24"
+          style={{
+            background:
+              "linear-gradient(135deg, #dbeafe 0%, #e0e7ff 30%, #c7d2fe 50%, #ddd6fe 70%, #fce7f3 100%)",
+          }}
+        />
+
+        {/* Content */}
+        <div className="px-4 pt-3 pb-4">
+          <p className="text-[15px] font-semibold text-text-primary leading-snug">
+            {name}
+          </p>
+          <p className="text-[13px] text-text-secondary mt-1.5 leading-relaxed">
+            {description}
+          </p>
+
+          <div className="mt-4">
+            <Button size="sm" onClick={onLogin}>
+              Log in
+            </Button>
+          </div>
+        </div>
+      </div>
+    </div>,
+    document.body,
+  );
+}
+
+// ---- Main Sidebar ----
+
 export default function Sidebar({
   collapsed,
   activeRoute = "/evaluations",
 }: SidebarProps) {
   const router = useRouter();
-  const { currentUser } = useAuth();
+  const { currentUser, googleProfile, isAuthenticated, session, logout } =
+    useAuth();
+  const isGoogleUser = !!session?.accessToken;
   const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({
     Evaluations: true,
     Configurations: false,
   });
+  const [showLoginModal, setShowLoginModal] = useState(false);
+  const [hoveredGate, setHoveredGate] = useState<string | null>(null);
+  const [gateRect, setGateRect] = useState<DOMRect | null>(null);
+  const gateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
   useEffect(() => {
     const saved = localStorage.getItem("sidebar-expanded-menus");
@@ -65,6 +139,38 @@ export default function Sidebar({
     localStorage.setItem("sidebar-expanded-menus", JSON.stringify(newState));
   };
 
+  const handleGateEnter = useCallback((name: string, el: HTMLElement) => {
+    if (gateTimeoutRef.current) clearTimeout(gateTimeoutRef.current);
+    setHoveredGate(name);
+    setGateRect(el.getBoundingClientRect());
+  }, []);
+
+  const handleGateLeave = useCallback(() => {
+    gateTimeoutRef.current = setTimeout(() => {
+      setHoveredGate(null);
+      setGateRect(null);
+    }, 200);
+  }, []);
+
+  const handleGatePopoverEnter = useCallback(() => {
+    if (gateTimeoutRef.current) clearTimeout(gateTimeoutRef.current);
+  }, []);
+
+  const isRouteGated = (route?: string): boolean => {
+    if (isAuthenticated) return false;
+    if (!route) return false;
+    return !PUBLIC_ROUTES.has(route);
+  };
+
+  const isItemGated = (item: MenuItem): boolean => {
+    if (isAuthenticated) return false;
+    if (item.route) return isRouteGated(item.route);
+    if (item.submenu) {
+      return item.submenu.every((sub) => isRouteGated(sub.route));
+    }
+    return false;
+  };
+
   const navItems: MenuItem[] = [
     {
       name: "Evaluations",
@@ -74,16 +180,20 @@ export default function Sidebar({
         { name: "Speech-to-Text", route: "/speech-to-text" },
         { name: "Text-to-Speech", route: "/text-to-speech" },
       ],
+      gateDescription:
+        "Log in to compare model response quality across different configs.",
     },
     {
       name: "Documents",
       route: "/document",
       icon: <DocumentFileIcon />,
+      gateDescription: "Log in to upload and manage your documents.",
     },
     {
       name: "Knowledge Base",
       route: "/knowledge-base",
       icon: <BookOpenIcon />,
+      gateDescription: "Log in to manage your knowledge bases for RAG.",
     },
     {
       name: "Configurations",
@@ -92,6 +202,7 @@ export default function Sidebar({
         { name: "Library", route: "/configurations" },
         { name: "Prompt Editor", route: "/configurations/prompt-editor" },
       ],
+      gateDescription: "Log in to manage prompts and model configurations.",
     },
     ...(currentUser?.is_superuser
       ? [
@@ -107,15 +218,26 @@ export default function Sidebar({
       : []),
   ];
 
-  const bottomItem: MenuItem = {
-    name: "Keystore",
-    route: "/keystore",
-    icon: <KeyIcon className="w-5 h-5" />,
+  // Find the gate description for the currently hovered item
+  const getGateDescription = (name: string): string => {
+    for (const item of navItems) {
+      if (item.name === name)
+        return (
+          item.gateDescription || `Log in to access ${name.toLowerCase()}.`
+        );
+      if (item.submenu) {
+        for (const sub of item.submenu) {
+          if (sub.name === name)
+            return `Log in to access ${name.toLowerCase()}.`;
+        }
+      }
+    }
+    return "Log in to access this feature.";
   };
 
   return (
     <aside
-      className={`border-r border-border transition-all duration-300 ease-in-out h-full shrink-0 flex flex-col bg-bg-secondary overflow-hidden ${collapsed ? "w-0" : "w-60"}`}
+      className={`border-r border-border transition-all duration-300 ease-in-out h-full shrink-0 flex flex-col bg-bg-secondary ${collapsed ? "w-0 overflow-hidden" : "w-60"}`}
     >
       <div className="px-5 py-[13px] border-b border-border">
         <h2 className="text-sm font-semibold text-text-primary tracking-tight">
@@ -129,6 +251,7 @@ export default function Sidebar({
           const hasSubmenu = item.submenu && item.submenu.length > 0;
           const isExpanded = expandedMenus[item.name];
           const isActive = activeRoute === item.route;
+          const gated = isItemGated(item);
 
           const hasActiveChild =
             hasSubmenu &&
@@ -142,7 +265,12 @@ export default function Sidebar({
           return (
             <div key={item.name}>
               <button
+                onMouseEnter={(e) =>
+                  gated && handleGateEnter(item.name, e.currentTarget)
+                }
+                onMouseLeave={() => gated && handleGateLeave()}
                 onClick={() => {
+                  if (gated) return;
                   if (hasSubmenu) {
                     toggleMenu(item.name);
                   } else if (item.route) {
@@ -152,7 +280,7 @@ export default function Sidebar({
                 className={`w-full text-left px-3 py-2 rounded-lg text-[14px] flex items-center gap-2.5 transition-all duration-150 border ${
                   isActive
                     ? "bg-neutral-100 text-text-primary font-semibold border-border"
-                    : isActive || hasActiveChild
+                    : hasActiveChild
                       ? "bg-transparent text-text-primary font-semibold border-transparent"
                       : "bg-transparent text-black font-medium border-transparent hover:bg-neutral-100"
                 }`}
@@ -174,13 +302,14 @@ export default function Sidebar({
                 )}
               </button>
 
-              {hasSubmenu && isExpanded && (
+              {hasSubmenu && isExpanded && !gated && (
                 <div className="ml-3 mt-1 space-y-0.5 overflow-hidden border-l-2 border-border pl-3 animate-[slideDown_0.15s_ease-out]">
                   {item.submenu?.map((subItem) => {
                     const hasNestedSubmenu =
                       subItem.submenu && subItem.submenu.length > 0;
                     const isNestedExpanded = expandedMenus[subItem.name];
                     const isSubActive = activeRoute === subItem.route;
+                    const subGated = isRouteGated(subItem.route);
                     const hasActiveNestedChild =
                       hasNestedSubmenu &&
                       subItem.submenu?.some(
@@ -190,7 +319,13 @@ export default function Sidebar({
                     return (
                       <div key={subItem.name}>
                         <button
+                          onMouseEnter={(e) =>
+                            subGated &&
+                            handleGateEnter(subItem.name, e.currentTarget)
+                          }
+                          onMouseLeave={() => subGated && handleGateLeave()}
                           onClick={() => {
+                            if (subGated) return;
                             if (hasNestedSubmenu) {
                               toggleMenu(subItem.name);
                             } else if (subItem.route) {
@@ -200,7 +335,7 @@ export default function Sidebar({
                           className={`w-full text-left px-3 py-1.5 rounded-md text-[13px] flex items-center justify-between gap-2 transition-all duration-150 border ${
                             isSubActive
                               ? "bg-neutral-100 text-text-primary font-medium border-border"
-                              : isSubActive || hasActiveNestedChild
+                              : hasActiveNestedChild
                                 ? "bg-transparent text-text-primary font-medium border-transparent"
                                 : "bg-transparent text-black font-normal border-transparent hover:bg-neutral-100"
                           }`}
@@ -259,25 +394,123 @@ export default function Sidebar({
         })}
       </nav>
 
-      <div className="px-3 py-[11px] border-t border-border w-60">
-        <button
-          onClick={() => router.push(bottomItem.route!)}
-          className={`w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2.5 transition-all duration-150 border ${
-            activeRoute === bottomItem.route
-              ? "bg-neutral-100 text-text-primary font-semibold border-border"
-              : "bg-transparent text-text-secondary font-medium border-transparent hover:bg-neutral-100 hover:text-text-primary"
-          }`}
-        >
-          <span
-            className={
-              activeRoute === bottomItem.route ? "opacity-100" : "opacity-70"
-            }
-          >
-            {bottomItem.icon}
-          </span>
-          {bottomItem.name}
-        </button>
+      {/* Bottom section */}
+      <div className="w-60">
+        {!isGoogleUser && (
+          <div className="px-3 py-3">
+            <button
+              onClick={() => router.push("/keystore")}
+              className={`w-full text-left px-3 py-2 rounded-lg text-[14px] flex items-center gap-2.5 transition-all duration-150 border ${
+                activeRoute === "/keystore"
+                  ? "bg-neutral-100 text-text-primary font-semibold border-border"
+                  : "bg-transparent text-black font-medium border-transparent hover:bg-neutral-100"
+              }`}
+            >
+              <span
+                className={
+                  activeRoute === "/keystore" ? "opacity-100" : "opacity-70"
+                }
+              >
+                <KeyIcon className="w-5 h-5" />
+              </span>
+              Keystore
+            </button>
+          </div>
+        )}
+        {/* Keystore */}
+
+        {/* User profile (authenticated) or login prompt */}
+        {isAuthenticated && (currentUser || googleProfile) ? (
+          <div className="px-3 pb-3 pt-1 border-t border-border">
+            <div className="flex items-center gap-3 px-2 py-2">
+              {googleProfile?.picture ? (
+                <Image
+                  src={googleProfile.picture}
+                  alt={googleProfile.name}
+                  width={32}
+                  height={32}
+                  className="rounded-full shrink-0"
+                  referrerPolicy="no-referrer"
+                />
+              ) : (
+                <div className="w-8 h-8 rounded-full bg-green-600 flex items-center justify-center shrink-0">
+                  <span className="text-xs font-semibold text-white">
+                    {(currentUser?.full_name || currentUser?.email || "U")
+                      .charAt(0)
+                      .toUpperCase()}
+                  </span>
+                </div>
+              )}
+              <div className="flex-1 min-w-0">
+                <p className="text-sm font-medium text-text-primary truncate">
+                  {googleProfile?.name ||
+                    currentUser?.full_name ||
+                    currentUser?.email}
+                </p>
+                <p className="text-[11px] text-text-secondary truncate">
+                  {currentUser?.email}
+                </p>
+              </div>
+              <button
+                onClick={logout}
+                className="p-1.5 rounded-md text-text-secondary hover:bg-neutral-100 hover:text-text-primary transition-colors shrink-0"
+                title="Log out"
+              >
+                <svg
+                  className="w-4 h-4"
+                  fill="none"
+                  viewBox="0 0 24 24"
+                  stroke="currentColor"
+                  strokeWidth={2}
+                >
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
+                  />
+                </svg>
+              </button>
+            </div>
+          </div>
+        ) : !isAuthenticated ? (
+          <div className="px-4 py-4 w-60 border-t border-border">
+            <div className="rounded-lg bg-neutral-50 py-3">
+              <p className="text-sm font-semibold text-text-primary">
+                Get full access
+              </p>
+              <p className="text-xs text-text-secondary mt-1 leading-relaxed">
+                Log in to run evaluations, manage configs, and upload documents.
+              </p>
+              <button
+                onClick={() => setShowLoginModal(true)}
+                className="mt-3 w-full py-2 rounded-full border border-border bg-white text-sm font-medium text-text-primary hover:bg-neutral-50 transition-colors"
+              >
+                Log in
+              </button>
+            </div>
+          </div>
+        ) : null}
       </div>
+
+      {/* Gate popover — rendered via portal so it's never clipped */}
+      {hoveredGate && gateRect && (
+        <GatePopover
+          name={hoveredGate}
+          description={getGateDescription(hoveredGate)}
+          anchorRect={gateRect}
+          onMouseEnter={handleGatePopoverEnter}
+          onMouseLeave={handleGateLeave}
+          onLogin={() => {
+            setHoveredGate(null);
+            setShowLoginModal(true);
+          }}
+        />
+      )}
+
+      <LoginModal
+        open={showLoginModal}
+        onClose={() => setShowLoginModal(false)}
+      />
     </aside>
   );
 }
diff --git a/app/components/SimplifiedConfigEditor.tsx b/app/components/SimplifiedConfigEditor.tsx
index aace472..a65fbf6 100644
--- a/app/components/SimplifiedConfigEditor.tsx
+++ b/app/components/SimplifiedConfigEditor.tsx
@@ -14,6 +14,8 @@
 import { useState, useEffect } from "react";
 import ConfigDrawer from "./ConfigDrawer";
 import { useToast } from "./Toast";
+import { apiFetch } from "@/app/lib/apiClient";
+import { useAuth } from "@/app/lib/context/AuthContext";
 import {
   ConfigPublic,
   ConfigVersionPublic,
@@ -24,6 +26,7 @@ import {
   ConfigListResponse,
   ConfigWithVersionResponse,
   ConfigVersionListResponse,
+  ConfigVersionResponse,
 } from "@/app/lib/types/configs";
 import { colors } from "../lib/colors";
 
@@ -80,20 +83,8 @@ export default function SimplifiedConfigEditor({
   const [isLoading, setIsLoading] = useState<boolean>(false);
   const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
   const [commitMessage, setCommitMessage] = useState<string>("");
-
-  // Get API key from localStorage
-  const getApiKey = (): string | null => {
-    try {
-      const stored = localStorage.getItem("kaapi_api_keys");
-      if (stored) {
-        const keys = JSON.parse(stored);
-        return keys.length > 0 ? keys[0].key : null;
-      }
-    } catch (e) {
-      console.error("Failed to get API key:", e);
-    }
-    return null;
-  };
+  const { activeKey, isAuthenticated } = useAuth();
+  const apiKeyStr = activeKey?.key ?? "";
 
   // Flatten config versions for UI
   const flattenConfigVersion = (
@@ -143,21 +134,15 @@ export default function SimplifiedConfigEditor({
   useEffect(() => {
     const fetchConfigs = async () => {
       setIsLoading(true);
-      const apiKey = getApiKey();
-      if (!apiKey) {
-        console.warn(
-          "No API key found. Please add an API key in the Keystore.",
-        );
+      if (!isAuthenticated) {
         setIsLoading(false);
         return;
       }
+      const apiKey = apiKeyStr;
 
       try {
         // Fetch all configs
-        const response = await fetch("/api/configs", {
-          headers: { "X-API-KEY": apiKey },
-        });
-        const data: ConfigListResponse = await response.json();
+        const data = await apiFetch<ConfigListResponse>("/api/configs", apiKey);
 
         if (!data.success || !data.data) {
           console.error("Failed to fetch configs:", data.error);
@@ -169,24 +154,19 @@ export default function SimplifiedConfigEditor({
         const allVersions: SavedConfig[] = [];
         for (const config of data.data) {
           try {
-            const versionsResponse = await fetch(
+            const versionsData = await apiFetch<ConfigVersionListResponse>(
               `/api/configs/${config.id}/versions`,
-              {
-                headers: { "X-API-KEY": apiKey },
-              },
+              apiKey,
             );
-            const versionsData: ConfigVersionListResponse =
-              await versionsResponse.json();
 
             if (versionsData.success && versionsData.data) {
               // Fetch full version details for each version
               for (const versionItem of versionsData.data) {
                 try {
-                  const versionResponse = await fetch(
+                  const versionData = await apiFetch<ConfigVersionResponse>(
                     `/api/configs/${config.id}/versions/${versionItem.version}`,
-                    { headers: { "X-API-KEY": apiKey } },
+                    apiKey,
                   );
-                  const versionData = await versionResponse.json();
 
                   if (versionData.success && versionData.data) {
                     allVersions.push(
@@ -260,11 +240,11 @@ export default function SimplifiedConfigEditor({
       return;
     }
 
-    const apiKey = getApiKey();
-    if (!apiKey) {
-      toast.error("No API key found. Please add an API key in the Keystore.");
+    if (!isAuthenticated) {
+      toast.error("Please log in to save configurations.");
       return;
     }
+    const apiKey = apiKeyStr;
 
     try {
       // Build config blob
@@ -312,20 +292,15 @@ export default function SimplifiedConfigEditor({
             `Updated to ${modelName} with temperature ${temperature}`,
         };
 
-        const response = await fetch(
+        const data = await apiFetch<ConfigVersionResponse>(
           `/api/configs/${existingConfig.config_id}/versions`,
+          apiKey,
           {
             method: "POST",
-            headers: {
-              "X-API-KEY": apiKey,
-              "Content-Type": "application/json",
-            },
             body: JSON.stringify(versionCreate),
           },
         );
 
-        const data = await response.json();
-
         if (!data.success) {
           toast.error(
             `Failed to create version: ${data.error || "Unknown error"}`,
@@ -345,16 +320,14 @@ export default function SimplifiedConfigEditor({
           commit_message: commitMessage.trim() || "Initial version",
         };
 
-        const response = await fetch("/api/configs", {
-          method: "POST",
-          headers: {
-            "X-API-KEY": apiKey,
-            "Content-Type": "application/json",
+        const data = await apiFetch<ConfigWithVersionResponse>(
+          "/api/configs",
+          apiKey,
+          {
+            method: "POST",
+            body: JSON.stringify(configCreate),
           },
-          body: JSON.stringify(configCreate),
-        });
-
-        const data: ConfigWithVersionResponse = await response.json();
+        );
 
         if (!data.success || !data.data) {
           toast.error(
@@ -367,32 +340,27 @@ export default function SimplifiedConfigEditor({
       }
 
       // Refresh configs list
-      const response = await fetch("/api/configs", {
-        headers: { "X-API-KEY": apiKey },
-      });
-      const data: ConfigListResponse = await response.json();
+      const refreshData = await apiFetch<ConfigListResponse>(
+        "/api/configs",
+        apiKey,
+      );
 
-      if (data.success && data.data) {
+      if (refreshData.success && refreshData.data) {
         const allVersions: SavedConfig[] = [];
-        for (const config of data.data) {
+        for (const config of refreshData.data) {
           try {
-            const versionsResponse = await fetch(
+            const versionsData = await apiFetch<ConfigVersionListResponse>(
               `/api/configs/${config.id}/versions`,
-              {
-                headers: { "X-API-KEY": apiKey },
-              },
+              apiKey,
             );
-            const versionsData: ConfigVersionListResponse =
-              await versionsResponse.json();
 
             if (versionsData.success && versionsData.data) {
               for (const versionItem of versionsData.data) {
                 try {
-                  const versionResponse = await fetch(
+                  const versionData = await apiFetch<ConfigVersionResponse>(
                     `/api/configs/${config.id}/versions/${versionItem.version}`,
-                    { headers: { "X-API-KEY": apiKey } },
+                    apiKey,
                   );
-                  const versionData = await versionResponse.json();
 
                   if (versionData.success && versionData.data) {
                     allVersions.push(
diff --git a/app/components/auth/FeatureGateModal.tsx b/app/components/auth/FeatureGateModal.tsx
new file mode 100644
index 0000000..2e0b821
--- /dev/null
+++ b/app/components/auth/FeatureGateModal.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Button } from "@/app/components";
+
+interface FeatureGateModalProps {
+  feature: string;
+  description: string;
+  onLogin: () => void;
+}
+
+export default function FeatureGateModal({
+  feature,
+  description,
+  onLogin,
+}: FeatureGateModalProps) {
+  return (
+    <div className="flex-1 flex items-center justify-center bg-bg-secondary">
+      <div className="max-w-sm text-center px-6">
+        <h3 className="text-lg font-semibold text-text-primary">{feature}</h3>
+        <p className="text-sm text-text-secondary mt-2 leading-relaxed">
+          {description}
+        </p>
+        <div className="mt-6">
+          <Button size="lg" onClick={onLogin}>
+            Log in
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/app/components/auth/LoginModal.tsx b/app/components/auth/LoginModal.tsx
new file mode 100644
index 0000000..d285f88
--- /dev/null
+++ b/app/components/auth/LoginModal.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useState } from "react";
+import { GoogleLogin, CredentialResponse } from "@react-oauth/google";
+import Modal from "@/app/components/Modal";
+import { Button, Field } from "@/app/components";
+import { useAuth, User, GoogleProfile } from "@/app/lib/context/AuthContext";
+import { useToast } from "@/app/components/Toast";
+import { apiFetch } from "@/app/lib/apiClient";
+
+interface LoginModalProps {
+  open: boolean;
+  onClose: () => void;
+}
+
+export default function LoginModal({ open, onClose }: LoginModalProps) {
+  const { addKey, loginWithGoogle } = useAuth();
+  const toast = useToast();
+  const [apiKey, setApiKey] = useState("");
+  const [isLoggingIn, setIsLoggingIn] = useState(false);
+
+  const handleGoogleSuccess = async (
+    credentialResponse: CredentialResponse,
+  ) => {
+    const token = credentialResponse.credential;
+    if (!token) {
+      toast.error("No credential received from Google.");
+      return;
+    }
+
+    setIsLoggingIn(true);
+
+    try {
+      const data = await apiFetch<{
+        access_token: string;
+        token_type: string;
+        user: User;
+        google_profile: GoogleProfile;
+      }>("/api/auth/google", "", {
+        method: "POST",
+        body: JSON.stringify({ token }),
+      });
+
+      loginWithGoogle(data.access_token, data.user, data.google_profile);
+
+      toast.success("Logged in successfully!");
+      onClose();
+    } catch (err) {
+      toast.error(
+        err instanceof Error ? err.message : "Failed to connect to server.",
+      );
+    } finally {
+      setIsLoggingIn(false);
+    }
+  };
+
+  const handleApiKeySubmit = () => {
+    if (!apiKey.trim()) {
+      toast.error("Please enter an API key.");
+      return;
+    }
+    addKey({
+      id: crypto.randomUUID(),
+      label: "API Key",
+      key: apiKey.trim(),
+      provider: "default",
+      createdAt: new Date().toISOString(),
+    });
+    toast.success("API key added successfully!");
+    setApiKey("");
+    onClose();
+  };
+
+  return (
+    <Modal open={open} onClose={onClose} maxWidth="max-w-md">
+      <div className="px-6 pb-6 text-center">
+        <h2 className="text-2xl font-semibold text-text-primary">
+          Log in or connect
+        </h2>
+        <p className="text-base text-text-secondary mt-2">
+          Sign in to access all features including evaluations, configurations,
+          and more.
+        </p>
+      </div>
+
+      <div className="px-6 pb-8 space-y-3">
+        {/* Google Login */}
+        <div className="flex justify-center">
+          {isLoggingIn ? (
+            <div className="w-full flex items-center justify-center py-3.5 rounded-full border border-border text-sm text-text-secondary">
+              Signing in...
+            </div>
+          ) : (
+            <GoogleLogin
+              onSuccess={handleGoogleSuccess}
+              onError={() => toast.error("Google login failed.")}
+              width="400"
+              shape="pill"
+              text="continue_with"
+              size="large"
+            />
+          )}
+        </div>
+
+        {/* Divider */}
+        <div className="flex items-center gap-3 py-1">
+          <div className="flex-1 h-px bg-border" />
+          <span className="text-xs text-text-secondary font-medium">OR</span>
+          <div className="flex-1 h-px bg-border" />
+        </div>
+
+        {/* API Key */}
+        <div>
+          <Field
+            label=""
+            value={apiKey}
+            onChange={setApiKey}
+            placeholder="Enter your X-API-KEY"
+            type="password"
+          />
+        </div>
+
+        <Button fullWidth size="lg" onClick={handleApiKeySubmit}>
+          Continue
+        </Button>
+      </div>
+    </Modal>
+  );
+}
diff --git a/app/components/auth/ProtectedPage.tsx b/app/components/auth/ProtectedPage.tsx
new file mode 100644
index 0000000..73b5c1c
--- /dev/null
+++ b/app/components/auth/ProtectedPage.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { useState, ReactNode } from "react";
+import { useAuth } from "@/app/lib/context/AuthContext";
+import { FeatureGateModal, LoginModal } from "@/app/components/auth";
+
+interface ProtectedPageProps {
+  feature: string;
+  description: string;
+  children: ReactNode;
+}
+
+export default function ProtectedPage({
+  feature,
+  description,
+  children,
+}: ProtectedPageProps) {
+  const { isAuthenticated } = useAuth();
+  const [showLoginModal, setShowLoginModal] = useState(false);
+
+  if (!isAuthenticated) {
+    return (
+      <>
+        <FeatureGateModal
+          feature={feature}
+          description={description}
+          onLogin={() => setShowLoginModal(true)}
+        />
+        <LoginModal
+          open={showLoginModal}
+          onClose={() => setShowLoginModal(false)}
+        />
+      </>
+    );
+  }
+
+  return <>{children}</>;
+}
diff --git a/app/components/auth/index.ts b/app/components/auth/index.ts
new file mode 100644
index 0000000..366a852
--- /dev/null
+++ b/app/components/auth/index.ts
@@ -0,0 +1,3 @@
+export { default as LoginModal } from "./LoginModal";
+export { default as FeatureGateModal } from "./FeatureGateModal";
+export { default as ProtectedPage } from "./ProtectedPage";
diff --git a/app/components/document/DocumentListing.tsx b/app/components/document/DocumentListing.tsx
index 296f0c6..edec9b1 100644
--- a/app/components/document/DocumentListing.tsx
+++ b/app/components/document/DocumentListing.tsx
@@ -1,11 +1,10 @@
 "use client";
 
-import { APIKey } from "@/app/lib/types/credentials";
 import { formatDate } from "@/app/components/utils";
 import { Document } from "@/app/(main)/document/page";
+import { useAuth } from "@/app/lib/context/AuthContext";
 import {
   RefreshIcon,
-  KeyIcon,
   DocumentFileIcon,
   TrashIcon,
 } from "@/app/components/icons";
@@ -19,7 +18,6 @@ interface DocumentListingProps {
   isLoading: boolean;
   isLoadingMore: boolean;
   error: string | null;
-  apiKey: APIKey | null;
   scrollRef: React.RefObject<HTMLDivElement | null>;
 }
 
@@ -32,9 +30,9 @@ export function DocumentListing({
   isLoading,
   isLoadingMore,
   error,
-  apiKey,
   scrollRef,
 }: DocumentListingProps) {
+  const { isAuthenticated } = useAuth();
   return (
     <div className="h-full flex flex-col">
       <div className="px-4 py-2.5 border-b bg-[hsl(0,0%,100%)] border-[hsl(0,0%,85%)]">
@@ -61,21 +59,12 @@ export function DocumentListing({
             <RefreshIcon className="w-12 h-12 mx-auto mb-4 animate-spin" />
             <p className="text-sm">Loading documents...</p>
           </div>
-        ) : !apiKey ? (
+        ) : !isAuthenticated ? (
           <div className="text-center py-12 text-[hsl(330,3%,49%)]">
-            <KeyIcon className="mx-auto mb-4" />
             <p className="font-medium mb-2 text-[hsl(330,3%,19%)]">
-              No API key found
+              Login required
             </p>
-            <p className="text-sm mb-4">
-              Please add an API key in the Keystore
-            </p>
-            <a
-              href="/keystore"
-              className="inline-block px-6 py-2 rounded-md text-sm font-medium transition-colors bg-[#171717] text-white"
-            >
-              Go to Keystore
-            </a>
+            <p className="text-sm">Please log in to manage documents</p>
           </div>
         ) : error ? (
           <div className="border rounded-lg p-6 bg-[hsl(8,86%,95%)] border-[hsl(8,86%,80%)]">
diff --git a/app/components/document/DocumentPreview.tsx b/app/components/document/DocumentPreview.tsx
index fb62d00..96d8ff8 100644
--- a/app/components/document/DocumentPreview.tsx
+++ b/app/components/document/DocumentPreview.tsx
@@ -1,6 +1,7 @@
 "use client";
 
 import { useState, useEffect } from "react";
+import Image from "next/image";
 import { formatDate } from "@/app/components/utils";
 import { Document } from "@/app/(main)/document/page";
 import {
@@ -135,10 +136,13 @@ export function DocumentPreview({ document, isLoading }: DocumentPreviewProps) {
                     </p>
                   </div>
                 ) : (
-                  <img
+                  <Image
                     src={document.signed_url}
                     alt={document.fname}
+                    width={800}
+                    height={600}
                     className="max-w-full h-auto rounded"
+                    unoptimized
                     onError={() => {
                       setImageLoadError(true);
                     }}
diff --git a/app/components/evaluations/DatasetsTab.tsx b/app/components/evaluations/DatasetsTab.tsx
index f89fd29..cc39df1 100644
--- a/app/components/evaluations/DatasetsTab.tsx
+++ b/app/components/evaluations/DatasetsTab.tsx
@@ -2,9 +2,9 @@
 
 import { useState, useEffect, useRef } from "react";
 import { colors } from "@/app/lib/colors";
-import { APIKey } from "@/app/lib/types/credentials";
 import { Dataset } from "@/app/(main)/datasets/page";
 import { useToast } from "@/app/components/Toast";
+import { apiFetch } from "@/app/lib/apiClient";
 import EvalDatasetDescription from "./EvalDatasetDescription";
 import Loader from "@/app/components/Loader";
 
@@ -24,8 +24,7 @@ export interface DatasetsTabProps {
   resetForm: () => void;
   storedDatasets: Dataset[];
   isDatasetsLoading: boolean;
-  apiKeys: APIKey[];
-  selectedKeyId: string;
+  apiKey: string;
   loadStoredDatasets: () => void;
   toast: ReturnType<typeof useToast>;
 }
@@ -46,8 +45,7 @@ export default function DatasetsTab({
   resetForm,
   storedDatasets,
   isDatasetsLoading,
-  apiKeys,
-  selectedKeyId,
+  apiKey,
   loadStoredDatasets,
   toast,
 }: DatasetsTabProps) {
@@ -73,24 +71,17 @@ export default function DatasetsTab({
   }, [showDuplicationInfo]);
 
   const handleDeleteDataset = async (datasetId: number) => {
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) return;
-
     setDeletingId(datasetId);
     try {
-      const response = await fetch(`/api/evaluations/datasets/${datasetId}`, {
+      await apiFetch(`/api/evaluations/datasets/${datasetId}`, apiKey, {
         method: "DELETE",
-        headers: { "X-API-KEY": selectedKey.key },
       });
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(errorData.error || "Failed to delete dataset");
-      }
       toast.success("Dataset deleted");
       loadStoredDatasets();
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    } catch (err: any) {
-      toast.error(err.message || "Failed to delete dataset");
+    } catch (err) {
+      toast.error(
+        err instanceof Error ? err.message : "Failed to delete dataset",
+      );
     } finally {
       setDeletingId(null);
     }
@@ -106,23 +97,16 @@ export default function DatasetsTab({
   } | null>(null);
 
   const handleViewDataset = async (datasetId: number, datasetName: string) => {
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) return;
-
     setViewingId(datasetId);
     try {
-      const response = await fetch(
+      const data = await apiFetch<{
+        data?: { signed_url?: string };
+        signed_url?: string;
+        csv_content?: string;
+      }>(
         `/api/evaluations/datasets/${datasetId}?include_signed_url=true&fetch_content=true`,
-        {
-          method: "GET",
-          headers: { "X-API-KEY": selectedKey.key },
-        },
+        apiKey,
       );
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(errorData.error || "Failed to get dataset");
-      }
-      const data = await response.json();
       const signedUrl = data?.data?.signed_url || data?.signed_url;
       const csvText = data?.csv_content;
       if (!csvText) {
diff --git a/app/components/evaluations/EvaluationsTab.tsx b/app/components/evaluations/EvaluationsTab.tsx
index 76f6401..d6c06c0 100644
--- a/app/components/evaluations/EvaluationsTab.tsx
+++ b/app/components/evaluations/EvaluationsTab.tsx
@@ -1,6 +1,7 @@
 "use client";
 
 import { useState, useEffect, useCallback } from "react";
+import { apiFetch } from "@/app/lib/apiClient";
 import { colors } from "@/app/lib/colors";
 import { Dataset } from "@/app/(main)/datasets/page";
 import { EvalJob, AssistantConfig } from "@/app/components/types";
@@ -8,14 +9,13 @@ import ConfigSelector from "@/app/components/ConfigSelector";
 import Loader from "@/app/components/Loader";
 import EvalRunCard from "./EvalRunCard";
 import EvalDatasetDescription from "./EvalDatasetDescription";
-import { APIKey } from "@/app/lib/types/credentials";
+import { useAuth } from "@/app/lib/context/AuthContext";
 
 type Tab = "datasets" | "evaluations";
 
 export interface EvaluationsTabProps {
   leftPanelWidth: number;
-  apiKeys: APIKey[];
-  selectedKeyId: string;
+  apiKey: string;
   storedDatasets: Dataset[];
   selectedDatasetId: string;
   setSelectedDatasetId: (id: string) => void;
@@ -31,8 +31,7 @@ export interface EvaluationsTabProps {
 
 export default function EvaluationsTab({
   leftPanelWidth,
-  apiKeys,
-  selectedKeyId,
+  apiKey,
   storedDatasets,
   selectedDatasetId,
   setSelectedDatasetId,
@@ -64,34 +63,19 @@ export default function EvaluationsTab({
     !isEvaluating;
 
   // Fetch evaluation jobs
+  const { isAuthenticated } = useAuth();
+
   const fetchEvaluations = useCallback(async () => {
-    if (!selectedKeyId) {
-      setError("Please select an API key first");
-      return;
-    }
-    const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-    if (!selectedKey) {
-      setError("Selected API key not found");
-      return;
-    }
+    if (!isAuthenticated) return;
 
     setIsLoading(true);
     setError(null);
 
     try {
-      const response = await fetch("/api/evaluations", {
-        method: "GET",
-        headers: { "X-API-KEY": selectedKey.key },
-      });
-      if (!response.ok) {
-        const errorData = await response.json().catch(() => ({}));
-        throw new Error(
-          errorData.error ||
-            errorData.message ||
-            `Failed to fetch evaluations: ${response.status}`,
-        );
-      }
-      const data = await response.json();
+      const data = await apiFetch<EvalJob[] | { data: EvalJob[] }>(
+        "/api/evaluations",
+        apiKey,
+      );
       setEvalJobs(Array.isArray(data) ? data : data.data || []);
     } catch (err: unknown) {
       setError(
@@ -100,25 +84,21 @@ export default function EvaluationsTab({
     } finally {
       setIsLoading(false);
     }
-  }, [apiKeys, selectedKeyId]);
+  }, [apiKey, isAuthenticated]);
 
   // Fetch assistant config
   const fetchAssistantConfig = useCallback(
     async (assistantId: string) => {
-      if (!selectedKeyId) return;
-      const selectedKey = apiKeys.find((k) => k.id === selectedKeyId);
-      if (!selectedKey) return;
+      if (!isAuthenticated) return;
 
       try {
-        const response = await fetch(`/api/assistant/${assistantId}`, {
-          method: "GET",
-          headers: { "X-API-KEY": selectedKey.key },
-        });
-        if (!response.ok) return;
-        const result = await response.json();
+        const result = await apiFetch<{
+          success: boolean;
+          data?: AssistantConfig;
+        }>(`/api/assistant/${assistantId}`, apiKey);
         if (result.success && result.data) {
           setAssistantConfigs((prev) =>
-            new Map(prev).set(assistantId, result.data),
+            new Map(prev).set(assistantId, result.data!),
           );
         }
       } catch (err) {
@@ -128,7 +108,7 @@ export default function EvaluationsTab({
         );
       }
     },
-    [apiKeys, selectedKeyId],
+    [apiKey, isAuthenticated],
   );
 
   useEffect(() => {
@@ -140,8 +120,8 @@ export default function EvaluationsTab({
   }, [evalJobs, assistantConfigs, fetchAssistantConfig]);
 
   useEffect(() => {
-    if (selectedKeyId) fetchEvaluations();
-  }, [selectedKeyId, fetchEvaluations]);
+    if (isAuthenticated) fetchEvaluations();
+  }, [isAuthenticated, fetchEvaluations]);
 
   return (
     <div className="flex-1 flex overflow-hidden">
diff --git a/app/components/speech-to-text/ModelComparisonCard.tsx b/app/components/speech-to-text/ModelComparisonCard.tsx
index 38ad0bb..687b30a 100644
--- a/app/components/speech-to-text/ModelComparisonCard.tsx
+++ b/app/components/speech-to-text/ModelComparisonCard.tsx
@@ -71,7 +71,6 @@ export default function ModelComparisonCard({
   // Also reset when modelId changes (new model added)
   useEffect(() => {
     if (status === "pending") {
-      // eslint-disable-next-line react-hooks/set-state-in-effect
       setIsExpanded(false);
     }
   }, [status, modelId]);
diff --git a/app/hooks/useConfigs.ts b/app/hooks/useConfigs.ts
index 5613802..2e9e5c0 100644
--- a/app/hooks/useConfigs.ts
+++ b/app/hooks/useConfigs.ts
@@ -69,15 +69,15 @@ export function useConfigs(options?: { pageSize?: number }): UseConfigsResult {
   const [error, setError] = useState<string | null>(null);
   const [isCached, setIsCached] = useState<boolean>(false);
   const [totalKnownCount, setTotalKnownCount] = useState<number>(0);
-  const { activeKey, isHydrated } = useAuth();
-  const apiKey = activeKey?.key;
+  const { activeKey, isHydrated, isAuthenticated } = useAuth();
+  const apiKey = activeKey?.key ?? "";
 
   const fetchConfigs = useCallback(
     async (force: boolean = false) => {
       if (!isHydrated) return;
 
-      if (!apiKey) {
-        setError("No API key found. Please add an API key in the Keystore.");
+      if (!isAuthenticated) {
+        setError("Please log in to continue.");
         setIsLoading(false);
         return;
       }
@@ -239,7 +239,7 @@ export function useConfigs(options?: { pageSize?: number }): UseConfigsResult {
         return;
       }
 
-      if (!apiKey) return;
+      if (!isAuthenticated) return;
 
       const loadPromise = (async () => {
         const versionsData = await apiFetch<{
@@ -288,7 +288,7 @@ export function useConfigs(options?: { pageSize?: number }): UseConfigsResult {
       const existing = pendingSingleVersionLoads.get(key);
       if (existing) return existing;
 
-      if (!apiKey) return null;
+      if (!isAuthenticated) return null;
 
       const configSource = configs.find((c) => c.config_id === config_id);
       // Fall back to the lightweight allConfigMeta when the config hasn't been detail-fetched yet
@@ -364,8 +364,8 @@ export function useConfigs(options?: { pageSize?: number }): UseConfigsResult {
   const loadMoreConfigs = useCallback(async () => {
     if (!configState.allConfigMeta || configState.allConfigMeta.length === 0)
       return;
-    const apiKey = activeKey?.key;
-    if (!apiKey) return;
+    const localApiKey = activeKey?.key ?? "";
+    if (!isAuthenticated) return;
 
     const loadedIds = new Set(
       (configState.inMemoryCache?.configs ?? configs).map((c) => c.config_id),
@@ -384,7 +384,11 @@ export function useConfigs(options?: { pageSize?: number }): UseConfigsResult {
 
     configState.pendingLoadMore = (async () => {
       const { newVersions, newVersionCounts, newConfigMeta } =
-        await fetchNextConfigBatch(apiKey, loadedIds, pageSize ?? PAGE_SIZE);
+        await fetchNextConfigBatch(
+          localApiKey,
+          loadedIds,
+          pageSize ?? PAGE_SIZE,
+        );
 
       setConfigs((prev) => {
         const merged = [...prev, ...newVersions];
diff --git a/app/hooks/usePaginatedList.ts b/app/hooks/usePaginatedList.ts
index 21f64d0..dc038f2 100644
--- a/app/hooks/usePaginatedList.ts
+++ b/app/hooks/usePaginatedList.ts
@@ -49,8 +49,8 @@ export function usePaginatedList<T>(options: {
     limit = DEFAULT_PAGE_LIMIT,
     extraParams,
   } = options;
-  const { activeKey, isHydrated } = useAuth();
-  const apiKey = activeKey?.key;
+  const { activeKey, isHydrated, isAuthenticated } = useAuth();
+  const apiKey = activeKey?.key ?? "";
 
   const [items, setItems] = useState<T[]>([]);
   const [isLoading, setIsLoading] = useState(true);
@@ -63,8 +63,8 @@ export function usePaginatedList<T>(options: {
 
   const fetchPage = useCallback(
     async (skip: number, replace: boolean) => {
-      if (!apiKey) {
-        setError("No API key found. Please add an API key in the Keystore.");
+      if (!isAuthenticated) {
+        setError("Please log in to continue.");
         return;
       }
 
@@ -91,13 +91,13 @@ export function usePaginatedList<T>(options: {
       setHasMore(data.metadata?.has_more ?? false);
       skipRef.current = skip + newItems.length;
     },
-    [apiKey, endpoint, query, limit, extraParams],
+    [apiKey, isAuthenticated, endpoint, query, limit, extraParams],
   );
 
   useEffect(() => {
     if (!isHydrated) return;
-    if (!apiKey) {
-      setError("No API key found. Please add an API key in the Keystore.");
+    if (!isAuthenticated) {
+      setError("Please log in to continue.");
       setIsLoading(false);
       return;
     }
@@ -121,7 +121,7 @@ export function usePaginatedList<T>(options: {
     return () => {
       cancelled = true;
     };
-  }, [fetchPage, isHydrated, apiKey]);
+  }, [fetchPage, isHydrated, apiKey, isAuthenticated]);
 
   const loadMore = useCallback(() => {
     if (loadingMoreRef.current || !hasMore || isLoading) return;
diff --git a/app/layout.tsx b/app/layout.tsx
index 48db982..00a016c 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,9 +1,7 @@
 import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
-import { ToastProvider } from "@/app/components/Toast";
-import { AuthProvider } from "@/app/lib/context/AuthContext";
-import { AppProvider } from "@/app/lib/context/AppContext";
+import Providers from "./Providers";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -30,11 +28,7 @@ export default function RootLayout({
       <body
         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
       >
-        <ToastProvider>
-          <AuthProvider>
-            <AppProvider>{children}</AppProvider>
-          </AuthProvider>
-        </ToastProvider>
+        <Providers>{children}</Providers>
       </body>
     </html>
   );
diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts
index 790743e..4d62510 100644
--- a/app/lib/apiClient.ts
+++ b/app/lib/apiClient.ts
@@ -4,8 +4,8 @@ const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
 
 /**
  * Passthrough proxy helper for Next.js route handlers.
- * Extracts X-API-KEY from the incoming request and forwards it to the backend.
- * Returns raw { status, data } so the route handler can relay the exact HTTP status.
+ * Extracts X-API-KEY and cookies from the incoming request and forwards them to the backend.
+ * Returns raw { status, data, headers } so the route handler can relay the exact HTTP status.
  */
 export async function apiClient(
   request: NextRequest | Request,
@@ -13,48 +13,122 @@ export async function apiClient(
   options: RequestInit = {},
 ) {
   const apiKey = request.headers.get("X-API-KEY") || "";
+  const cookie = request.headers.get("Cookie") || "";
   const headers = new Headers(options.headers);
-  // Don't set Content-Type for FormData — the browser sets it with the boundary
   if (!(options.body instanceof FormData)) {
     headers.set("Content-Type", "application/json");
   }
   headers.set("X-API-KEY", apiKey);
+  if (cookie) headers.set("Cookie", cookie);
 
   const response = await fetch(`${BACKEND_URL}${endpoint}`, {
     ...options,
     headers,
+    credentials: "include",
   });
 
-  // 204 No Content has no body
   const text = response.status === 204 ? "" : await response.text();
   const data = text ? JSON.parse(text) : null;
 
-  return { status: response.status, data };
+  return { status: response.status, data, headers: response.headers };
+}
+
+/**
+ * Dispatched when both the access token AND refresh token are expired /
+ * invalid.  AuthContext listens for this and triggers logout.
+ */
+export const AUTH_EXPIRED_EVENT = "kaapi:auth-expired";
+
+/**
+ * Singleton refresh promise so concurrent 401s don't fire multiple
+ * refresh requests — they all await the same in-flight call.
+ */
+let refreshPromise: Promise<boolean> | null = null;
+
+async function tryRefreshToken(): Promise<boolean> {
+  if (refreshPromise) return refreshPromise;
+
+  refreshPromise = (async () => {
+    try {
+      const res = await fetch("/api/auth/refresh", {
+        method: "POST",
+        credentials: "include",
+      });
+      return res.ok;
+    } catch {
+      return false;
+    }
+  })().finally(() => {
+    refreshPromise = null;
+  });
+
+  return refreshPromise;
 }
 
 /**
  * Client-side fetch helper for Next.js route handlers (/api/*).
- * Attaches the X-API-KEY header and throws on non-OK responses.
- * Use this in "use client" pages instead of raw fetch calls.
+ * Attaches the X-API-KEY header and includes credentials for cookie-based auth.
+ *
+ * On a 401 response it automatically attempts a token refresh via
+ * `/api/auth/refresh`.  If the refresh succeeds the original request is
+ * retried once.  If the refresh also fails, a `kaapi:auth-expired` event
+ * is dispatched so AuthContext can trigger logout.
  */
 export async function apiFetch<T>(
   url: string,
   apiKey: string,
   options: RequestInit = {},
 ): Promise<T> {
-  const headers = new Headers(options.headers);
-  if (!(options.body instanceof FormData)) {
-    headers.set("Content-Type", "application/json");
-  }
-  headers.set("X-API-KEY", apiKey);
+  const buildHeaders = () => {
+    const headers = new Headers(options.headers);
+    if (!(options.body instanceof FormData)) {
+      headers.set("Content-Type", "application/json");
+    }
+    headers.set("X-API-KEY", apiKey);
+    return headers;
+  };
+
   const res = await fetch(url, {
     ...options,
-    headers,
+    headers: buildHeaders(),
+    credentials: "include",
   });
-  const data = await res.json();
-  if (!res.ok)
+
+  // Happy path
+  if (res.ok) return (await res.json()) as T;
+
+  // Not a 401 — throw immediately
+  if (res.status !== 401) {
+    const data = await res.json();
     throw new Error(
       data.error || data.message || `Request failed: ${res.status}`,
     );
-  return data as T;
+  }
+
+  // 401 — attempt a silent token refresh
+  const refreshed = await tryRefreshToken();
+
+  if (refreshed) {
+    const retry = await fetch(url, {
+      ...options,
+      headers: buildHeaders(),
+      credentials: "include",
+    });
+    const retryData = await retry.json();
+    if (retry.ok) return retryData as T;
+
+    throw new Error(
+      retryData.error || retryData.message || `Request failed: ${retry.status}`,
+    );
+  }
+
+  if (typeof window !== "undefined") {
+    window.dispatchEvent(new CustomEvent(AUTH_EXPIRED_EVENT));
+  }
+  const data = await res.json().catch(() => ({}));
+  throw new Error(
+    (data as Record<string, string>).error ||
+      (data as Record<string, string>).message ||
+      "Session expired. Please log in again.",
+  );
 }
diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx
index c977f09..fbf59a1 100644
--- a/app/lib/context/AuthContext.tsx
+++ b/app/lib/context/AuthContext.tsx
@@ -8,27 +8,17 @@ import {
   useEffect,
 } from "react";
 import { APIKey } from "@/app/lib/types/credentials";
-import { apiFetch } from "@/app/lib/apiClient";
+import {
+  User,
+  GoogleProfile,
+  Session,
+  AuthContextValue,
+} from "@/app/lib/types/auth";
+import { apiFetch, AUTH_EXPIRED_EVENT } from "@/app/lib/apiClient";
+export type { User, GoogleProfile, Session } from "@/app/lib/types/auth";
 
 const STORAGE_KEY = "kaapi_api_keys";
-
-export interface User {
-  id: number;
-  email: string;
-  full_name: string;
-  is_active: boolean;
-  is_superuser: boolean;
-}
-
-interface AuthContextValue {
-  apiKeys: APIKey[];
-  activeKey: APIKey | null;
-  isHydrated: boolean;
-  currentUser: User | null;
-  addKey: (key: APIKey) => void;
-  removeKey: (id: string) => void;
-  setKeys: (keys: APIKey[]) => void;
-}
+const SESSION_KEY = "kaapi_session";
 
 const AuthContext = createContext<AuthContextValue | null>(null);
 
@@ -36,37 +26,63 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
   const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
   const [isHydrated, setIsHydrated] = useState(false);
   const [currentUser, setCurrentUser] = useState<User | null>(null);
+  const [session, setSession] = useState<Session | null>(null);
 
-  // Initialize from localStorage after hydration to avoid SSR mismatch.
+  // Initialize from localStorage after hydration
   useEffect(() => {
     try {
-      const stored = localStorage.getItem(STORAGE_KEY);
-      if (stored) setApiKeys(JSON.parse(stored));
+      const storedKeys = localStorage.getItem(STORAGE_KEY);
+      if (storedKeys) setApiKeys(JSON.parse(storedKeys));
+
+      const storedSession = localStorage.getItem(SESSION_KEY);
+      if (storedSession) {
+        const parsed = JSON.parse(storedSession) as Session;
+        setSession(parsed);
+        if (parsed.user) setCurrentUser(parsed.user);
+      }
     } catch {
       /* ignore malformed data */
     }
     setIsHydrated(true);
   }, []);
 
+  // Always fetch the latest user profile from the backend on hydration.
   useEffect(() => {
-    const apiKey = apiKeys[0]?.key;
-    if (!apiKey || !isHydrated) return;
+    if (!isHydrated) return;
+    const hasApiKey = !!apiKeys[0]?.key;
+    const hasSession = !!session;
+    if (!hasApiKey && !hasSession) return;
 
     let cancelled = false;
 
     (async () => {
       try {
-        const data = await apiFetch<User>("/api/users/me", apiKey);
-        if (!cancelled) setCurrentUser(data);
+        const data = await apiFetch<User>(
+          "/api/users/me",
+          apiKeys[0]?.key ?? "",
+        );
+        if (!cancelled) {
+          setCurrentUser(data);
+          const storedRaw = localStorage.getItem(SESSION_KEY);
+          if (storedRaw) {
+            try {
+              const stored = JSON.parse(storedRaw);
+              stored.user = data;
+              localStorage.setItem(SESSION_KEY, JSON.stringify(stored));
+            } catch {
+              /* ignore */
+            }
+          }
+        }
       } catch {
-        // silently ignore — user info is non-critical
+        // silently ignore
       }
     })();
 
     return () => {
       cancelled = true;
     };
-  }, [apiKeys, isHydrated]);
+  }, [apiKeys, session, isHydrated]);
 
   const persist = useCallback((keys: APIKey[]) => {
     setApiKeys(keys);
@@ -87,16 +103,53 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
   );
   const setKeys = useCallback((keys: APIKey[]) => persist(keys), [persist]);
 
+  const loginWithGoogle = useCallback(
+    (accessToken: string, user?: User, googleProfile?: GoogleProfile) => {
+      const newSession: Session = {
+        accessToken,
+        user: user ?? null,
+        googleProfile: googleProfile ?? null,
+      };
+      setSession(newSession);
+      localStorage.setItem(SESSION_KEY, JSON.stringify(newSession));
+
+      if (user) setCurrentUser(user);
+    },
+    [],
+  );
+
+  const logout = useCallback(() => {
+    setSession(null);
+    setCurrentUser(null);
+    localStorage.removeItem(SESSION_KEY);
+    persist([]);
+  }, [persist]);
+
+  // logout when both access + refresh tokens are expired
+  useEffect(() => {
+    const handleExpired = () => logout();
+    window.addEventListener(AUTH_EXPIRED_EVENT, handleExpired);
+    return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handleExpired);
+  }, [logout]);
+
+  const activeKey = apiKeys[0] ?? null;
+  const isAuthenticated = !!activeKey || !!session;
+
   return (
     <AuthContext.Provider
       value={{
         apiKeys,
-        activeKey: apiKeys[0] ?? null,
+        activeKey,
         isHydrated,
         currentUser,
+        googleProfile: session?.googleProfile ?? null,
+        session,
+        isAuthenticated,
         addKey,
         removeKey,
         setKeys,
+        loginWithGoogle,
+        logout,
       }}
     >
       {children}
diff --git a/app/lib/types/auth.ts b/app/lib/types/auth.ts
new file mode 100644
index 0000000..6a2f84b
--- /dev/null
+++ b/app/lib/types/auth.ts
@@ -0,0 +1,42 @@
+import { APIKey } from "./credentials";
+
+export interface User {
+  id: number;
+  email: string;
+  full_name: string;
+  is_active: boolean;
+  is_superuser: boolean;
+}
+
+export interface GoogleProfile {
+  email: string;
+  name: string;
+  picture: string;
+  given_name: string;
+  family_name: string;
+}
+
+export interface Session {
+  accessToken: string;
+  user: User | null;
+  googleProfile: GoogleProfile | null;
+}
+
+export interface AuthContextValue {
+  apiKeys: APIKey[];
+  activeKey: APIKey | null;
+  isHydrated: boolean;
+  currentUser: User | null;
+  googleProfile: GoogleProfile | null;
+  session: Session | null;
+  isAuthenticated: boolean;
+  addKey: (key: APIKey) => void;
+  removeKey: (id: string) => void;
+  setKeys: (keys: APIKey[]) => void;
+  loginWithGoogle: (
+    accessToken: string,
+    user?: User,
+    googleProfile?: GoogleProfile,
+  ) => void;
+  logout: () => void;
+}
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..5a2c26e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,14 @@
 import type { NextConfig } from "next";
 
 const nextConfig: NextConfig = {
-  /* config options here */
+  images: {
+    remotePatterns: [
+      {
+        protocol: "https",
+        hostname: "lh3.googleusercontent.com",
+      },
+    ],
+  },
 };
 
 export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 48bbccf..3aa4d57 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
       "name": "kaapi-frontend",
       "version": "0.1.0",
       "dependencies": {
+        "@react-oauth/google": "^0.13.4",
         "date-fns": "^4.1.0",
         "date-fns-tz": "^3.2.0",
         "next": "^16.0.7",
@@ -1250,6 +1251,16 @@
         "node": ">=12.4.0"
       }
     },
+    "node_modules/@react-oauth/google": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz",
+      "integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
     "node_modules/@rtsao/scc": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
diff --git a/package.json b/package.json
index 38e2ba0..e65d7f7 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "prepare": "husky"
   },
   "dependencies": {
+    "@react-oauth/google": "^0.13.4",
     "date-fns": "^4.1.0",
     "date-fns-tz": "^3.2.0",
     "next": "^16.0.7",

From 06acfdc444f9f7c4f7fe2d124a7e9c2b062c687b Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Tue, 31 Mar 2026 23:22:19 +0530
Subject: [PATCH 02/10] feat(*): added the logout api endpoint

---
 app/api/auth/logout/route.ts    | 19 +++++++++++++++++++
 app/lib/context/AuthContext.tsx | 10 +++++++++-
 app/lib/types/auth.ts           |  2 +-
 3 files changed, 29 insertions(+), 2 deletions(-)
 create mode 100644 app/api/auth/logout/route.ts

diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..3b5994f
--- /dev/null
+++ b/app/api/auth/logout/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest, NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
+
+export async function POST(request: NextRequest) {
+  const { status, data, headers } = await apiClient(
+    request,
+    "/api/v1/auth/logout",
+    { method: "POST" },
+  );
+
+  const res = NextResponse.json(data, { status });
+
+  const setCookies = headers.getSetCookie?.() ?? [];
+  for (const cookie of setCookies) {
+    res.headers.append("Set-Cookie", cookie);
+  }
+
+  return res;
+}
diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx
index fbf59a1..f78f796 100644
--- a/app/lib/context/AuthContext.tsx
+++ b/app/lib/context/AuthContext.tsx
@@ -118,7 +118,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
     [],
   );
 
-  const logout = useCallback(() => {
+  const logout = useCallback(async () => {
+    try {
+      await fetch("/api/auth/logout", {
+        method: "POST",
+        credentials: "include",
+      });
+    } catch {
+      // Clear local state even if the backend call fails
+    }
     setSession(null);
     setCurrentUser(null);
     localStorage.removeItem(SESSION_KEY);
diff --git a/app/lib/types/auth.ts b/app/lib/types/auth.ts
index 6a2f84b..0c535ed 100644
--- a/app/lib/types/auth.ts
+++ b/app/lib/types/auth.ts
@@ -38,5 +38,5 @@ export interface AuthContextValue {
     user?: User,
     googleProfile?: GoogleProfile,
   ) => void;
-  logout: () => void;
+  logout: () => Promise<void>;
 }

From b2ef7833842f48069a313eeb55c346fd458fcaf7 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Tue, 31 Mar 2026 23:24:57 +0530
Subject: [PATCH 03/10] fix(*): align the bottom action items tab

---
 app/(main)/speech-to-text/page.tsx         | 2 +-
 app/(main)/text-to-speech/page.tsx         | 2 +-
 app/components/evaluations/DatasetsTab.tsx | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/(main)/speech-to-text/page.tsx b/app/(main)/speech-to-text/page.tsx
index 115d4a1..a6b114e 100644
--- a/app/(main)/speech-to-text/page.tsx
+++ b/app/(main)/speech-to-text/page.tsx
@@ -1556,7 +1556,7 @@ function DatasetsTab({
 
         {/* Bottom Action Bar */}
         <div
-          className="flex-shrink-0 border-t px-4 py-3 flex items-center justify-end gap-3"
+          className="flex-shrink-0 border-t px-4 py-4 flex items-center justify-end gap-3"
           style={{
             borderColor: colors.border,
             backgroundColor: colors.bg.primary,
diff --git a/app/(main)/text-to-speech/page.tsx b/app/(main)/text-to-speech/page.tsx
index a8af34d..5badc73 100644
--- a/app/(main)/text-to-speech/page.tsx
+++ b/app/(main)/text-to-speech/page.tsx
@@ -1083,7 +1083,7 @@ function DatasetsTab({
 
         {/* Bottom Action Bar */}
         <div
-          className="flex-shrink-0 border-t px-4 py-3 flex items-center justify-end gap-3"
+          className="flex-shrink-0 border-t px-4 py-4 flex items-center justify-end gap-3"
           style={{
             borderColor: colors.border,
             backgroundColor: colors.bg.primary,
diff --git a/app/components/evaluations/DatasetsTab.tsx b/app/components/evaluations/DatasetsTab.tsx
index cc39df1..304343c 100644
--- a/app/components/evaluations/DatasetsTab.tsx
+++ b/app/components/evaluations/DatasetsTab.tsx
@@ -481,7 +481,7 @@ export default function DatasetsTab({
 
         {/* Bottom Action Bar */}
         <div
-          className="flex-shrink-0 border-t px-4 py-3 flex items-center justify-end gap-3"
+          className="flex-shrink-0 border-t px-4 py-4 flex items-center justify-end gap-3"
           style={{
             borderColor: colors.border,
             backgroundColor: colors.bg.primary,

From 3fbc5d30ebb2851dc52bac16baa83fd33f1cce51 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Wed, 1 Apr 2026 21:46:03 +0530
Subject: [PATCH 04/10] fix(*): UI updates and users flow

---
 app/(main)/settings/credentials/page.tsx      |  55 +---
 app/(main)/settings/onboarding/page.tsx       |  33 +-
 app/api/user-projects/[userId]/route.ts       |  25 ++
 app/api/user-projects/route.ts                |  40 +++
 app/components/GatePopover.tsx                |  54 +++
 app/components/Modal.tsx                      |   2 +-
 app/components/Sidebar.tsx                    | 247 ++++----------
 app/components/auth/LoginModal.tsx            |  63 ++--
 app/components/icons/index.tsx                |   1 +
 app/components/icons/sidebar/LogoutIcon.tsx   |  21 ++
 app/{ => components/providers}/Providers.tsx  |   0
 app/components/providers/index.ts             |   1 +
 app/components/settings/SettingsSidebar.tsx   | 139 ++++++++
 .../settings/credentials/CredentialForm.tsx   |   2 +-
 .../settings/onboarding/ProjectList.tsx       |  32 +-
 .../settings/onboarding/UserList.tsx          | 308 ++++++++++++++++++
 app/components/settings/onboarding/index.ts   |   1 +
 app/components/user-menu/Branding.tsx         |  12 +
 app/components/user-menu/UserMenuPopover.tsx  |  83 +++++
 app/components/user-menu/index.ts             |   2 +
 app/layout.tsx                                |   2 +-
 app/lib/navConfig.ts                          |  46 +++
 app/lib/types/nav.ts                          |  53 +++
 app/lib/types/onboarding.ts                   |  24 ++
 24 files changed, 948 insertions(+), 298 deletions(-)
 create mode 100644 app/api/user-projects/[userId]/route.ts
 create mode 100644 app/api/user-projects/route.ts
 create mode 100644 app/components/GatePopover.tsx
 create mode 100644 app/components/icons/sidebar/LogoutIcon.tsx
 rename app/{ => components/providers}/Providers.tsx (100%)
 create mode 100644 app/components/providers/index.ts
 create mode 100644 app/components/settings/SettingsSidebar.tsx
 create mode 100644 app/components/settings/onboarding/UserList.tsx
 create mode 100644 app/components/user-menu/Branding.tsx
 create mode 100644 app/components/user-menu/UserMenuPopover.tsx
 create mode 100644 app/components/user-menu/index.ts
 create mode 100644 app/lib/navConfig.ts
 create mode 100644 app/lib/types/nav.ts

diff --git a/app/(main)/settings/credentials/page.tsx b/app/(main)/settings/credentials/page.tsx
index 0888c78..c80364e 100644
--- a/app/(main)/settings/credentials/page.tsx
+++ b/app/(main)/settings/credentials/page.tsx
@@ -8,11 +8,10 @@
 "use client";
 
 import { useState, useEffect } from "react";
-import Sidebar from "@/app/components/Sidebar";
+import SettingsSidebar from "@/app/components/settings/SettingsSidebar";
 import { colors } from "@/app/lib/colors";
 import { useToast } from "@/app/components/Toast";
 import { useAuth } from "@/app/lib/context/AuthContext";
-import { useApp } from "@/app/lib/context/AppContext";
 import {
   PROVIDERS,
   Credential,
@@ -25,7 +24,6 @@ import { apiFetch } from "@/app/lib/apiClient";
 
 export default function CredentialsPage() {
   const toast = useToast();
-  const { sidebarCollapsed, setSidebarCollapsed } = useApp();
   const { apiKeys, isAuthenticated } = useAuth();
   const [selectedProvider, setSelectedProvider] = useState<ProviderDef>(
     PROVIDERS[0],
@@ -198,10 +196,7 @@ export default function CredentialsPage() {
       style={{ backgroundColor: colors.bg.secondary }}
     >
       <div className="flex flex-1 overflow-hidden">
-        <Sidebar
-          collapsed={sidebarCollapsed}
-          activeRoute="/settings/credentials"
-        />
+        <SettingsSidebar />
 
         <div className="flex-1 flex flex-col overflow-hidden">
           <div
@@ -211,41 +206,19 @@ export default function CredentialsPage() {
               borderColor: colors.border,
             }}
           >
-            <div className="flex items-center gap-3">
-              <button
-                onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
-                className="p-1.5 rounded-md"
-                style={{ color: colors.text.secondary }}
-                aria-label="Toggle sidebar"
+            <div>
+              <h1
+                className="text-base font-semibold"
+                style={{
+                  color: colors.text.primary,
+                  letterSpacing: "-0.01em",
+                }}
               >
-                <svg
-                  className="w-5 h-5"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                >
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    strokeWidth={2}
-                    d="M4 6h16M4 12h16M4 18h16"
-                  />
-                </svg>
-              </button>
-              <div>
-                <h1
-                  className="text-base font-semibold"
-                  style={{
-                    color: colors.text.primary,
-                    letterSpacing: "-0.01em",
-                  }}
-                >
-                  Settings
-                </h1>
-                <p className="text-xs" style={{ color: colors.text.secondary }}>
-                  Manage provider credentials
-                </p>
-              </div>
+                Credentials
+              </h1>
+              <p className="text-xs" style={{ color: colors.text.secondary }}>
+                Manage provider credentials
+              </p>
             </div>
           </div>
 
diff --git a/app/(main)/settings/onboarding/page.tsx b/app/(main)/settings/onboarding/page.tsx
index ea01344..3b7f22d 100644
--- a/app/(main)/settings/onboarding/page.tsx
+++ b/app/(main)/settings/onboarding/page.tsx
@@ -2,9 +2,8 @@
 
 import { useState, useEffect, useCallback } from "react";
 import { useRouter } from "next/navigation";
-import Sidebar from "@/app/components/Sidebar";
+import SettingsSidebar from "@/app/components/settings/SettingsSidebar";
 import PageHeader from "@/app/components/PageHeader";
-import { useApp } from "@/app/lib/context/AppContext";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { usePaginatedList, useInfiniteScroll } from "@/app/hooks";
 import {
@@ -13,6 +12,7 @@ import {
   OrganizationList,
   ProjectList,
   StepIndicator,
+  UserList,
 } from "@/app/components/settings/onboarding";
 import {
   Organization,
@@ -25,7 +25,7 @@ import { colors } from "@/app/lib/colors";
 import { ArrowLeftIcon } from "@/app/components/icons";
 import { DEFAULT_PAGE_LIMIT } from "@/app/lib/constants";
 
-type View = "loading" | "list" | "projects" | "form" | "success";
+type View = "loading" | "list" | "projects" | "users" | "form" | "success";
 
 function OrganizationListSkeleton() {
   return (
@@ -60,10 +60,10 @@ function OrganizationListSkeleton() {
 
 export default function OnboardingPage() {
   const router = useRouter();
-  const { sidebarCollapsed } = useApp();
   const { activeKey, currentUser, isHydrated, isAuthenticated } = useAuth();
   const [view, setView] = useState<View>("loading");
   const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
+  const [selectedProject, setSelectedProject] = useState<Project | null>(null);
   const [projects, setProjects] = useState<Project[]>([]);
   const [isLoadingProjects, setIsLoadingProjects] = useState(false);
   const [onboardData, setOnboardData] = useState<OnboardResponseData | null>(
@@ -139,22 +139,30 @@ export default function OnboardingPage() {
     setView("success");
   };
 
+  const handleSelectProject = (project: Project) => {
+    setSelectedProject(project);
+    setView("users");
+  };
+
   const handleBackToOrgs = () => {
     setSelectedOrg(null);
+    setSelectedProject(null);
     setProjects([]);
     setView("list");
   };
 
+  const handleBackToProjects = () => {
+    setSelectedProject(null);
+    setView("projects");
+  };
+
   return (
     <div
       className="w-full h-screen flex flex-col"
       style={{ backgroundColor: colors.bg.secondary }}
     >
       <div className="flex flex-1 overflow-hidden">
-        <Sidebar
-          collapsed={sidebarCollapsed}
-          activeRoute="/settings/onboarding"
-        />
+        <SettingsSidebar />
 
         <div className="flex-1 flex flex-col overflow-hidden">
           <PageHeader
@@ -182,6 +190,15 @@ export default function OnboardingPage() {
                   projects={projects}
                   isLoading={isLoadingProjects}
                   onBack={handleBackToOrgs}
+                  onSelectProject={handleSelectProject}
+                />
+              )}
+
+              {view === "users" && selectedOrg && selectedProject && (
+                <UserList
+                  organization={selectedOrg}
+                  project={selectedProject}
+                  onBack={handleBackToProjects}
                 />
               )}
 
diff --git a/app/api/user-projects/[userId]/route.ts b/app/api/user-projects/[userId]/route.ts
new file mode 100644
index 0000000..b793306
--- /dev/null
+++ b/app/api/user-projects/[userId]/route.ts
@@ -0,0 +1,25 @@
+import { NextRequest, NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
+
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: Promise<{ userId: string }> },
+) {
+  try {
+    const { userId } = await params;
+    const { searchParams } = new URL(request.url);
+    const queryString = searchParams.toString();
+
+    const { status, data } = await apiClient(
+      request,
+      `/api/v1/user-projects/${userId}${queryString ? `?${queryString}` : ""}`,
+      { method: "DELETE" },
+    );
+    return NextResponse.json(data, { status });
+  } catch {
+    return NextResponse.json(
+      { success: false, error: "Failed to connect to backend" },
+      { status: 500 },
+    );
+  }
+}
diff --git a/app/api/user-projects/route.ts b/app/api/user-projects/route.ts
new file mode 100644
index 0000000..cfff7e0
--- /dev/null
+++ b/app/api/user-projects/route.ts
@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
+
+export async function GET(request: NextRequest) {
+  try {
+    const { searchParams } = new URL(request.url);
+    const queryString = searchParams.toString();
+
+    const { status, data } = await apiClient(
+      request,
+      `/api/v1/user-projects/${queryString ? `?${queryString}` : ""}`,
+    );
+    return NextResponse.json(data, { status });
+  } catch {
+    return NextResponse.json(
+      { success: false, error: "Failed to connect to backend" },
+      { status: 500 },
+    );
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const body = await request.json();
+    const { status, data } = await apiClient(
+      request,
+      "/api/v1/user-projects/",
+      {
+        method: "POST",
+        body: JSON.stringify(body),
+      },
+    );
+    return NextResponse.json(data, { status });
+  } catch {
+    return NextResponse.json(
+      { success: false, error: "Failed to connect to backend" },
+      { status: 500 },
+    );
+  }
+}
diff --git a/app/components/GatePopover.tsx b/app/components/GatePopover.tsx
new file mode 100644
index 0000000..0ed1a0b
--- /dev/null
+++ b/app/components/GatePopover.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { createPortal } from "react-dom";
+import { Button } from "@/app/components";
+import { GatePopoverProps } from "@/app/lib/types/nav";
+
+export default function GatePopover({
+  name,
+  description,
+  anchorRect,
+  onMouseEnter,
+  onMouseLeave,
+  onLogin,
+}: GatePopoverProps) {
+  return createPortal(
+    <div
+      className="fixed z-9999 w-72"
+      style={{
+        top: anchorRect.top - 40,
+        left: anchorRect.right + 10,
+      }}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+    >
+      <div className="bg-white rounded-2xl shadow-2xl border border-border overflow-hidden">
+        {/* Gradient banner */}
+        <div
+          className="h-24"
+          style={{
+            background:
+              "linear-gradient(135deg, #dbeafe 0%, #e0e7ff 30%, #c7d2fe 50%, #ddd6fe 70%, #fce7f3 100%)",
+          }}
+        />
+
+        {/* Content */}
+        <div className="px-4 pt-3 pb-4">
+          <p className="text-[15px] font-semibold text-text-primary leading-snug">
+            {name}
+          </p>
+          <p className="text-[13px] text-text-secondary mt-1.5 leading-relaxed">
+            {description}
+          </p>
+
+          <div className="mt-4">
+            <Button size="sm" onClick={onLogin}>
+              Log in
+            </Button>
+          </div>
+        </div>
+      </div>
+    </div>,
+    document.body,
+  );
+}
diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx
index bb767e1..c2591dc 100644
--- a/app/components/Modal.tsx
+++ b/app/components/Modal.tsx
@@ -58,7 +58,7 @@ export default function Modal({
             {showClose && (
               <button
                 onClick={onClose}
-                className="p-1 rounded-md text-text-secondary transition-colors hover:bg-neutral-100 hover:text-text-primary"
+                className="p-1 rounded-md text-text-secondary transition-colors hover:bg-neutral-100 hover:text-text-primary cursor-pointer"
               >
                 <CloseIcon className="w-5 h-5" />
               </button>
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
index dcec97c..6d623ab 100644
--- a/app/components/Sidebar.tsx
+++ b/app/components/Sidebar.tsx
@@ -6,7 +6,6 @@
 "use client";
 
 import React, { useState, useEffect, useRef, useCallback } from "react";
-import { createPortal } from "react-dom";
 import { useRouter } from "next/navigation";
 import Image from "next/image";
 import { useAuth } from "@/app/lib/context/AuthContext";
@@ -15,109 +14,30 @@ import {
   DocumentFileIcon,
   BookOpenIcon,
   GearIcon,
-  SlidersIcon,
-  KeyIcon,
   ChevronRightIcon,
 } from "@/app/components/icons";
 import { LoginModal } from "@/app/components/auth";
-import { Button } from "@/app/components";
-
-interface SubMenuItem {
-  name: string;
-  route?: string;
-  comingSoon?: boolean;
-  submenu?: SubMenuItem[];
-}
-
-interface MenuItem {
-  name: string;
-  route?: string;
-  icon: React.ReactNode;
-  submenu?: SubMenuItem[];
-  gateDescription?: string;
-}
-
-interface SidebarProps {
-  collapsed: boolean;
-  activeRoute?: string;
-}
+import { Branding, UserMenuPopover } from "@/app/components/user-menu";
+import GatePopover from "@/app/components/GatePopover";
+import { NAV_ITEMS } from "@/app/lib/navConfig";
+import { MenuItem, SidebarProps } from "@/app/lib/types/nav";
 
 /** Routes that are always accessible without auth */
-const PUBLIC_ROUTES = new Set(["/evaluations", "/keystore"]);
-
-// ---- Gate Popover (rendered via portal) ----
-
-function GatePopover({
-  name,
-  description,
-  anchorRect,
-  onMouseEnter,
-  onMouseLeave,
-  onLogin,
-}: {
-  name: string;
-  description: string;
-  anchorRect: DOMRect;
-  onMouseEnter: () => void;
-  onMouseLeave: () => void;
-  onLogin: () => void;
-}) {
-  return createPortal(
-    <div
-      className="fixed z-9999 w-72"
-      style={{
-        top: anchorRect.top - 40,
-        left: anchorRect.right + 10,
-      }}
-      onMouseEnter={onMouseEnter}
-      onMouseLeave={onMouseLeave}
-    >
-      <div className="bg-white rounded-2xl shadow-2xl border border-border overflow-hidden">
-        {/* Gradient banner */}
-        <div
-          className="h-24"
-          style={{
-            background:
-              "linear-gradient(135deg, #dbeafe 0%, #e0e7ff 30%, #c7d2fe 50%, #ddd6fe 70%, #fce7f3 100%)",
-          }}
-        />
-
-        {/* Content */}
-        <div className="px-4 pt-3 pb-4">
-          <p className="text-[15px] font-semibold text-text-primary leading-snug">
-            {name}
-          </p>
-          <p className="text-[13px] text-text-secondary mt-1.5 leading-relaxed">
-            {description}
-          </p>
-
-          <div className="mt-4">
-            <Button size="sm" onClick={onLogin}>
-              Log in
-            </Button>
-          </div>
-        </div>
-      </div>
-    </div>,
-    document.body,
-  );
-}
-
-// ---- Main Sidebar ----
+const PUBLIC_ROUTES = new Set(["/evaluations"]);
 
 export default function Sidebar({
   collapsed,
   activeRoute = "/evaluations",
 }: SidebarProps) {
   const router = useRouter();
-  const { currentUser, googleProfile, isAuthenticated, session, logout } =
-    useAuth();
-  const isGoogleUser = !!session?.accessToken;
+  const { currentUser, googleProfile, isAuthenticated, logout } = useAuth();
   const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({
     Evaluations: true,
     Configurations: false,
   });
   const [showLoginModal, setShowLoginModal] = useState(false);
+  const [showUserMenu, setShowUserMenu] = useState(false);
+  const userMenuRef = useRef<HTMLDivElement>(null);
   const [hoveredGate, setHoveredGate] = useState<string | null>(null);
   const [gateRect, setGateRect] = useState<DOMRect | null>(null);
   const gateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -133,6 +53,21 @@ export default function Sidebar({
     }
   }, []);
 
+  // Close user menu on click outside
+  useEffect(() => {
+    if (!showUserMenu) return;
+    const handleClickOutside = (e: MouseEvent) => {
+      if (
+        userMenuRef.current &&
+        !userMenuRef.current.contains(e.target as Node)
+      ) {
+        setShowUserMenu(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => document.removeEventListener("mousedown", handleClickOutside);
+  }, [showUserMenu]);
+
   const toggleMenu = (menuName: string) => {
     const newState = { ...expandedMenus, [menuName]: !expandedMenus[menuName] };
     setExpandedMenus(newState);
@@ -171,54 +106,23 @@ export default function Sidebar({
     return false;
   };
 
-  const navItems: MenuItem[] = [
-    {
-      name: "Evaluations",
-      icon: <ClipboardIcon />,
-      submenu: [
-        { name: "Text", route: "/evaluations" },
-        { name: "Speech-to-Text", route: "/speech-to-text" },
-        { name: "Text-to-Speech", route: "/text-to-speech" },
-      ],
-      gateDescription:
-        "Log in to compare model response quality across different configs.",
-    },
-    {
-      name: "Documents",
-      route: "/document",
-      icon: <DocumentFileIcon />,
-      gateDescription: "Log in to upload and manage your documents.",
-    },
-    {
-      name: "Knowledge Base",
-      route: "/knowledge-base",
-      icon: <BookOpenIcon />,
-      gateDescription: "Log in to manage your knowledge bases for RAG.",
-    },
-    {
-      name: "Configurations",
-      icon: <GearIcon className="w-5 h-5" />,
-      submenu: [
-        { name: "Library", route: "/configurations" },
-        { name: "Prompt Editor", route: "/configurations/prompt-editor" },
-      ],
-      gateDescription: "Log in to manage prompts and model configurations.",
-    },
-    ...(currentUser?.is_superuser
-      ? [
-          {
-            name: "Settings",
-            icon: <SlidersIcon />,
-            submenu: [
-              { name: "Credentials", route: "/settings/credentials" },
-              { name: "Onboarding", route: "/settings/onboarding" },
-            ],
-          },
-        ]
-      : []),
-  ];
+  const iconMap: Record<string, React.ReactNode> = {
+    clipboard: <ClipboardIcon />,
+    document: <DocumentFileIcon />,
+    book: <BookOpenIcon />,
+    gear: <GearIcon className="w-5 h-5" />,
+  };
+
+  const navItems: MenuItem[] = NAV_ITEMS.filter(
+    (item) => !item.superuserOnly || currentUser?.is_superuser,
+  ).map((item) => ({
+    name: item.name,
+    route: item.route,
+    icon: iconMap[item.icon],
+    submenu: item.submenu,
+    gateDescription: item.gateDescription,
+  }));
 
-  // Find the gate description for the currently hovered item
   const getGateDescription = (name: string): string => {
     for (const item of navItems) {
       if (item.name === name)
@@ -239,12 +143,7 @@ export default function Sidebar({
     <aside
       className={`border-r border-border transition-all duration-300 ease-in-out h-full shrink-0 flex flex-col bg-bg-secondary ${collapsed ? "w-0 overflow-hidden" : "w-60"}`}
     >
-      <div className="px-5 py-[13px] border-b border-border">
-        <h2 className="text-sm font-semibold text-text-primary tracking-tight">
-          Kaapi Konsole
-        </h2>
-        <p className="text-xs mt-0.5 text-text-secondary">Tech4Dev</p>
-      </div>
+      <Branding />
 
       <nav className="p-3 space-y-1 flex-1 overflow-y-auto w-60">
         {navItems.map((item) => {
@@ -394,35 +293,25 @@ export default function Sidebar({
         })}
       </nav>
 
-      {/* Bottom section */}
       <div className="w-60">
-        {!isGoogleUser && (
-          <div className="px-3 py-3">
+        {isAuthenticated && (currentUser || googleProfile) ? (
+          <div
+            className="px-3 pb-3 pt-1 border-t border-border relative"
+            ref={userMenuRef}
+          >
+            {showUserMenu && (
+              <UserMenuPopover
+                currentUser={currentUser}
+                googleProfile={googleProfile}
+                onClose={() => setShowUserMenu(false)}
+                onLogout={logout}
+              />
+            )}
+
             <button
-              onClick={() => router.push("/keystore")}
-              className={`w-full text-left px-3 py-2 rounded-lg text-[14px] flex items-center gap-2.5 transition-all duration-150 border ${
-                activeRoute === "/keystore"
-                  ? "bg-neutral-100 text-text-primary font-semibold border-border"
-                  : "bg-transparent text-black font-medium border-transparent hover:bg-neutral-100"
-              }`}
+              onClick={() => setShowUserMenu((prev) => !prev)}
+              className="w-full flex items-center gap-3 px-2 py-2 rounded-lg cursor-pointer hover:bg-neutral-100 transition-colors"
             >
-              <span
-                className={
-                  activeRoute === "/keystore" ? "opacity-100" : "opacity-70"
-                }
-              >
-                <KeyIcon className="w-5 h-5" />
-              </span>
-              Keystore
-            </button>
-          </div>
-        )}
-        {/* Keystore */}
-
-        {/* User profile (authenticated) or login prompt */}
-        {isAuthenticated && (currentUser || googleProfile) ? (
-          <div className="px-3 pb-3 pt-1 border-t border-border">
-            <div className="flex items-center gap-3 px-2 py-2">
               {googleProfile?.picture ? (
                 <Image
                   src={googleProfile.picture}
@@ -441,7 +330,7 @@ export default function Sidebar({
                   </span>
                 </div>
               )}
-              <div className="flex-1 min-w-0">
+              <div className="flex-1 min-w-0 text-left">
                 <p className="text-sm font-medium text-text-primary truncate">
                   {googleProfile?.name ||
                     currentUser?.full_name ||
@@ -451,26 +340,7 @@ export default function Sidebar({
                   {currentUser?.email}
                 </p>
               </div>
-              <button
-                onClick={logout}
-                className="p-1.5 rounded-md text-text-secondary hover:bg-neutral-100 hover:text-text-primary transition-colors shrink-0"
-                title="Log out"
-              >
-                <svg
-                  className="w-4 h-4"
-                  fill="none"
-                  viewBox="0 0 24 24"
-                  stroke="currentColor"
-                  strokeWidth={2}
-                >
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
-                  />
-                </svg>
-              </button>
-            </div>
+            </button>
           </div>
         ) : !isAuthenticated ? (
           <div className="px-4 py-4 w-60 border-t border-border">
@@ -492,7 +362,6 @@ export default function Sidebar({
         ) : null}
       </div>
 
-      {/* Gate popover — rendered via portal so it's never clipped */}
       {hoveredGate && gateRect && (
         <GatePopover
           name={hoveredGate}
diff --git a/app/components/auth/LoginModal.tsx b/app/components/auth/LoginModal.tsx
index d285f88..84b9131 100644
--- a/app/components/auth/LoginModal.tsx
+++ b/app/components/auth/LoginModal.tsx
@@ -1,9 +1,9 @@
 "use client";
 
 import { useState } from "react";
+import { useRouter } from "next/navigation";
 import { GoogleLogin, CredentialResponse } from "@react-oauth/google";
 import Modal from "@/app/components/Modal";
-import { Button, Field } from "@/app/components";
 import { useAuth, User, GoogleProfile } from "@/app/lib/context/AuthContext";
 import { useToast } from "@/app/components/Toast";
 import { apiFetch } from "@/app/lib/apiClient";
@@ -14,9 +14,9 @@ interface LoginModalProps {
 }
 
 export default function LoginModal({ open, onClose }: LoginModalProps) {
-  const { addKey, loginWithGoogle } = useAuth();
+  const router = useRouter();
+  const { loginWithGoogle } = useAuth();
   const toast = useToast();
-  const [apiKey, setApiKey] = useState("");
   const [isLoggingIn, setIsLoggingIn] = useState(false);
 
   const handleGoogleSuccess = async (
@@ -54,27 +54,10 @@ export default function LoginModal({ open, onClose }: LoginModalProps) {
     }
   };
 
-  const handleApiKeySubmit = () => {
-    if (!apiKey.trim()) {
-      toast.error("Please enter an API key.");
-      return;
-    }
-    addKey({
-      id: crypto.randomUUID(),
-      label: "API Key",
-      key: apiKey.trim(),
-      provider: "default",
-      createdAt: new Date().toISOString(),
-    });
-    toast.success("API key added successfully!");
-    setApiKey("");
-    onClose();
-  };
-
   return (
     <Modal open={open} onClose={onClose} maxWidth="max-w-md">
       <div className="px-6 pb-6 text-center">
-        <h2 className="text-2xl font-semibold text-text-primary">
+        <h2 className="text-2xl font-medium text-text-primary">
           Log in or connect
         </h2>
         <p className="text-base text-text-secondary mt-2">
@@ -83,8 +66,7 @@ export default function LoginModal({ open, onClose }: LoginModalProps) {
         </p>
       </div>
 
-      <div className="px-6 pb-8 space-y-3">
-        {/* Google Login */}
+      <div className="px-6 pb-8 space-y-4">
         <div className="flex justify-center">
           {isLoggingIn ? (
             <div className="w-full flex items-center justify-center py-3.5 rounded-full border border-border text-sm text-text-secondary">
@@ -92,37 +74,30 @@ export default function LoginModal({ open, onClose }: LoginModalProps) {
             </div>
           ) : (
             <GoogleLogin
+              type="standard"
               onSuccess={handleGoogleSuccess}
               onError={() => toast.error("Google login failed.")}
               width="400"
               shape="pill"
               text="continue_with"
               size="large"
+              logo_alignment="center"
             />
           )}
         </div>
 
-        {/* Divider */}
-        <div className="flex items-center gap-3 py-1">
-          <div className="flex-1 h-px bg-border" />
-          <span className="text-xs text-text-secondary font-medium">OR</span>
-          <div className="flex-1 h-px bg-border" />
-        </div>
-
-        {/* API Key */}
-        <div>
-          <Field
-            label=""
-            value={apiKey}
-            onChange={setApiKey}
-            placeholder="Enter your X-API-KEY"
-            type="password"
-          />
-        </div>
-
-        <Button fullWidth size="lg" onClick={handleApiKeySubmit}>
-          Continue
-        </Button>
+        <p className="text-center text-[13px] text-text-secondary">
+          Want to use an X-API key instead?{" "}
+          <button
+            onClick={() => {
+              onClose();
+              router.push("/keystore");
+            }}
+            className="text-accent-primary hover:underline font-medium cursor-pointer"
+          >
+            Go to Keystore
+          </button>
+        </p>
       </div>
     </Modal>
   );
diff --git a/app/components/icons/index.tsx b/app/components/icons/index.tsx
index 086c50e..b8d3dfe 100644
--- a/app/components/icons/index.tsx
+++ b/app/components/icons/index.tsx
@@ -32,6 +32,7 @@ export { default as CloudUploadIcon } from "./document/CloudUploadIcon";
 export { default as ClipboardIcon } from "./sidebar/ClipboardIcon";
 export { default as BookOpenIcon } from "./sidebar/BookOpenIcon";
 export { default as SlidersIcon } from "./sidebar/SlidersIcon";
+export { default as LogoutIcon } from "./sidebar/LogoutIcon";
 
 // Prompt Editor Icons
 export { default as ChevronRightIcon } from "./prompt-editor/ChevronRightIcon";
diff --git a/app/components/icons/sidebar/LogoutIcon.tsx b/app/components/icons/sidebar/LogoutIcon.tsx
new file mode 100644
index 0000000..3a053d6
--- /dev/null
+++ b/app/components/icons/sidebar/LogoutIcon.tsx
@@ -0,0 +1,21 @@
+export default function LogoutIcon({
+  className = "w-5 h-5",
+}: {
+  className?: string;
+}) {
+  return (
+    <svg
+      className={className}
+      fill="none"
+      viewBox="0 0 24 24"
+      stroke="currentColor"
+      strokeWidth={1.5}
+    >
+      <path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
+      />
+    </svg>
+  );
+}
diff --git a/app/Providers.tsx b/app/components/providers/Providers.tsx
similarity index 100%
rename from app/Providers.tsx
rename to app/components/providers/Providers.tsx
diff --git a/app/components/providers/index.ts b/app/components/providers/index.ts
new file mode 100644
index 0000000..1220b47
--- /dev/null
+++ b/app/components/providers/index.ts
@@ -0,0 +1 @@
+export { default as Providers } from "./Providers";
diff --git a/app/components/settings/SettingsSidebar.tsx b/app/components/settings/SettingsSidebar.tsx
new file mode 100644
index 0000000..e2cc889
--- /dev/null
+++ b/app/components/settings/SettingsSidebar.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+import { useRouter, usePathname } from "next/navigation";
+import Image from "next/image";
+import { ArrowLeftIcon, KeyIcon, SlidersIcon } from "@/app/components/icons";
+import { SETTINGS_NAV } from "@/app/lib/navConfig";
+import { useAuth } from "@/app/lib/context/AuthContext";
+import { Branding, UserMenuPopover } from "@/app/components/user-menu";
+
+const iconMap: Record<string, React.ReactNode> = {
+  key: <KeyIcon className="w-[18px] h-[18px]" />,
+  sliders: <SlidersIcon className="w-[18px] h-[18px]" />,
+};
+
+export default function SettingsSidebar() {
+  const router = useRouter();
+  const pathname = usePathname();
+  const { currentUser, googleProfile, isAuthenticated, logout } = useAuth();
+  const [showUserMenu, setShowUserMenu] = useState(false);
+  const userMenuRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (!showUserMenu) return;
+    const handleClickOutside = (e: MouseEvent) => {
+      if (
+        userMenuRef.current &&
+        !userMenuRef.current.contains(e.target as Node)
+      ) {
+        setShowUserMenu(false);
+      }
+    };
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => document.removeEventListener("mousedown", handleClickOutside);
+  }, [showUserMenu]);
+
+  const displayName =
+    googleProfile?.name || currentUser?.full_name || currentUser?.email || "";
+  const initials = (currentUser?.full_name || currentUser?.email || "U")
+    .charAt(0)
+    .toUpperCase();
+
+  return (
+    <aside className="w-60 h-full border-r border-border bg-bg-secondary flex flex-col shrink-0">
+      <Branding />
+
+      <div className="px-3 pt-3 pb-1">
+        <button
+          onClick={() => router.push("/evaluations")}
+          className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-[14px] font-medium text-text-secondary hover:text-text-primary hover:bg-neutral-100 transition-all duration-150 w-full"
+        >
+          <ArrowLeftIcon className="w-4 h-4" />
+          Dashboard
+        </button>
+      </div>
+
+      <div className="mx-4 my-2 h-px bg-border" />
+
+      <nav className="flex-1 overflow-y-auto px-3 pt-1 pb-4">
+        {SETTINGS_NAV.map((section) => (
+          <div key={section.label} className="mb-5">
+            <p className="px-3 mb-1.5 text-[11px] font-medium text-text-secondary uppercase tracking-wider">
+              {section.label}
+            </p>
+            <div className="space-y-0.5">
+              {section.items.map((item) => {
+                const isActive = pathname === item.route;
+                return (
+                  <button
+                    key={item.route}
+                    onClick={() => router.push(item.route)}
+                    className={`w-full text-left px-3 py-2 rounded-lg text-[14px] flex items-center gap-2.5 transition-all duration-150 border ${
+                      isActive
+                        ? "bg-neutral-100 text-text-primary font-semibold border-border"
+                        : "bg-transparent text-black font-medium border-transparent hover:bg-neutral-100"
+                    }`}
+                  >
+                    {item.icon && (
+                      <span className={isActive ? "opacity-100" : "opacity-70"}>
+                        {iconMap[item.icon]}
+                      </span>
+                    )}
+                    {item.name}
+                  </button>
+                );
+              })}
+            </div>
+          </div>
+        ))}
+      </nav>
+
+      {isAuthenticated && (currentUser || googleProfile) && (
+        <div
+          className="px-3 pb-3 pt-1 border-t border-border relative"
+          ref={userMenuRef}
+        >
+          {showUserMenu && (
+            <UserMenuPopover
+              currentUser={currentUser}
+              googleProfile={googleProfile}
+              onClose={() => setShowUserMenu(false)}
+              onLogout={logout}
+            />
+          )}
+
+          <button
+            onClick={() => setShowUserMenu((prev) => !prev)}
+            className="w-full flex items-center gap-3 px-2 py-2 rounded-lg cursor-pointer hover:bg-neutral-100 transition-colors"
+          >
+            {googleProfile?.picture ? (
+              <Image
+                src={googleProfile.picture}
+                alt={googleProfile.name}
+                width={32}
+                height={32}
+                className="rounded-full shrink-0"
+                referrerPolicy="no-referrer"
+              />
+            ) : (
+              <div className="w-8 h-8 rounded-full bg-green-600 flex items-center justify-center shrink-0">
+                <span className="text-xs font-semibold text-white">
+                  {initials}
+                </span>
+              </div>
+            )}
+            <div className="flex-1 min-w-0 text-left">
+              <p className="text-sm font-medium text-text-primary truncate">
+                {displayName}
+              </p>
+              <p className="text-[11px] text-text-secondary truncate">
+                {currentUser?.email}
+              </p>
+            </div>
+          </button>
+        </div>
+      )}
+    </aside>
+  );
+}
diff --git a/app/components/settings/credentials/CredentialForm.tsx b/app/components/settings/credentials/CredentialForm.tsx
index f9417a6..39a5d74 100644
--- a/app/components/settings/credentials/CredentialForm.tsx
+++ b/app/components/settings/credentials/CredentialForm.tsx
@@ -227,7 +227,7 @@ export default function CredentialForm({
               <button
                 onClick={onDelete}
                 disabled={isSaving || isDeleting}
-                className={`ml-auto px-4 py-2 rounded-full text-sm font-medium border transition-colors bg-transparent border-[#ef444440] text-[#ef4444] hover:bg-[#ef444415] hover:border-[#ef4444] ${isDeleting ? "opacity-70" : ""}`}
+                className={`ml-auto px-4 py-2 rounded-full text-sm font-medium border transition-colors bg-transparent border-[#ef444440] text-[#ef4444] hover:bg-[#ef444415] hover:border-[#ef4444] disabled:cursor-not-allowed ${isDeleting ? "opacity-70" : ""}`}
               >
                 {isDeleting ? "Removing…" : "Remove"}
               </button>
diff --git a/app/components/settings/onboarding/ProjectList.tsx b/app/components/settings/onboarding/ProjectList.tsx
index 25e5227..be368fc 100644
--- a/app/components/settings/onboarding/ProjectList.tsx
+++ b/app/components/settings/onboarding/ProjectList.tsx
@@ -1,12 +1,13 @@
 import { Organization, Project } from "@/app/lib/types/onboarding";
 import { formatRelativeTime } from "@/app/lib/utils";
-import { ArrowLeftIcon } from "@/app/components/icons";
+import { ArrowLeftIcon, ChevronRightIcon } from "@/app/components/icons";
 
 interface ProjectListProps {
   organization: Organization;
   projects: Project[];
   isLoading: boolean;
   onBack: () => void;
+  onSelectProject: (project: Project) => void;
 }
 
 function ProjectListSkeleton() {
@@ -36,6 +37,7 @@ export default function ProjectList({
   projects,
   isLoading,
   onBack,
+  onSelectProject,
 }: ProjectListProps) {
   return (
     <div>
@@ -68,9 +70,10 @@ export default function ProjectList({
       ) : (
         <div className="space-y-2">
           {projects.map((project) => (
-            <div
+            <button
               key={project.id}
-              className="flex items-center justify-between p-4 rounded-lg border border-border bg-white"
+              onClick={() => onSelectProject(project)}
+              className="w-full flex items-center justify-between p-4 rounded-lg border border-border bg-white text-left transition-colors hover:bg-neutral-50"
             >
               <div>
                 <p className="text-sm font-medium text-text-primary">
@@ -85,16 +88,19 @@ export default function ProjectList({
                   Created {formatRelativeTime(project.inserted_at)}
                 </p>
               </div>
-              <span
-                className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${
-                  project.is_active
-                    ? "bg-green-50 text-green-700 border border-green-200"
-                    : "bg-neutral-100 text-text-secondary border border-border"
-                }`}
-              >
-                {project.is_active ? "Active" : "Inactive"}
-              </span>
-            </div>
+              <div className="flex items-center gap-2">
+                <span
+                  className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${
+                    project.is_active
+                      ? "bg-green-50 text-green-700 border border-green-200"
+                      : "bg-neutral-100 text-text-secondary border border-border"
+                  }`}
+                >
+                  {project.is_active ? "Active" : "Inactive"}
+                </span>
+                <ChevronRightIcon className="w-4 h-4 text-text-secondary" />
+              </div>
+            </button>
           ))}
         </div>
       )}
diff --git a/app/components/settings/onboarding/UserList.tsx b/app/components/settings/onboarding/UserList.tsx
new file mode 100644
index 0000000..5309bd7
--- /dev/null
+++ b/app/components/settings/onboarding/UserList.tsx
@@ -0,0 +1,308 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import {
+  Organization,
+  Project,
+  UserProject,
+  UserProjectListResponse,
+  UserProjectDeleteResponse,
+} from "@/app/lib/types/onboarding";
+import { formatRelativeTime, isValidEmail } from "@/app/lib/utils";
+import { ArrowLeftIcon } from "@/app/components/icons";
+import { Button, Field } from "@/app/components";
+import { useAuth } from "@/app/lib/context/AuthContext";
+import { apiFetch } from "@/app/lib/apiClient";
+import { useToast } from "@/app/components/Toast";
+
+interface UserListProps {
+  organization: Organization;
+  project: Project;
+  onBack: () => void;
+}
+
+function UserListSkeleton() {
+  return (
+    <div className="space-y-2 animate-pulse">
+      {[1, 2, 3].map((i) => (
+        <div
+          key={i}
+          className="flex items-center justify-between p-4 rounded-lg border border-border bg-white"
+        >
+          <div className="flex-1">
+            <div className="h-4 w-40 bg-neutral-200 rounded mb-2" />
+            <div className="h-3 w-56 bg-neutral-100 rounded" />
+          </div>
+          <div className="h-8 w-16 bg-neutral-100 rounded" />
+        </div>
+      ))}
+    </div>
+  );
+}
+
+export default function UserList({
+  organization,
+  project,
+  onBack,
+}: UserListProps) {
+  const toast = useToast();
+  const { activeKey, currentUser } = useAuth();
+  const apiKey = activeKey?.key ?? "";
+
+  const [users, setUsers] = useState<UserProject[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [userRows, setUserRows] = useState([{ email: "", full_name: "" }]);
+  const [rowErrors, setRowErrors] = useState<Record<number, string>>({});
+  const [isAdding, setIsAdding] = useState(false);
+  const [removingId, setRemovingId] = useState<number | null>(null);
+
+  const fetchUsers = useCallback(async () => {
+    setIsLoading(true);
+    try {
+      const data = await apiFetch<UserProjectListResponse>(
+        `/api/user-projects?project_id=${project.id}`,
+        apiKey,
+      );
+      const list = data.data || [];
+      setUsers(list);
+    } catch (err) {
+      console.error("Failed to fetch users:", err);
+    } finally {
+      setIsLoading(false);
+    }
+  }, [apiKey, project.id]);
+
+  useEffect(() => {
+    fetchUsers();
+  }, [fetchUsers]);
+
+  const updateRow = (
+    index: number,
+    field: "email" | "full_name",
+    value: string,
+  ) => {
+    setUserRows((prev) =>
+      prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)),
+    );
+    if (rowErrors[index]) {
+      setRowErrors((prev) => {
+        const next = { ...prev };
+        delete next[index];
+        return next;
+      });
+    }
+  };
+
+  const addRow = () => {
+    setUserRows((prev) => [...prev, { email: "", full_name: "" }]);
+  };
+
+  const removeRow = (index: number) => {
+    if (userRows.length === 1) return;
+    setUserRows((prev) => prev.filter((_, i) => i !== index));
+    setRowErrors((prev) => {
+      const next: Record<number, string> = {};
+      Object.entries(prev).forEach(([k, v]) => {
+        const key = Number(k);
+        if (key < index) next[key] = v;
+        else if (key > index) next[key - 1] = v;
+      });
+      return next;
+    });
+  };
+
+  const handleAddUsers = async () => {
+    const filledRows = userRows.filter((r) => r.email.trim());
+
+    if (filledRows.length === 0) {
+      setRowErrors({ 0: "Please enter an email address" });
+      return;
+    }
+
+    const errors: Record<number, string> = {};
+    userRows.forEach((row, i) => {
+      if (row.email.trim() && !isValidEmail(row.email.trim())) {
+        errors[i] = "Invalid email address";
+      }
+    });
+
+    if (Object.keys(errors).length > 0) {
+      setRowErrors(errors);
+      return;
+    }
+
+    setRowErrors({});
+    setIsAdding(true);
+
+    try {
+      const payload = filledRows.map((r) => {
+        const entry: { email: string; full_name?: string } = {
+          email: r.email.trim(),
+        };
+        if (r.full_name.trim()) entry.full_name = r.full_name.trim();
+        return entry;
+      });
+
+      await apiFetch<UserProjectListResponse>("/api/user-projects", apiKey, {
+        method: "POST",
+        body: JSON.stringify({
+          organization_id: organization.id,
+          project_id: project.id,
+          users: payload,
+        }),
+      });
+      toast.success(
+        `${filledRows.length} user${filledRows.length > 1 ? "s" : ""} added successfully`,
+      );
+      setUserRows([{ email: "", full_name: "" }]);
+      await fetchUsers();
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to add users");
+    } finally {
+      setIsAdding(false);
+    }
+  };
+
+  const handleRemoveUser = async (userId: number) => {
+    setRemovingId(userId);
+    try {
+      await apiFetch<UserProjectDeleteResponse>(
+        `/api/user-projects/${userId}?project_id=${project.id}`,
+        apiKey,
+        {
+          method: "DELETE",
+        },
+      );
+      toast.success("User removed");
+      setUsers((prev) => prev.filter((u) => u.user_id !== userId));
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to remove user");
+    } finally {
+      setRemovingId(null);
+    }
+  };
+
+  return (
+    <div>
+      <Button variant="ghost" size="sm" className="px-0!" onClick={onBack}>
+        <ArrowLeftIcon className="w-3.5 h-3.5" /> Back to projects
+      </Button>
+
+      <div className="mb-4">
+        <h2 className="text-lg font-semibold text-text-primary">
+          {project.name}
+        </h2>
+        <p className="text-xs text-text-secondary mt-0.5">
+          {organization.name} &middot;{" "}
+          {isLoading
+            ? "Loading users..."
+            : `${users.length} user${users.length !== 1 ? "s" : ""}`}
+        </p>
+      </div>
+
+      {currentUser?.is_superuser && (
+        <div className="mb-6 p-4 rounded-lg border border-border bg-white">
+          <p className="text-sm font-medium text-text-primary mb-3">
+            Add Users
+          </p>
+          <div className="space-y-3">
+            {userRows.map((row, index) => (
+              <div key={index} className="flex gap-2 items-start">
+                <div className="flex-1">
+                  <Field
+                    label=""
+                    value={row.email}
+                    onChange={(val) => updateRow(index, "email", val)}
+                    placeholder="Email address"
+                    error={rowErrors[index]}
+                  />
+                </div>
+                <div className="flex-1">
+                  <Field
+                    label=""
+                    value={row.full_name}
+                    onChange={(val) => updateRow(index, "full_name", val)}
+                    placeholder="Full name (optional)"
+                  />
+                </div>
+                {userRows.length > 1 && (
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    onClick={() => removeRow(index)}
+                  >
+                    &times;
+                  </Button>
+                )}
+              </div>
+            ))}
+          </div>
+          <div className="flex items-center justify-between mt-3">
+            <Button variant="ghost" size="sm" onClick={addRow}>
+              + Add another
+            </Button>
+            <Button
+              onClick={handleAddUsers}
+              disabled={isAdding || userRows.every((r) => !r.email.trim())}
+              size="md"
+            >
+              {isAdding
+                ? "Adding..."
+                : `Add User${userRows.filter((r) => r.email.trim()).length > 1 ? "s" : ""}`}
+            </Button>
+          </div>
+        </div>
+      )}
+
+      {isLoading ? (
+        <UserListSkeleton />
+      ) : users.length === 0 ? (
+        <div className="text-center py-12 text-text-secondary text-sm">
+          No users found for this project.
+        </div>
+      ) : (
+        <div className="space-y-2">
+          {users.map((user) => (
+            <div
+              key={user.user_id}
+              className="flex items-center justify-between p-4 rounded-lg border border-border bg-white"
+            >
+              <div>
+                <p className="text-sm font-medium text-text-primary">
+                  {user.full_name}
+                </p>
+                <p className="text-xs text-text-secondary mt-0.5">
+                  {user.email}
+                </p>
+                <p className="text-xs text-text-secondary mt-0.5">
+                  Added {formatRelativeTime(user.inserted_at)}
+                </p>
+              </div>
+              <div className="flex items-center gap-3">
+                <span
+                  className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${
+                    user.is_active
+                      ? "bg-green-50 text-green-700 border border-green-200"
+                      : "bg-neutral-100 text-text-secondary border border-border"
+                  }`}
+                >
+                  {user.is_active ? "Active" : "Inactive"}
+                </span>
+                {currentUser?.is_superuser && (
+                  <Button
+                    variant="danger"
+                    size="sm"
+                    onClick={() => handleRemoveUser(user.user_id)}
+                    disabled={removingId === user.user_id}
+                  >
+                    {removingId === user.user_id ? "Removing..." : "Remove"}
+                  </Button>
+                )}
+              </div>
+            </div>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/app/components/settings/onboarding/index.ts b/app/components/settings/onboarding/index.ts
index ad930bb..d0e766d 100644
--- a/app/components/settings/onboarding/index.ts
+++ b/app/components/settings/onboarding/index.ts
@@ -3,3 +3,4 @@ export { default as OnboardingSuccess } from "./OnboardingSuccess";
 export { default as OrganizationList } from "./OrganizationList";
 export { default as ProjectList } from "./ProjectList";
 export { default as StepIndicator } from "./StepIndicator";
+export { default as UserList } from "./UserList";
diff --git a/app/components/user-menu/Branding.tsx b/app/components/user-menu/Branding.tsx
new file mode 100644
index 0000000..19353d4
--- /dev/null
+++ b/app/components/user-menu/Branding.tsx
@@ -0,0 +1,12 @@
+const Branding = () => {
+  return (
+    <div className="px-5 py-[13px] border-b border-border">
+      <h2 className="text-sm font-semibold text-text-primary tracking-tight">
+        Kaapi Konsole
+      </h2>
+      <p className="text-xs mt-0.5 text-text-secondary">Tech4Dev</p>
+    </div>
+  );
+};
+
+export default Branding;
diff --git a/app/components/user-menu/UserMenuPopover.tsx b/app/components/user-menu/UserMenuPopover.tsx
new file mode 100644
index 0000000..cc13868
--- /dev/null
+++ b/app/components/user-menu/UserMenuPopover.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { GearIcon, LogoutIcon } from "@/app/components/icons";
+import { User, GoogleProfile } from "@/app/lib/types/auth";
+
+interface UserMenuPopoverProps {
+  currentUser: User | null;
+  googleProfile: GoogleProfile | null;
+  onClose: () => void;
+  onLogout: () => void;
+}
+
+export default function UserMenuPopover({
+  currentUser,
+  googleProfile,
+  onClose,
+  onLogout,
+}: UserMenuPopoverProps) {
+  const router = useRouter();
+
+  const displayName =
+    googleProfile?.name || currentUser?.full_name || currentUser?.email || "";
+  const initials = (currentUser?.full_name || currentUser?.email || "U")
+    .charAt(0)
+    .toUpperCase();
+
+  return (
+    <div className="absolute bottom-full left-3 right-3 mb-2 bg-white rounded-xl border border-border shadow-lg py-2 z-50">
+      <div className="px-4 py-3 flex items-center gap-3">
+        {googleProfile?.picture ? (
+          <Image
+            src={googleProfile.picture}
+            alt={googleProfile.name}
+            width={36}
+            height={36}
+            className="rounded-full shrink-0"
+            referrerPolicy="no-referrer"
+          />
+        ) : (
+          <div className="w-9 h-9 rounded-full bg-green-600 flex items-center justify-center shrink-0">
+            <span className="text-sm font-semibold text-white">{initials}</span>
+          </div>
+        )}
+        <div className="min-w-0">
+          <p className="text-sm font-semibold text-text-primary truncate">
+            {displayName}
+          </p>
+          <p className="text-xs text-text-secondary truncate">
+            {currentUser?.email}
+          </p>
+        </div>
+      </div>
+
+      <div className="h-px bg-border mx-2" />
+
+      {currentUser?.is_superuser && (
+        <button
+          onClick={() => {
+            onClose();
+            router.push("/settings/onboarding");
+          }}
+          className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-primary hover:bg-neutral-50 transition-colors cursor-pointer"
+        >
+          <GearIcon className="w-5 h-5 text-text-secondary" />
+          Settings
+        </button>
+      )}
+
+      <button
+        onClick={() => {
+          onClose();
+          onLogout();
+        }}
+        className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-primary hover:bg-neutral-50 transition-colors cursor-pointer"
+      >
+        <LogoutIcon className="w-5 h-5 text-text-secondary" />
+        Log out
+      </button>
+    </div>
+  );
+}
diff --git a/app/components/user-menu/index.ts b/app/components/user-menu/index.ts
new file mode 100644
index 0000000..611d249
--- /dev/null
+++ b/app/components/user-menu/index.ts
@@ -0,0 +1,2 @@
+export { default as UserMenuPopover } from "./UserMenuPopover";
+export { default as Branding } from "./Branding";
diff --git a/app/layout.tsx b/app/layout.tsx
index 00a016c..dd6d587 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,7 +1,7 @@
 import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
-import Providers from "./Providers";
+import { Providers } from "@/app/components/providers";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
diff --git a/app/lib/navConfig.ts b/app/lib/navConfig.ts
new file mode 100644
index 0000000..57e999a
--- /dev/null
+++ b/app/lib/navConfig.ts
@@ -0,0 +1,46 @@
+import { NavItemConfig, SettingsNavSection } from "@/app/lib/types/nav";
+
+export const SETTINGS_NAV: SettingsNavSection[] = [
+  {
+    label: "Settings",
+    items: [
+      { name: "Credentials", route: "/settings/credentials", icon: "key" },
+      { name: "Onboarding", route: "/settings/onboarding", icon: "sliders" },
+    ],
+  },
+];
+
+export const NAV_ITEMS: NavItemConfig[] = [
+  {
+    name: "Evaluations",
+    icon: "clipboard",
+    submenu: [
+      { name: "Text", route: "/evaluations" },
+      { name: "Speech-to-Text", route: "/speech-to-text" },
+      { name: "Text-to-Speech", route: "/text-to-speech" },
+    ],
+    gateDescription:
+      "Log in to compare model response quality across different configs.",
+  },
+  {
+    name: "Documents",
+    route: "/document",
+    icon: "document",
+    gateDescription: "Log in to upload and manage your documents.",
+  },
+  {
+    name: "Knowledge Base",
+    route: "/knowledge-base",
+    icon: "book",
+    gateDescription: "Log in to manage your knowledge bases for RAG.",
+  },
+  {
+    name: "Configurations",
+    icon: "gear",
+    submenu: [
+      { name: "Library", route: "/configurations" },
+      { name: "Prompt Editor", route: "/configurations/prompt-editor" },
+    ],
+    gateDescription: "Log in to manage prompts and model configurations.",
+  },
+];
diff --git a/app/lib/types/nav.ts b/app/lib/types/nav.ts
new file mode 100644
index 0000000..e17d3ec
--- /dev/null
+++ b/app/lib/types/nav.ts
@@ -0,0 +1,53 @@
+import React from "react";
+
+export interface NavSubItem {
+  name: string;
+  route: string;
+}
+
+export interface NavItemConfig {
+  name: string;
+  icon: string;
+  route?: string;
+  submenu?: NavSubItem[];
+  gateDescription?: string;
+  superuserOnly?: boolean;
+}
+
+export interface SubMenuItem {
+  name: string;
+  route?: string;
+  comingSoon?: boolean;
+  submenu?: SubMenuItem[];
+}
+
+export interface MenuItem {
+  name: string;
+  route?: string;
+  icon: React.ReactNode;
+  submenu?: SubMenuItem[];
+  gateDescription?: string;
+}
+
+export interface SidebarProps {
+  collapsed: boolean;
+  activeRoute?: string;
+}
+
+export interface GatePopoverProps {
+  name: string;
+  description: string;
+  anchorRect: DOMRect;
+  onMouseEnter: () => void;
+  onMouseLeave: () => void;
+  onLogin: () => void;
+}
+
+export interface SettingsNavItem extends NavSubItem {
+  icon?: string;
+}
+
+export interface SettingsNavSection {
+  label: string;
+  items: SettingsNavItem[];
+}
diff --git a/app/lib/types/onboarding.ts b/app/lib/types/onboarding.ts
index b5cd49e..c6babde 100644
--- a/app/lib/types/onboarding.ts
+++ b/app/lib/types/onboarding.ts
@@ -32,6 +32,30 @@ export interface ProjectListResponse {
   metadata?: Record<string, unknown>;
 }
 
+export interface UserProject {
+  user_id: number;
+  email: string;
+  full_name: string;
+  is_active: boolean;
+  inserted_at: string;
+}
+
+export interface UserProjectListResponse {
+  success: boolean;
+  data?: UserProject[];
+  error?: string;
+  errors?: { field: string; message: string }[];
+  metadata?: Record<string, unknown>;
+}
+
+export interface UserProjectDeleteResponse {
+  success: boolean;
+  data?: { message: string };
+  error?: string;
+  errors?: { field: string; message: string }[];
+  metadata?: Record<string, unknown>;
+}
+
 export interface OnboardRequest {
   organization_name: string;
   project_name: string;

From a236f2d6ffa65fea0b7098bcd909912f95593f37 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Wed, 1 Apr 2026 21:55:05 +0530
Subject: [PATCH 05/10] fix(*): small updates

---
 app/api/auth/google/route.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts
index c65bfd6..5da816d 100644
--- a/app/api/auth/google/route.ts
+++ b/app/api/auth/google/route.ts
@@ -32,7 +32,6 @@ export async function POST(request: Request) {
 
     const response = NextResponse.json(data, { status });
 
-    // Forward Set-Cookie from backend so the browser stores the auth cookie
     const setCookie = backendHeaders.getSetCookie?.();
     if (setCookie) {
       setCookie.forEach((cookie: string) => {

From 7e45d415f949b55875bb3643298a0f0885672f26 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:13:43 +0530
Subject: [PATCH 06/10] fix(*): some cases handling & UI updates

---
 app/components/Button.tsx                     |   2 +-
 app/components/auth/LoginModal.tsx            |   2 -
 .../settings/onboarding/AddUserModal.tsx      | 199 +++++++++++++++++
 .../settings/onboarding/UserList.tsx          | 207 ++++--------------
 app/components/settings/onboarding/index.ts   |   1 +
 app/lib/apiClient.ts                          |  16 ++
 6 files changed, 259 insertions(+), 168 deletions(-)
 create mode 100644 app/components/settings/onboarding/AddUserModal.tsx

diff --git a/app/components/Button.tsx b/app/components/Button.tsx
index 30fb389..73a8a04 100644
--- a/app/components/Button.tsx
+++ b/app/components/Button.tsx
@@ -52,7 +52,7 @@ export default function Button({
   return (
     <button
       disabled={disabled}
-      className={`rounded-lg font-medium transition-colors inline-flex items-center justify-center gap-2 ${
+      className={`rounded-lg font-medium transition-colors inline-flex items-center justify-center gap-2 cursor-pointer ${
         sizeStyles[size]
       } ${disabled ? styles.disabled : styles.base} ${
         fullWidth ? "w-full" : ""
diff --git a/app/components/auth/LoginModal.tsx b/app/components/auth/LoginModal.tsx
index 84b9131..3eda49a 100644
--- a/app/components/auth/LoginModal.tsx
+++ b/app/components/auth/LoginModal.tsx
@@ -42,8 +42,6 @@ export default function LoginModal({ open, onClose }: LoginModalProps) {
       });
 
       loginWithGoogle(data.access_token, data.user, data.google_profile);
-
-      toast.success("Logged in successfully!");
       onClose();
     } catch (err) {
       toast.error(
diff --git a/app/components/settings/onboarding/AddUserModal.tsx b/app/components/settings/onboarding/AddUserModal.tsx
new file mode 100644
index 0000000..697489b
--- /dev/null
+++ b/app/components/settings/onboarding/AddUserModal.tsx
@@ -0,0 +1,199 @@
+"use client";
+
+import { useState } from "react";
+import Modal from "@/app/components/Modal";
+import { Button, Field } from "@/app/components";
+import { isValidEmail } from "@/app/lib/utils";
+import { UserProjectListResponse } from "@/app/lib/types/onboarding";
+import { apiFetch } from "@/app/lib/apiClient";
+import { useToast } from "@/app/components/Toast";
+
+interface AddUserModalProps {
+  open: boolean;
+  onClose: () => void;
+  organizationId: number;
+  projectId: number;
+  apiKey: string;
+  onUsersAdded: () => void;
+}
+
+export default function AddUserModal({
+  open,
+  onClose,
+  organizationId,
+  projectId,
+  apiKey,
+  onUsersAdded,
+}: AddUserModalProps) {
+  const toast = useToast();
+  const [userRows, setUserRows] = useState([{ email: "", full_name: "" }]);
+  const [rowErrors, setRowErrors] = useState<Record<number, string>>({});
+  const [isAdding, setIsAdding] = useState(false);
+
+  const updateRow = (
+    index: number,
+    field: "email" | "full_name",
+    value: string,
+  ) => {
+    setUserRows((prev) =>
+      prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)),
+    );
+    if (rowErrors[index]) {
+      setRowErrors((prev) => {
+        const next = { ...prev };
+        delete next[index];
+        return next;
+      });
+    }
+  };
+
+  const addRow = () => {
+    setUserRows((prev) => [...prev, { email: "", full_name: "" }]);
+  };
+
+  const removeRow = (index: number) => {
+    if (userRows.length === 1) return;
+    setUserRows((prev) => prev.filter((_, i) => i !== index));
+    setRowErrors((prev) => {
+      const next: Record<number, string> = {};
+      Object.entries(prev).forEach(([k, v]) => {
+        const key = Number(k);
+        if (key < index) next[key] = v;
+        else if (key > index) next[key - 1] = v;
+      });
+      return next;
+    });
+  };
+
+  const resetForm = () => {
+    setUserRows([{ email: "", full_name: "" }]);
+    setRowErrors({});
+  };
+
+  const handleClose = () => {
+    resetForm();
+    onClose();
+  };
+
+  const handleSubmit = async () => {
+    const filledRows = userRows.filter((r) => r.email.trim());
+
+    if (filledRows.length === 0) {
+      setRowErrors({ 0: "Please enter an email address" });
+      return;
+    }
+
+    const errors: Record<number, string> = {};
+    userRows.forEach((row, i) => {
+      if (row.email.trim() && !isValidEmail(row.email.trim())) {
+        errors[i] = "Invalid email address";
+      }
+    });
+
+    if (Object.keys(errors).length > 0) {
+      setRowErrors(errors);
+      return;
+    }
+
+    setRowErrors({});
+    setIsAdding(true);
+
+    try {
+      const payload = filledRows.map((r) => {
+        const entry: { email: string; full_name?: string } = {
+          email: r.email.trim(),
+        };
+        if (r.full_name.trim()) entry.full_name = r.full_name.trim();
+        return entry;
+      });
+
+      await apiFetch<UserProjectListResponse>("/api/user-projects", apiKey, {
+        method: "POST",
+        body: JSON.stringify({
+          organization_id: organizationId,
+          project_id: projectId,
+          users: payload,
+        }),
+      });
+
+      toast.success(
+        `${filledRows.length} user${filledRows.length > 1 ? "s" : ""} added`,
+      );
+      resetForm();
+      onUsersAdded();
+      onClose();
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to add users");
+    } finally {
+      setIsAdding(false);
+    }
+  };
+
+  const filledCount = userRows.filter((r) => r.email.trim()).length;
+
+  return (
+    <Modal
+      open={open}
+      onClose={handleClose}
+      title="Add Users"
+      maxWidth="max-w-lg"
+    >
+      <div className="px-6 pb-6">
+        <p className="text-sm text-text-secondary mb-4">
+          Add users by email. Full name is optional.
+        </p>
+
+        <div className="space-y-3">
+          {userRows.map((row, index) => (
+            <div key={index} className="flex gap-2 items-start">
+              <div className="flex-1">
+                <Field
+                  label=""
+                  value={row.email}
+                  onChange={(val) => updateRow(index, "email", val)}
+                  placeholder="Email address"
+                  error={rowErrors[index]}
+                />
+              </div>
+              <div className="flex-1">
+                <Field
+                  label=""
+                  value={row.full_name}
+                  onChange={(val) => updateRow(index, "full_name", val)}
+                  placeholder="Full name (optional)"
+                />
+              </div>
+              {userRows.length > 1 && (
+                <button
+                  onClick={() => removeRow(index)}
+                  className="mt-2 p-1 text-text-secondary hover:text-red-500 transition-colors cursor-pointer"
+                >
+                  &times;
+                </button>
+              )}
+            </div>
+          ))}
+        </div>
+
+        <div className="mt-3 mb-5">
+          <Button variant="ghost" size="sm" onClick={addRow}>
+            + Add another
+          </Button>
+        </div>
+
+        <div className="flex items-center justify-end gap-2 pt-4 border-t border-border">
+          <Button variant="outline" size="md" onClick={handleClose}>
+            Cancel
+          </Button>
+          <Button
+            onClick={handleSubmit}
+            disabled={isAdding || filledCount === 0}
+            size="md"
+          >
+            {isAdding ? "Adding..." : `Add User${filledCount > 1 ? "s" : ""}`}
+          </Button>
+        </div>
+      </div>
+    </Modal>
+  );
+}
diff --git a/app/components/settings/onboarding/UserList.tsx b/app/components/settings/onboarding/UserList.tsx
index 5309bd7..6b3c10e 100644
--- a/app/components/settings/onboarding/UserList.tsx
+++ b/app/components/settings/onboarding/UserList.tsx
@@ -8,12 +8,17 @@ import {
   UserProjectListResponse,
   UserProjectDeleteResponse,
 } from "@/app/lib/types/onboarding";
-import { formatRelativeTime, isValidEmail } from "@/app/lib/utils";
-import { ArrowLeftIcon } from "@/app/components/icons";
-import { Button, Field } from "@/app/components";
+import { formatRelativeTime } from "@/app/lib/utils";
+import {
+  ArrowLeftIcon,
+  CheckCircleIcon,
+  TrashIcon,
+} from "@/app/components/icons";
+import { Button } from "@/app/components";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { apiFetch } from "@/app/lib/apiClient";
 import { useToast } from "@/app/components/Toast";
+import AddUserModal from "./AddUserModal";
 
 interface UserListProps {
   organization: Organization;
@@ -51,9 +56,7 @@ export default function UserList({
 
   const [users, setUsers] = useState<UserProject[]>([]);
   const [isLoading, setIsLoading] = useState(true);
-  const [userRows, setUserRows] = useState([{ email: "", full_name: "" }]);
-  const [rowErrors, setRowErrors] = useState<Record<number, string>>({});
-  const [isAdding, setIsAdding] = useState(false);
+  const [showAddModal, setShowAddModal] = useState(false);
   const [removingId, setRemovingId] = useState<number | null>(null);
 
   const fetchUsers = useCallback(async () => {
@@ -76,102 +79,13 @@ export default function UserList({
     fetchUsers();
   }, [fetchUsers]);
 
-  const updateRow = (
-    index: number,
-    field: "email" | "full_name",
-    value: string,
-  ) => {
-    setUserRows((prev) =>
-      prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)),
-    );
-    if (rowErrors[index]) {
-      setRowErrors((prev) => {
-        const next = { ...prev };
-        delete next[index];
-        return next;
-      });
-    }
-  };
-
-  const addRow = () => {
-    setUserRows((prev) => [...prev, { email: "", full_name: "" }]);
-  };
-
-  const removeRow = (index: number) => {
-    if (userRows.length === 1) return;
-    setUserRows((prev) => prev.filter((_, i) => i !== index));
-    setRowErrors((prev) => {
-      const next: Record<number, string> = {};
-      Object.entries(prev).forEach(([k, v]) => {
-        const key = Number(k);
-        if (key < index) next[key] = v;
-        else if (key > index) next[key - 1] = v;
-      });
-      return next;
-    });
-  };
-
-  const handleAddUsers = async () => {
-    const filledRows = userRows.filter((r) => r.email.trim());
-
-    if (filledRows.length === 0) {
-      setRowErrors({ 0: "Please enter an email address" });
-      return;
-    }
-
-    const errors: Record<number, string> = {};
-    userRows.forEach((row, i) => {
-      if (row.email.trim() && !isValidEmail(row.email.trim())) {
-        errors[i] = "Invalid email address";
-      }
-    });
-
-    if (Object.keys(errors).length > 0) {
-      setRowErrors(errors);
-      return;
-    }
-
-    setRowErrors({});
-    setIsAdding(true);
-
-    try {
-      const payload = filledRows.map((r) => {
-        const entry: { email: string; full_name?: string } = {
-          email: r.email.trim(),
-        };
-        if (r.full_name.trim()) entry.full_name = r.full_name.trim();
-        return entry;
-      });
-
-      await apiFetch<UserProjectListResponse>("/api/user-projects", apiKey, {
-        method: "POST",
-        body: JSON.stringify({
-          organization_id: organization.id,
-          project_id: project.id,
-          users: payload,
-        }),
-      });
-      toast.success(
-        `${filledRows.length} user${filledRows.length > 1 ? "s" : ""} added successfully`,
-      );
-      setUserRows([{ email: "", full_name: "" }]);
-      await fetchUsers();
-    } catch (err) {
-      toast.error(err instanceof Error ? err.message : "Failed to add users");
-    } finally {
-      setIsAdding(false);
-    }
-  };
-
   const handleRemoveUser = async (userId: number) => {
     setRemovingId(userId);
     try {
       await apiFetch<UserProjectDeleteResponse>(
         `/api/user-projects/${userId}?project_id=${project.id}`,
         apiKey,
-        {
-          method: "DELETE",
-        },
+        { method: "DELETE" },
       );
       toast.success("User removed");
       setUsers((prev) => prev.filter((u) => u.user_id !== userId));
@@ -188,71 +102,24 @@ export default function UserList({
         <ArrowLeftIcon className="w-3.5 h-3.5" /> Back to projects
       </Button>
 
-      <div className="mb-4">
-        <h2 className="text-lg font-semibold text-text-primary">
-          {project.name}
-        </h2>
-        <p className="text-xs text-text-secondary mt-0.5">
-          {organization.name} &middot;{" "}
-          {isLoading
-            ? "Loading users..."
-            : `${users.length} user${users.length !== 1 ? "s" : ""}`}
-        </p>
-      </div>
-
-      {currentUser?.is_superuser && (
-        <div className="mb-6 p-4 rounded-lg border border-border bg-white">
-          <p className="text-sm font-medium text-text-primary mb-3">
-            Add Users
+      <div className="flex items-center justify-between mb-4">
+        <div>
+          <h2 className="text-lg font-semibold text-text-primary">
+            {project.name}
+          </h2>
+          <p className="text-xs text-text-secondary mt-0.5">
+            {organization.name} &middot;{" "}
+            {isLoading
+              ? "Loading users..."
+              : `${users.length} user${users.length !== 1 ? "s" : ""}`}
           </p>
-          <div className="space-y-3">
-            {userRows.map((row, index) => (
-              <div key={index} className="flex gap-2 items-start">
-                <div className="flex-1">
-                  <Field
-                    label=""
-                    value={row.email}
-                    onChange={(val) => updateRow(index, "email", val)}
-                    placeholder="Email address"
-                    error={rowErrors[index]}
-                  />
-                </div>
-                <div className="flex-1">
-                  <Field
-                    label=""
-                    value={row.full_name}
-                    onChange={(val) => updateRow(index, "full_name", val)}
-                    placeholder="Full name (optional)"
-                  />
-                </div>
-                {userRows.length > 1 && (
-                  <Button
-                    variant="ghost"
-                    size="sm"
-                    onClick={() => removeRow(index)}
-                  >
-                    &times;
-                  </Button>
-                )}
-              </div>
-            ))}
-          </div>
-          <div className="flex items-center justify-between mt-3">
-            <Button variant="ghost" size="sm" onClick={addRow}>
-              + Add another
-            </Button>
-            <Button
-              onClick={handleAddUsers}
-              disabled={isAdding || userRows.every((r) => !r.email.trim())}
-              size="md"
-            >
-              {isAdding
-                ? "Adding..."
-                : `Add User${userRows.filter((r) => r.email.trim()).length > 1 ? "s" : ""}`}
-            </Button>
-          </div>
         </div>
-      )}
+        {currentUser?.is_superuser && (
+          <Button size="sm" onClick={() => setShowAddModal(true)}>
+            + Add User
+          </Button>
+        )}
+      </div>
 
       {isLoading ? (
         <UserListSkeleton />
@@ -278,31 +145,41 @@ export default function UserList({
                   Added {formatRelativeTime(user.inserted_at)}
                 </p>
               </div>
-              <div className="flex items-center gap-3">
+              <div className="flex items-center gap-2">
                 <span
-                  className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${
+                  className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full font-medium ${
                     user.is_active
                       ? "bg-green-50 text-green-700 border border-green-200"
                       : "bg-neutral-100 text-text-secondary border border-border"
                   }`}
                 >
+                  <CheckCircleIcon className="w-3.5 h-3.5" />
                   {user.is_active ? "Active" : "Inactive"}
                 </span>
                 {currentUser?.is_superuser && (
-                  <Button
-                    variant="danger"
-                    size="sm"
+                  <button
                     onClick={() => handleRemoveUser(user.user_id)}
                     disabled={removingId === user.user_id}
+                    className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 font-medium rounded-lg border border-red bg-white text-red-600 hover:bg-red-100 transition-colors cursor-pointer disabled:opacity-50"
                   >
+                    <TrashIcon className="w-3.5 h-3.5" />
                     {removingId === user.user_id ? "Removing..." : "Remove"}
-                  </Button>
+                  </button>
                 )}
               </div>
             </div>
           ))}
         </div>
       )}
+
+      <AddUserModal
+        open={showAddModal}
+        onClose={() => setShowAddModal(false)}
+        organizationId={organization.id}
+        projectId={project.id}
+        apiKey={apiKey}
+        onUsersAdded={fetchUsers}
+      />
     </div>
   );
 }
diff --git a/app/components/settings/onboarding/index.ts b/app/components/settings/onboarding/index.ts
index d0e766d..eb20cd9 100644
--- a/app/components/settings/onboarding/index.ts
+++ b/app/components/settings/onboarding/index.ts
@@ -4,3 +4,4 @@ export { default as OrganizationList } from "./OrganizationList";
 export { default as ProjectList } from "./ProjectList";
 export { default as StepIndicator } from "./StepIndicator";
 export { default as UserList } from "./UserList";
+export { default as AddUserModal } from "./AddUserModal";
diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts
index 4d62510..98b5916 100644
--- a/app/lib/apiClient.ts
+++ b/app/lib/apiClient.ts
@@ -97,6 +97,22 @@ export async function apiFetch<T>(
   // Happy path
   if (res.ok) return (await res.json()) as T;
 
+  // 403 "access revoked" — force logout immediately, no refresh attempt
+  if (res.status === 403) {
+    const data = await res.json().catch(() => ({}));
+    const msg =
+      (data as Record<string, string>).error ||
+      (data as Record<string, string>).message ||
+      "";
+    if (msg.toLowerCase().includes("access revoked")) {
+      if (typeof window !== "undefined") {
+        window.dispatchEvent(new CustomEvent(AUTH_EXPIRED_EVENT));
+      }
+      throw new Error(msg || "Access revoked. Please log in again.");
+    }
+    throw new Error(msg || `Request failed: ${res.status}`);
+  }
+
   // Not a 401 — throw immediately
   if (res.status !== 401) {
     const data = await res.json();

From e57c2e4841f5b357ca697d5afbd80d4c8e8b598b Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Thu, 2 Apr 2026 15:12:28 +0530
Subject: [PATCH 07/10] fix(*): some edge cases handled

---
 app/lib/apiClient.ts            | 120 ++++++++++++++++----------------
 app/lib/constants.ts            |   3 +
 app/lib/context/AuthContext.tsx |   3 +-
 3 files changed, 64 insertions(+), 62 deletions(-)

diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts
index 98b5916..c0181cd 100644
--- a/app/lib/apiClient.ts
+++ b/app/lib/apiClient.ts
@@ -1,11 +1,14 @@
 import { NextRequest } from "next/server";
+import { AUTH_EXPIRED_EVENT } from "@/app/lib/constants";
 
 const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
 
+/** Coalesces concurrent refresh calls into a single request. */
+let refreshPromise: Promise<boolean> | null = null;
+
 /**
- * Passthrough proxy helper for Next.js route handlers.
- * Extracts X-API-KEY and cookies from the incoming request and forwards them to the backend.
- * Returns raw { status, data, headers } so the route handler can relay the exact HTTP status.
+ * Forwards a request to the backend, relaying auth headers (X-API-KEY, Cookie).
+ * Returns raw { status, data, headers } so the route handler can relay the response.
  */
 export async function apiClient(
   request: NextRequest | Request,
@@ -33,18 +36,27 @@ export async function apiClient(
   return { status: response.status, data, headers: response.headers };
 }
 
-/**
- * Dispatched when both the access token AND refresh token are expired /
- * invalid.  AuthContext listens for this and triggers logout.
- */
-export const AUTH_EXPIRED_EVENT = "kaapi:auth-expired";
+/** Parse an error body into a readable message string. */
+function extractErrorMessage(
+  body: Record<string, unknown>,
+  fallback: string,
+): string {
+  const msg =
+    (body.error as string) ||
+    (body.message as string) ||
+    (body.detail as string) ||
+    "";
+  return msg || fallback;
+}
 
-/**
- * Singleton refresh promise so concurrent 401s don't fire multiple
- * refresh requests — they all await the same in-flight call.
- */
-let refreshPromise: Promise<boolean> | null = null;
+/** Dispatch the auth-expired event (client-side only). */
+function dispatchAuthExpired() {
+  if (typeof window !== "undefined") {
+    window.dispatchEvent(new CustomEvent(AUTH_EXPIRED_EVENT));
+  }
+}
 
+/** Attempt a silent token refresh. Returns true if new cookies were set. */
 async function tryRefreshToken(): Promise<boolean> {
   if (refreshPromise) return refreshPromise;
 
@@ -67,12 +79,11 @@ async function tryRefreshToken(): Promise<boolean> {
 
 /**
  * Client-side fetch helper for Next.js route handlers (/api/*).
- * Attaches the X-API-KEY header and includes credentials for cookie-based auth.
  *
- * On a 401 response it automatically attempts a token refresh via
- * `/api/auth/refresh`.  If the refresh succeeds the original request is
- * retried once.  If the refresh also fails, a `kaapi:auth-expired` event
- * is dispatched so AuthContext can trigger logout.
+ * - Attaches X-API-KEY header and `credentials: "include"` for cookie auth.
+ * - On **403 "revoked"**: forces immediate logout (no refresh).
+ * - On **401**: tries a silent token refresh, retries once, then logs out.
+ * - All other errors are thrown as-is.
  */
 export async function apiFetch<T>(
   url: string,
@@ -88,63 +99,50 @@ export async function apiFetch<T>(
     return headers;
   };
 
-  const res = await fetch(url, {
-    ...options,
-    headers: buildHeaders(),
-    credentials: "include",
-  });
+  const doFetch = () =>
+    fetch(url, { ...options, headers: buildHeaders(), credentials: "include" });
+
+  const res = await doFetch();
 
-  // Happy path
+  // Response OK → return parsed JSON
   if (res.ok) return (await res.json()) as T;
 
-  // 403 "access revoked" — force logout immediately, no refresh attempt
-  if (res.status === 403) {
-    const data = await res.json().catch(() => ({}));
-    const msg =
-      (data as Record<string, string>).error ||
-      (data as Record<string, string>).message ||
-      "";
-    if (msg.toLowerCase().includes("access revoked")) {
-      if (typeof window !== "undefined") {
-        window.dispatchEvent(new CustomEvent(AUTH_EXPIRED_EVENT));
-      }
-      throw new Error(msg || "Access revoked. Please log in again.");
-    }
-    throw new Error(msg || `Request failed: ${res.status}`);
+  const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
+  const message = extractErrorMessage(body, `Request failed: ${res.status}`);
+
+  // 403 with "revoked" → force logout, no refresh attempt
+  if (res.status === 403 && message.toLowerCase().includes("revoked")) {
+    dispatchAuthExpired();
+    throw new Error(message.trim() || "Access revoked. Please log in again.");
   }
 
-  // Not a 401 — throw immediately
+  // Non-401 errors → throw immediately
   if (res.status !== 401) {
-    const data = await res.json();
-    throw new Error(
-      data.error || data.message || `Request failed: ${res.status}`,
-    );
+    throw new Error(message);
+  }
+
+  // Auth endpoints (login, register, etc.) — never auto-refresh, just throw
+  if (url.startsWith("/api/auth/")) {
+    throw new Error(message);
   }
 
-  // 401 — attempt a silent token refresh
+  // 401 → attempt silent token refresh, then retry once
   const refreshed = await tryRefreshToken();
 
   if (refreshed) {
-    const retry = await fetch(url, {
-      ...options,
-      headers: buildHeaders(),
-      credentials: "include",
-    });
-    const retryData = await retry.json();
-    if (retry.ok) return retryData as T;
+    const retry = await doFetch();
+    if (retry.ok) return (await retry.json()) as T;
 
+    const retryBody = (await retry.json().catch(() => ({}))) as Record<
+      string,
+      unknown
+    >;
     throw new Error(
-      retryData.error || retryData.message || `Request failed: ${retry.status}`,
+      extractErrorMessage(retryBody, `Request failed: ${retry.status}`),
     );
   }
 
-  if (typeof window !== "undefined") {
-    window.dispatchEvent(new CustomEvent(AUTH_EXPIRED_EVENT));
-  }
-  const data = await res.json().catch(() => ({}));
-  throw new Error(
-    (data as Record<string, string>).error ||
-      (data as Record<string, string>).message ||
-      "Session expired. Please log in again.",
-  );
+  // Refresh failed → both tokens expired, force logout
+  dispatchAuthExpired();
+  throw new Error("Session expired. Please log in again.");
 }
diff --git a/app/lib/constants.ts b/app/lib/constants.ts
index 4cb99d6..9bf5cbf 100644
--- a/app/lib/constants.ts
+++ b/app/lib/constants.ts
@@ -13,6 +13,9 @@ export const DEFAULT_PAGE_LIMIT = 10;
 /** Custom event dispatched when background validation invalidates the in-memory cache */
 export const CACHE_INVALIDATED_EVENT = "kaapi:config-cache-invalidated";
 
+/** Dispatched when the user's session is no longer valid (expired or revoked). */
+export const AUTH_EXPIRED_EVENT = "kaapi:auth-expired";
+
 export const PROVIDES_OPTIONS = [
   { value: "openai", label: "OpenAI" },
   { value: "anthropic", label: "Anthropic" },
diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx
index f78f796..eae4ccf 100644
--- a/app/lib/context/AuthContext.tsx
+++ b/app/lib/context/AuthContext.tsx
@@ -14,7 +14,8 @@ import {
   Session,
   AuthContextValue,
 } from "@/app/lib/types/auth";
-import { apiFetch, AUTH_EXPIRED_EVENT } from "@/app/lib/apiClient";
+import { apiFetch } from "@/app/lib/apiClient";
+import { AUTH_EXPIRED_EVENT } from "@/app/lib/constants";
 export type { User, GoogleProfile, Session } from "@/app/lib/types/auth";
 
 const STORAGE_KEY = "kaapi_api_keys";

From 0e4d1dc884298d8dfc02282d16f5b156c3b8c58f Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Thu, 2 Apr 2026 15:58:51 +0530
Subject: [PATCH 08/10] fix(*): authentication handle in the knowledge base

---
 app/(main)/document/page.tsx                |  11 +-
 app/(main)/knowledge-base/page.tsx          | 832 ++++++--------------
 app/components/document/DocumentListing.tsx |   2 +-
 app/components/document/DocumentPreview.tsx |   2 +-
 app/lib/types/document.ts                   |   9 +
 app/lib/types/knowledgeBase.ts              |  39 +
 6 files changed, 312 insertions(+), 583 deletions(-)
 create mode 100644 app/lib/types/document.ts
 create mode 100644 app/lib/types/knowledgeBase.ts

diff --git a/app/(main)/document/page.tsx b/app/(main)/document/page.tsx
index 18e7497..20d6c00 100644
--- a/app/(main)/document/page.tsx
+++ b/app/(main)/document/page.tsx
@@ -12,16 +12,9 @@ import { DocumentListing } from "@/app/components/document/DocumentListing";
 import { DocumentPreview } from "@/app/components/document/DocumentPreview";
 import { UploadDocumentModal } from "@/app/components/document/UploadDocumentModal";
 import { DEFAULT_PAGE_LIMIT } from "@/app/lib/constants";
+import { Document } from "@/app/lib/types/document";
 
-export interface Document {
-  id: string;
-  fname: string;
-  object_store_url: string;
-  signed_url?: string;
-  file_size?: number;
-  inserted_at?: string;
-  updated_at?: string;
-}
+export type { Document } from "@/app/lib/types/document";
 
 export default function DocumentPage() {
   const toast = useToast();
diff --git a/app/(main)/knowledge-base/page.tsx b/app/(main)/knowledge-base/page.tsx
index a27c52d..6239f69 100644
--- a/app/(main)/knowledge-base/page.tsx
+++ b/app/(main)/knowledge-base/page.tsx
@@ -1,62 +1,30 @@
 "use client";
 
 import { useState, useEffect, useRef } from "react";
-import { colors } from "@/app/lib/colors";
 import { formatDate } from "@/app/components/utils";
 import Sidebar from "@/app/components/Sidebar";
 import PageHeader from "@/app/components/PageHeader";
 import Modal from "@/app/components/Modal";
+import {
+  CloseIcon,
+  TrashIcon,
+  BookOpenIcon,
+  ChevronRightIcon,
+} from "@/app/components/icons";
+import { Button, Field } from "@/app/components";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import { useApp } from "@/app/lib/context/AppContext";
 import { apiFetch } from "@/app/lib/apiClient";
-
-export interface Document {
-  id: string;
-  fname: string;
-  object_store_url: string;
-  signed_url?: string;
-  file_size?: number;
-  inserted_at?: string;
-  updated_at?: string;
-}
-
-export interface Collection {
-  id: string;
-  name?: string;
-  description?: string;
-  knowledge_base_id?: string;
-  inserted_at: string;
-  updated_at: string;
-  status?: string;
-  job_id?: string;
-  documents?: Document[];
-}
-
-interface JobStatusData {
-  status?: string | null;
-  collection?: { id?: string; knowledge_base_id?: string };
-  collection_id?: string | null;
-}
-
-interface CollectionResponse {
-  data?: Collection[] | Collection;
-}
-
-interface DocumentResponse {
-  data?: Document[];
-}
-
-interface CreateCollectionResponse {
-  data?: { job_id?: string };
-}
-
-interface DeleteCollectionResponse {
-  data?: { job_id?: string };
-}
-
-interface DocumentDetailResponse {
-  data?: Document;
-}
+import { Document } from "@/app/lib/types/document";
+import {
+  Collection,
+  JobStatusData,
+  CollectionResponse,
+  DocumentResponse,
+  CreateCollectionResponse,
+  DeleteCollectionResponse,
+  DocumentDetailResponse,
+} from "@/app/lib/types/knowledgeBase";
 
 export default function KnowledgeBasePage() {
   const { sidebarCollapsed } = useApp();
@@ -490,7 +458,7 @@ export default function KnowledgeBasePage() {
 
     pollingRef.current = setInterval(async () => {
       const currentApiKey = apiKeyRef.current;
-      if (!currentApiKey) return;
+      if (!currentApiKey && !isAuthenticated) return;
 
       const jobs = activeJobsRef.current;
       if (jobs.size === 0) {
@@ -505,7 +473,7 @@ export default function KnowledgeBasePage() {
         try {
           const result = await apiFetch<
             { data?: JobStatusData } & JobStatusData
-          >(`/api/collections/jobs/${jobId}`, currentApiKey.key);
+          >(`/api/collections/jobs/${jobId}`, currentApiKey?.key ?? "");
           const jobData = result.data || result;
           const status = jobData.status || null;
           const realCollectionId =
@@ -721,7 +689,7 @@ export default function KnowledgeBasePage() {
           try {
             const jobResult = await apiFetch<
               { data?: JobStatusData } & JobStatusData
-            >(`/api/collections/jobs/${jobId}`, currentApiKey.key);
+            >(`/api/collections/jobs/${jobId}`, currentApiKey?.key ?? "");
             const jobData = jobResult.data || jobResult;
             const status = jobData.status;
             const statusLower = status?.toLowerCase();
@@ -801,16 +769,16 @@ export default function KnowledgeBasePage() {
   };
 
   useEffect(() => {
-    if (apiKey) {
+    if (isAuthenticated) {
       fetchCollections();
       fetchDocuments();
     }
-  }, [apiKey]);
+  }, [isAuthenticated]);
 
   // Keep apiKeyRef in sync so polling always has the current key
   useEffect(() => {
     apiKeyRef.current = apiKey;
-  }, [apiKey]);
+  }, [apiKey, isAuthenticated]);
 
   // Keep fetchCollectionsRef in sync so polling always has the current function
   useEffect(() => {
@@ -837,8 +805,8 @@ export default function KnowledgeBasePage() {
       }
     });
 
-    if (newJobAdded && apiKey) startPolling();
-  }, [collections, apiKey]);
+    if (newJobAdded && isAuthenticated) startPolling();
+  }, [collections, isAuthenticated]);
 
   // Reset showAllDocs when selectedCollection changes
   useEffect(() => {
@@ -856,10 +824,7 @@ export default function KnowledgeBasePage() {
   }, []);
 
   return (
-    <div
-      className="flex h-screen"
-      style={{ backgroundColor: colors.bg.primary }}
-    >
+    <div className="flex h-screen bg-bg-primary">
       <Sidebar collapsed={sidebarCollapsed} activeRoute="/knowledge-base" />
 
       {/* Main Content */}
@@ -872,165 +837,94 @@ export default function KnowledgeBasePage() {
         {/* Content Area - Split View */}
         <div className="flex-1 overflow-hidden flex">
           {/* Left Panel - Collections List */}
-          <div
-            className="w-1/3 border-r flex flex-col"
-            style={{ borderColor: colors.border }}
-          >
+          <div className="w-1/3 border-r border-border flex flex-col">
             {/* Create Button */}
             <div className="p-6 flex justify-end">
-              <button
+              <Button
+                variant="outline"
+                size="sm"
                 onClick={() => {
                   setShowCreateForm(true);
                   setSelectedCollection(null);
                 }}
-                className="px-4 py-2 rounded-md text-sm font-medium transition-colors"
-                style={{
-                  backgroundColor: colors.bg.secondary,
-                  color: colors.text.primary,
-                  border: `1px solid ${colors.border}`,
-                }}
               >
                 + Create
-              </button>
+              </Button>
             </div>
 
             {/* Collections List */}
             <div className="flex-1 overflow-y-auto px-6 pb-6">
               {isLoading && collections.length === 0 ? (
-                <div
-                  className="text-center py-8"
-                  style={{ color: colors.text.secondary }}
-                >
+                <div className="text-center py-8 text-text-secondary">
                   Loading knowledge bases...
                 </div>
               ) : collections.length === 0 ? (
-                <div
-                  className="text-center py-8"
-                  style={{ color: colors.text.secondary }}
-                >
+                <div className="text-center py-8 text-text-secondary">
                   No knowledge bases yet. Create your first one!
                 </div>
               ) : (
-                <div className="space-y-2">
-                  {collections.map((collection) => (
-                    <div
-                      key={collection.id}
-                      onClick={() => {
-                        setShowCreateForm(false);
-                        setShowDocumentPicker(false);
-                        fetchCollectionDetails(collection.id);
-                      }}
-                      className={`border rounded-lg p-3 cursor-pointer transition-colors ${
-                        selectedCollection?.id === collection.id
-                          ? "ring-2 ring-offset-1"
-                          : ""
-                      }`}
-                      style={{
-                        backgroundColor:
-                          selectedCollection?.id === collection.id
-                            ? "hsl(202, 100%, 95%)"
-                            : colors.bg.primary,
-                        borderColor:
-                          selectedCollection?.id === collection.id
-                            ? "hsl(202, 100%, 50%)"
-                            : colors.border,
-                      }}
-                    >
-                      <div className="flex items-start justify-between gap-2">
-                        <div className="flex-1 min-w-0">
-                          <div className="flex items-center gap-2 mb-1">
-                            <svg
-                              className="w-5 h-5 flex-shrink-0"
-                              fill="none"
-                              viewBox="0 0 24 24"
-                              stroke="currentColor"
-                              style={{ color: colors.text.primary }}
-                            >
-                              <path
-                                strokeLinecap="round"
-                                strokeLinejoin="round"
-                                strokeWidth={2}
-                                d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
+                <div className="space-y-1.5">
+                  {collections.map((collection) => {
+                    const isSelected = selectedCollection?.id === collection.id;
+                    return (
+                      <button
+                        key={collection.id}
+                        onClick={() => {
+                          setShowCreateForm(false);
+                          setShowDocumentPicker(false);
+                          fetchCollectionDetails(collection.id);
+                        }}
+                        className={`w-full text-left rounded-lg p-3 transition-all duration-150 border ${
+                          isSelected
+                            ? "bg-neutral-100 border-border font-semibold"
+                            : "bg-white border-border hover:bg-neutral-50"
+                        }`}
+                      >
+                        <div className="flex items-start justify-between gap-2">
+                          <div className="flex-1 min-w-0">
+                            <div className="flex items-center gap-2 mb-0.5">
+                              <BookOpenIcon
+                                className={`w-4 h-4 shrink-0 ${isSelected ? "text-text-primary" : "text-text-secondary"}`}
                               />
-                            </svg>
-                            <h3
-                              className="text-sm font-semibold truncate"
-                              style={{ color: colors.text.primary }}
-                            >
-                              {collection.name}
-                            </h3>
+                              <h3 className="text-sm font-medium text-text-primary truncate">
+                                {collection.name}
+                              </h3>
+                            </div>
+                            {collection.description && (
+                              <p className="text-xs text-text-secondary truncate pl-6">
+                                {collection.description}
+                              </p>
+                            )}
+                            <p className="text-xs text-text-secondary mt-0.5 pl-6">
+                              {formatDate(collection.inserted_at)}
+                            </p>
                           </div>
-                          {collection.description && (
-                            <p
-                              className="text-xs truncate"
-                              style={{ color: colors.text.secondary }}
+                          {!collection.id.startsWith("optimistic-") && (
+                            <span
+                              onClick={(e) => {
+                                e.stopPropagation();
+                                handleDeleteCollection(collection.id);
+                              }}
+                              className="p-1.5 rounded-md border border-red-200 bg-white text-red-500 hover:bg-red-50 transition-colors shrink-0 cursor-pointer"
+                              title="Delete Knowledge Base"
                             >
-                              {collection.description}
-                            </p>
+                              <TrashIcon className="w-3.5 h-3.5" />
+                            </span>
                           )}
-                          <p
-                            className="text-xs mt-1"
-                            style={{ color: colors.text.secondary }}
-                          >
-                            {formatDate(collection.inserted_at)}
-                          </p>
                         </div>
-                        {!collection.id.startsWith("optimistic-") && (
-                          <button
-                            onClick={(e) => {
-                              e.stopPropagation();
-                              handleDeleteCollection(collection.id);
-                            }}
-                            className="p-1.5 rounded-md transition-colors flex-shrink-0"
-                            style={{
-                              borderWidth: "1px",
-                              borderColor: "hsl(8, 86%, 80%)",
-                              backgroundColor: colors.bg.primary,
-                              color: "hsl(8, 86%, 40%)",
-                            }}
-                            onMouseEnter={(e) => {
-                              e.currentTarget.style.backgroundColor =
-                                "hsl(8, 86%, 95%)";
-                            }}
-                            onMouseLeave={(e) => {
-                              e.currentTarget.style.backgroundColor =
-                                colors.bg.primary;
-                            }}
-                            title="Delete Knowledge Base"
-                          >
-                            <svg
-                              className="w-4 h-4"
-                              fill="none"
-                              viewBox="0 0 24 24"
-                              stroke="currentColor"
-                            >
-                              <path
-                                strokeLinecap="round"
-                                strokeLinejoin="round"
-                                strokeWidth={2}
-                                d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
-                              />
-                            </svg>
-                          </button>
-                        )}
-                      </div>
-                    </div>
-                  ))}
+                      </button>
+                    );
+                  })}
                 </div>
               )}
             </div>
           </div>
 
-          {/* Right Panel - Create Form or Preview */}
           <div className="w-2/3 flex flex-col">
             {showCreateForm ? (
-              /* Create Form */
               <div className="flex-1 overflow-y-auto p-6">
                 <div className="flex items-center justify-between mb-6">
-                  <h2
-                    className="text-xl font-semibold"
-                    style={{ color: colors.text.primary }}
-                  >
+                  <h2 className="text-xl font-semibold text-text-primary">
                     Create Knowledge Base
                   </h2>
                   <button
@@ -1041,53 +935,25 @@ export default function KnowledgeBasePage() {
                       setCollectionDescription("");
                       setSelectedDocuments(new Set());
                     }}
-                    className="p-2 rounded-md transition-colors"
-                    style={{ color: colors.text.secondary }}
+                    className="p-2 rounded-md text-text-secondary hover:bg-neutral-100 transition-colors"
                   >
-                    <svg
-                      className="w-5 h-5"
-                      fill="none"
-                      viewBox="0 0 24 24"
-                      stroke="currentColor"
-                    >
-                      <path
-                        strokeLinecap="round"
-                        strokeLinejoin="round"
-                        strokeWidth={2}
-                        d="M6 18L18 6M6 6l12 12"
-                      />
-                    </svg>
+                    <CloseIcon className="w-5 h-5" />
                   </button>
                 </div>
 
                 {/* Name Input */}
                 <div className="mb-4">
-                  <label
-                    className="block text-sm font-medium mb-2"
-                    style={{ color: colors.text.primary }}
-                  >
-                    Name *
-                  </label>
-                  <input
-                    type="text"
+                  <Field
+                    label="Name *"
                     value={collectionName}
-                    onChange={(e) => setCollectionName(e.target.value)}
+                    onChange={setCollectionName}
                     placeholder="Enter collection name"
-                    className="w-full px-4 py-2 rounded-md border text-sm"
-                    style={{
-                      borderColor: colors.border,
-                      backgroundColor: colors.bg.secondary,
-                      color: colors.text.primary,
-                    }}
                   />
                 </div>
 
                 {/* Description Input */}
                 <div className="mb-4">
-                  <label
-                    className="block text-sm font-medium mb-2"
-                    style={{ color: colors.text.primary }}
-                  >
+                  <label className="block text-sm font-medium mb-2 text-text-primary">
                     Description
                   </label>
                   <textarea
@@ -1095,51 +961,26 @@ export default function KnowledgeBasePage() {
                     onChange={(e) => setCollectionDescription(e.target.value)}
                     placeholder="Enter collection description (optional)"
                     rows={3}
-                    className="w-full px-4 py-2 rounded-md border text-sm resize-none"
-                    style={{
-                      borderColor: colors.border,
-                      backgroundColor: colors.bg.secondary,
-                      color: colors.text.primary,
-                    }}
+                    className="w-full px-4 py-2 rounded-md border border-border bg-bg-secondary text-text-primary text-sm resize-none focus:outline-none focus:ring-1 focus:ring-border"
                   />
                 </div>
 
                 {/* Document Selection */}
                 <div className="mb-6">
-                  <label
-                    className="block text-sm font-medium mb-2"
-                    style={{ color: colors.text.primary }}
-                  >
+                  <label className="block text-sm font-medium mb-2 text-text-primary">
                     Select Documents *
                   </label>
                   <button
                     type="button"
                     onClick={() => setShowDocumentPicker(true)}
-                    className="w-full px-4 py-3 rounded-md border text-left flex items-center justify-between"
-                    style={{
-                      borderColor: colors.border,
-                      backgroundColor: colors.bg.secondary,
-                      color: colors.text.primary,
-                    }}
+                    className="w-full px-4 py-3 rounded-md border border-border bg-bg-secondary text-text-primary text-left flex items-center justify-between hover:bg-neutral-100 transition-colors"
                   >
                     <span className="text-sm">
                       {selectedDocuments.size === 0
                         ? "Click to select documents"
                         : `${selectedDocuments.size} document${selectedDocuments.size > 1 ? "s" : ""} selected`}
                     </span>
-                    <svg
-                      className="w-5 h-5"
-                      fill="none"
-                      viewBox="0 0 24 24"
-                      stroke="currentColor"
-                    >
-                      <path
-                        strokeLinecap="round"
-                        strokeLinejoin="round"
-                        strokeWidth={2}
-                        d="M9 5l7 7-7 7"
-                      />
-                    </svg>
+                    <ChevronRightIcon className="w-4 h-4 text-text-secondary" />
                   </button>
 
                   {/* Show selected documents */}
@@ -1153,37 +994,16 @@ export default function KnowledgeBasePage() {
                         return (
                           <div
                             key={docId}
-                            className="flex items-center justify-between p-2 rounded-md"
-                            style={{
-                              backgroundColor: colors.bg.secondary,
-                              borderColor: colors.border,
-                              border: `1px solid ${colors.border}`,
-                            }}
+                            className="flex items-center justify-between p-2 rounded-md border border-border bg-bg-secondary"
                           >
-                            <span
-                              className="text-sm truncate"
-                              style={{ color: colors.text.primary }}
-                            >
+                            <span className="text-sm text-text-primary truncate">
                               {doc.fname}
                             </span>
                             <button
                               onClick={() => toggleDocumentSelection(docId)}
-                              className="ml-2 p-1 rounded-md transition-colors"
-                              style={{ color: colors.text.secondary }}
+                              className="ml-2 p-1 rounded-md text-text-secondary hover:text-red-500 transition-colors"
                             >
-                              <svg
-                                className="w-4 h-4"
-                                fill="none"
-                                viewBox="0 0 24 24"
-                                stroke="currentColor"
-                              >
-                                <path
-                                  strokeLinecap="round"
-                                  strokeLinejoin="round"
-                                  strokeWidth={2}
-                                  d="M6 18L18 6M6 6l12 12"
-                                />
-                              </svg>
+                              <CloseIcon className="w-4 h-4" />
                             </button>
                           </div>
                         );
@@ -1194,128 +1014,79 @@ export default function KnowledgeBasePage() {
 
                 {/* Create Button */}
                 <div className="flex justify-end">
-                  <button
+                  <Button
                     onClick={handleCreateClick}
                     disabled={
                       isCreating ||
                       !collectionName.trim() ||
                       selectedDocuments.size === 0
                     }
-                    className="px-6 py-2 rounded-md text-sm font-medium"
-                    style={{
-                      backgroundColor: colors.bg.secondary,
-                      color: colors.text.primary,
-                      border: `1px solid ${colors.border}`,
-                      opacity:
-                        isCreating ||
-                        !collectionName.trim() ||
-                        selectedDocuments.size === 0
-                          ? 0.5
-                          : 1,
-                    }}
                   >
                     {isCreating ? "Creating..." : "Create"}
-                  </button>
+                  </Button>
                 </div>
               </div>
             ) : selectedCollection ? (
               <>
                 {/* Preview Header */}
-                <div
-                  className="p-6 border-b"
-                  style={{ borderColor: colors.border }}
-                >
+                <div className="p-6 border-b border-border">
                   <div className="flex items-start justify-between">
                     <div className="flex-1">
-                      <h2
-                        className="text-xl font-semibold"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <h2 className="text-xl font-semibold text-text-primary">
                         {selectedCollection.name}
                       </h2>
                       {selectedCollection.description && (
-                        <p
-                          className="text-sm mt-1"
-                          style={{ color: colors.text.secondary }}
-                        >
+                        <p className="text-sm mt-1 text-text-secondary">
                           {selectedCollection.description}
                         </p>
                       )}
                     </div>
                     {!selectedCollection.id.startsWith("optimistic-") && (
-                      <button
+                      <Button
+                        variant="danger"
+                        size="sm"
                         onClick={() =>
                           handleDeleteCollection(selectedCollection.id)
                         }
-                        className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors"
-                        style={{
-                          backgroundColor: "transparent",
-                          color: "#ef4444",
-                          border: "1px solid #ef4444",
-                        }}
                       >
                         Delete
-                      </button>
+                      </Button>
                     )}
                   </div>
 
                   {/* Metadata */}
                   <div className="space-y-3 mt-6">
                     <div className="flex items-center gap-3">
-                      <div
-                        className="text-xs uppercase font-semibold"
-                        style={{ color: colors.text.secondary }}
-                      >
+                      <div className="text-xs uppercase font-semibold text-text-secondary">
                         Status:
                       </div>
-                      <div
-                        className="text-sm font-semibold"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <div className="text-sm font-semibold text-text-primary">
                         {(selectedCollection.status || "N/A")
                           .replace(/_/g, " ")
                           .toUpperCase()}
                       </div>
                     </div>
                     <div className="flex items-center gap-3">
-                      <div
-                        className="text-xs uppercase font-semibold"
-                        style={{ color: colors.text.secondary }}
-                      >
+                      <div className="text-xs uppercase font-semibold text-text-secondary">
                         Knowledge Base ID:
                       </div>
-                      <div
-                        className="text-sm font-semibold"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <div className="text-sm font-semibold text-text-primary">
                         {selectedCollection.knowledge_base_id || "N/A"}
                       </div>
                     </div>
                     <div className="flex items-center gap-3">
-                      <div
-                        className="text-xs uppercase font-semibold"
-                        style={{ color: colors.text.secondary }}
-                      >
+                      <div className="text-xs uppercase font-semibold text-text-secondary">
                         Created:
                       </div>
-                      <div
-                        className="text-sm font-semibold"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <div className="text-sm font-semibold text-text-primary">
                         {formatDate(selectedCollection.inserted_at)}
                       </div>
                     </div>
                     <div className="flex items-center gap-3">
-                      <div
-                        className="text-xs uppercase font-semibold"
-                        style={{ color: colors.text.secondary }}
-                      >
+                      <div className="text-xs uppercase font-semibold text-text-secondary">
                         Last Updated:
                       </div>
-                      <div
-                        className="text-sm font-semibold"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <div className="text-sm font-semibold text-text-primary">
                         {formatDate(selectedCollection.updated_at)}
                       </div>
                     </div>
@@ -1325,10 +1096,7 @@ export default function KnowledgeBasePage() {
                 {/* Documents in Collection */}
                 <div className="flex-1 overflow-y-auto px-6 pt-6 pb-4">
                   <div className="flex items-center justify-between mb-4">
-                    <h3
-                      className="text-sm font-semibold"
-                      style={{ color: colors.text.primary }}
-                    >
+                    <h3 className="text-sm font-semibold text-text-primary">
                       Documents Present (
                       {selectedCollection.documents?.length || 0})
                     </h3>
@@ -1341,7 +1109,7 @@ export default function KnowledgeBasePage() {
                             const firstDoc = selectedCollection.documents![0];
                             setPreviewDoc(firstDoc);
 
-                            if (apiKey) {
+                            if (isAuthenticated) {
                               try {
                                 const data = await apiFetch<
                                   DocumentDetailResponse & Document
@@ -1360,12 +1128,7 @@ export default function KnowledgeBasePage() {
                               }
                             }
                           }}
-                          className="text-xs px-3 py-1.5 rounded-md hover:opacity-80 transition-opacity"
-                          style={{
-                            color: colors.text.primary,
-                            backgroundColor: colors.bg.secondary,
-                            border: `1px solid ${colors.border}`,
-                          }}
+                          className="text-xs px-3 py-1.5 rounded-md border border-border bg-bg-secondary text-text-primary hover:bg-neutral-100 transition-colors"
                         >
                           Preview
                         </button>
@@ -1377,23 +1140,14 @@ export default function KnowledgeBasePage() {
                   selectedCollection.documents.length > 0 ? (
                     <div>
                       {/* Header Row */}
-                      <div
-                        className="flex items-center justify-between pb-2 mb-2 border-b"
-                        style={{ borderColor: colors.border }}
-                      >
+                      <div className="flex items-center justify-between pb-2 mb-2 border-b border-border">
                         <div className="flex-1">
-                          <p
-                            className="text-[10px] font-semibold uppercase"
-                            style={{ color: colors.text.secondary }}
-                          >
+                          <p className="text-[10px] font-semibold uppercase text-text-secondary">
                             Name
                           </p>
                         </div>
                         <div className="flex-shrink-0">
-                          <p
-                            className="text-[10px] font-semibold uppercase"
-                            style={{ color: colors.text.secondary }}
-                          >
+                          <p className="text-[10px] font-semibold uppercase text-text-secondary">
                             Uploaded At
                           </p>
                         </div>
@@ -1414,18 +1168,12 @@ export default function KnowledgeBasePage() {
                               className="flex items-center justify-between py-1"
                             >
                               <div className="flex-1 min-w-0 mr-4">
-                                <p
-                                  className="text-sm truncate"
-                                  style={{ color: colors.text.primary }}
-                                >
+                                <p className="text-sm text-text-primary truncate">
                                   {doc.fname}
                                 </p>
                               </div>
                               <div className="flex-shrink-0">
-                                <p
-                                  className="text-xs"
-                                  style={{ color: colors.text.secondary }}
-                                >
+                                <p className="text-xs text-text-secondary">
                                   {doc.inserted_at
                                     ? formatDate(doc.inserted_at)
                                     : "N/A"}
@@ -1437,36 +1185,29 @@ export default function KnowledgeBasePage() {
 
                       {/* Show More / Show Less Button */}
                       {selectedCollection.documents.length > 3 && (
-                        <button
-                          onClick={() => setShowAllDocs(!showAllDocs)}
-                          className="w-full py-2 px-4 rounded-md text-sm font-medium transition-colors mt-3"
-                          style={{
-                            backgroundColor: "transparent",
-                            color: colors.text.secondary,
-                            border: `1px solid ${colors.border}`,
-                          }}
-                        >
-                          {showAllDocs
-                            ? "Show Less"
-                            : `Show More (${selectedCollection.documents.length - 3} more)`}
-                        </button>
+                        <div className="mt-3">
+                          <Button
+                            variant="outline"
+                            size="sm"
+                            fullWidth
+                            onClick={() => setShowAllDocs(!showAllDocs)}
+                          >
+                            {showAllDocs
+                              ? "Show Less"
+                              : `Show More (${selectedCollection.documents.length - 3} more)`}
+                          </Button>
+                        </div>
                       )}
                     </div>
                   ) : (
-                    <p
-                      className="text-sm text-center py-8"
-                      style={{ color: colors.text.secondary }}
-                    >
+                    <p className="text-sm text-center py-8 text-text-secondary">
                       No documents in this collection
                     </p>
                   )}
                 </div>
               </>
             ) : (
-              <div
-                className="flex-1 flex items-center justify-center"
-                style={{ color: colors.text.secondary }}
-              >
+              <div className="flex-1 flex items-center justify-center text-text-secondary">
                 Select a knowledge base to view details
               </div>
             )}
@@ -1474,55 +1215,36 @@ export default function KnowledgeBasePage() {
         </div>
       </div>
 
-      {showConfirmDelete && (
-        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
-          <div
-            className="rounded-lg p-6 w-full max-w-md"
-            style={{ backgroundColor: colors.bg.primary }}
-          >
-            <h2
-              className="text-xl font-semibold mb-4"
-              style={{ color: colors.text.primary }}
-            >
-              Delete Collection
-            </h2>
-            <p
-              className="text-sm mb-6"
-              style={{ color: colors.text.secondary }}
+      <Modal
+        open={showConfirmDelete}
+        onClose={() => {
+          setShowConfirmDelete(false);
+          setCollectionToDelete(null);
+        }}
+        title="Delete Collection"
+        maxWidth="max-w-md"
+      >
+        <div className="px-6 pb-6">
+          <p className="text-sm text-text-secondary mb-6">
+            Are you sure you want to delete this collection? This action cannot
+            be undone.
+          </p>
+          <div className="flex justify-end gap-3">
+            <Button
+              variant="outline"
+              onClick={() => {
+                setShowConfirmDelete(false);
+                setCollectionToDelete(null);
+              }}
             >
-              Are you sure you want to delete this collection? This action
-              cannot be undone.
-            </p>
-            <div className="flex justify-end gap-3">
-              <button
-                onClick={() => {
-                  setShowConfirmDelete(false);
-                  setCollectionToDelete(null);
-                }}
-                className="px-4 py-2 rounded-md text-sm font-medium"
-                style={{
-                  backgroundColor: "transparent",
-                  color: colors.text.secondary,
-                  border: `1px solid ${colors.border}`,
-                }}
-              >
-                Cancel
-              </button>
-              <button
-                onClick={handleConfirmDelete}
-                className="px-4 py-2 rounded-md text-sm font-medium"
-                style={{
-                  backgroundColor: "#ef4444",
-                  color: "#ffffff",
-                  border: "1px solid #ef4444",
-                }}
-              >
-                Delete
-              </button>
-            </div>
+              Cancel
+            </Button>
+            <Button variant="danger" onClick={handleConfirmDelete}>
+              Delete
+            </Button>
           </div>
         </div>
-      )}
+      </Modal>
 
       <Modal
         open={showDocumentPicker}
@@ -1532,10 +1254,7 @@ export default function KnowledgeBasePage() {
         <div className="p-6">
           <div className="mb-4">
             {availableDocuments.length === 0 ? (
-              <div
-                className="p-8 text-center text-sm"
-                style={{ color: colors.text.secondary }}
-              >
+              <div className="p-8 text-center text-sm text-text-secondary">
                 No documents available. Please upload documents first.
               </div>
             ) : (
@@ -1543,26 +1262,11 @@ export default function KnowledgeBasePage() {
                 {availableDocuments.map((doc) => (
                   <label
                     key={doc.id}
-                    className="flex items-center p-4 rounded-md border cursor-pointer transition-colors"
-                    style={{
-                      backgroundColor: selectedDocuments.has(doc.id)
-                        ? colors.bg.secondary
-                        : "transparent",
-                      borderColor: selectedDocuments.has(doc.id)
-                        ? colors.border
-                        : "transparent",
-                    }}
-                    onMouseEnter={(e) => {
-                      if (!selectedDocuments.has(doc.id)) {
-                        e.currentTarget.style.backgroundColor =
-                          colors.bg.secondary;
-                      }
-                    }}
-                    onMouseLeave={(e) => {
-                      if (!selectedDocuments.has(doc.id)) {
-                        e.currentTarget.style.backgroundColor = "transparent";
-                      }
-                    }}
+                    className={`flex items-center p-4 rounded-md border cursor-pointer transition-colors ${
+                      selectedDocuments.has(doc.id)
+                        ? "bg-neutral-50 border-border"
+                        : "border-transparent hover:bg-neutral-50"
+                    }`}
                   >
                     <input
                       type="checkbox"
@@ -1571,16 +1275,10 @@ export default function KnowledgeBasePage() {
                       className="mr-3 w-4 h-4"
                     />
                     <div className="flex-1 min-w-0">
-                      <p
-                        className="text-sm font-medium truncate"
-                        style={{ color: colors.text.primary }}
-                      >
+                      <p className="text-sm font-medium text-text-primary truncate">
                         {doc.fname}
                       </p>
-                      <p
-                        className="text-xs mt-1"
-                        style={{ color: colors.text.secondary }}
-                      >
+                      <p className="text-xs mt-1 text-text-secondary">
                         ID: {doc.id.substring(0, 8)}...
                       </p>
                     </div>
@@ -1591,132 +1289,122 @@ export default function KnowledgeBasePage() {
           </div>
 
           {/* Selected Count and Actions */}
-          <div
-            className="flex items-center justify-between pt-4 border-t"
-            style={{ borderColor: colors.border }}
-          >
-            <p className="text-sm" style={{ color: colors.text.secondary }}>
+          <div className="flex items-center justify-between pt-4 border-t border-border">
+            <p className="text-sm text-text-secondary">
               {selectedDocuments.size} document
               {selectedDocuments.size !== 1 ? "s" : ""} selected
             </p>
             <div className="flex gap-3">
-              <button
+              <Button
+                variant="outline"
                 onClick={() => setShowDocumentPicker(false)}
-                className="px-4 py-2 rounded-md text-sm font-medium"
-                style={{
-                  backgroundColor: "transparent",
-                  color: colors.text.secondary,
-                  border: `1px solid ${colors.border}`,
-                }}
               >
                 Cancel
-              </button>
-              <button
+              </Button>
+              <Button
                 onClick={() => setShowDocumentPicker(false)}
                 disabled={selectedDocuments.size === 0}
-                className="px-4 py-2 rounded-md text-sm font-medium"
-                style={{
-                  backgroundColor: colors.bg.secondary,
-                  color: colors.text.primary,
-                  border: `1px solid ${colors.border}`,
-                  opacity: selectedDocuments.size === 0 ? 0.5 : 1,
-                }}
               >
                 Done
-              </button>
+              </Button>
             </div>
           </div>
         </div>
       </Modal>
 
-      <Modal
-        open={showDocPreviewModal && !!selectedCollection?.documents}
-        onClose={() => {
-          setShowDocPreviewModal(false);
-          setPreviewDoc(null);
-        }}
-        title="Document Preview"
-        maxWidth="max-w-5xl"
-        maxHeight="h-[80vh]"
-      >
-        {/* Two-pane body */}
-        <div className="flex flex-1 overflow-hidden">
-          {/* Left pane — doc list */}
+      {showDocPreviewModal && !!selectedCollection?.documents && (
+        <div
+          className="fixed inset-0 bg-black/30 backdrop-blur-[2px] flex items-center justify-center z-50"
+          onClick={() => {
+            setShowDocPreviewModal(false);
+            setPreviewDoc(null);
+          }}
+        >
           <div
-            className="w-1/5 flex flex-col overflow-y-auto shrink-0"
-            style={{ borderRight: `1px solid ${colors.border}` }}
+            className="bg-white rounded-2xl shadow-xl border border-border w-full max-w-5xl h-[85vh] flex flex-col"
+            onClick={(e) => e.stopPropagation()}
           >
-            {selectedCollection?.documents?.map((doc: Document) => (
+            <div className="flex items-center justify-between px-6 py-4 shrink-0 border-b border-border">
+              <h2 className="text-lg font-semibold text-text-primary">
+                Document Preview
+              </h2>
               <button
-                key={doc.id}
-                onClick={async () => {
-                  setPreviewDoc(doc);
-
-                  if (apiKey) {
-                    try {
-                      const data = await apiFetch<
-                        DocumentDetailResponse & Document
-                      >(`/api/document/${doc.id}`, apiKey?.key ?? "");
-                      const documentDetails = (data.data || data) as Document;
-                      setPreviewDoc(documentDetails);
-                    } catch (err) {
-                      console.error(
-                        "Failed to fetch document details for preview:",
-                        err,
-                      );
-                    }
-                  }
-                }}
-                className="text-left px-4 py-3 shrink-0 transition-colors"
-                style={{
-                  backgroundColor:
-                    previewDoc?.id === doc.id
-                      ? colors.bg.secondary
-                      : "transparent",
-                  borderBottom: `1px solid ${colors.border}`,
+                onClick={() => {
+                  setShowDocPreviewModal(false);
+                  setPreviewDoc(null);
                 }}
+                className="p-1 rounded-md text-text-secondary transition-colors hover:bg-neutral-100 hover:text-text-primary cursor-pointer"
               >
-                <p
-                  className="text-sm truncate"
-                  style={{ color: colors.text.primary }}
-                >
-                  {doc.fname}
-                </p>
-                {doc.inserted_at && (
-                  <p
-                    className="text-xs mt-0.5"
-                    style={{ color: colors.text.secondary }}
-                  >
-                    {formatDate(doc.inserted_at)}
-                  </p>
-                )}
+                <CloseIcon className="w-5 h-5" />
               </button>
-            ))}
-          </div>
+            </div>
 
-          {/* Right pane — preview content */}
-          <div
-            className="flex-1 overflow-hidden flex items-center justify-center"
-            style={{ backgroundColor: colors.bg.secondary }}
-          >
-            {previewDoc?.signed_url ? (
-              <iframe
-                key={previewDoc.id}
-                src={previewDoc.signed_url}
-                title={previewDoc.fname}
-                className="w-full h-full"
-                style={{ border: "none" }}
-              />
-            ) : (
-              <p className="text-sm" style={{ color: colors.text.secondary }}>
-                {previewDoc
-                  ? "No preview available for this document"
-                  : "Select a document to preview"}
-              </p>
-            )}
+            {/* Two-pane body */}
+            <div className="flex flex-1 min-h-0">
+              {/* Left pane — doc list */}
+              <div className="w-56 shrink-0 border-r border-border overflow-y-auto">
+                {selectedCollection?.documents?.map((doc: Document) => (
+                  <button
+                    key={doc.id}
+                    onClick={async () => {
+                      setPreviewDoc(doc);
+                      if (isAuthenticated) {
+                        try {
+                          const data = await apiFetch<
+                            DocumentDetailResponse & Document
+                          >(`/api/document/${doc.id}`, apiKey?.key ?? "");
+                          const documentDetails = (data.data ||
+                            data) as Document;
+                          setPreviewDoc(documentDetails);
+                        } catch (err) {
+                          console.error(
+                            "Failed to fetch document details:",
+                            err,
+                          );
+                        }
+                      }
+                    }}
+                    className={`w-full text-left px-4 py-3 border-b border-border transition-colors ${
+                      previewDoc?.id === doc.id
+                        ? "bg-neutral-100"
+                        : "hover:bg-neutral-50"
+                    }`}
+                  >
+                    <p className="text-sm text-text-primary truncate">
+                      {doc.fname}
+                    </p>
+                    {doc.inserted_at && (
+                      <p className="text-xs text-text-secondary mt-0.5">
+                        {formatDate(doc.inserted_at)}
+                      </p>
+                    )}
+                  </button>
+                ))}
+              </div>
+
+              {/* Right pane — preview */}
+              <div className="flex-1 min-h-0 bg-neutral-50">
+                {previewDoc?.signed_url ? (
+                  <iframe
+                    key={previewDoc.id}
+                    src={previewDoc.signed_url}
+                    title={previewDoc.fname}
+                    className="w-full h-full border-none"
+                  />
+                ) : (
+                  <div className="flex items-center justify-center h-full">
+                    <p className="text-sm text-text-secondary">
+                      {previewDoc
+                        ? "No preview available for this document"
+                        : "Select a document to preview"}
+                    </p>
+                  </div>
+                )}
+              </div>
+            </div>
           </div>
         </div>
-      </Modal>
+      )}
     </div>
   );
 }
diff --git a/app/components/document/DocumentListing.tsx b/app/components/document/DocumentListing.tsx
index edec9b1..dd7643f 100644
--- a/app/components/document/DocumentListing.tsx
+++ b/app/components/document/DocumentListing.tsx
@@ -1,7 +1,7 @@
 "use client";
 
 import { formatDate } from "@/app/components/utils";
-import { Document } from "@/app/(main)/document/page";
+import { Document } from "@/app/lib/types/document";
 import { useAuth } from "@/app/lib/context/AuthContext";
 import {
   RefreshIcon,
diff --git a/app/components/document/DocumentPreview.tsx b/app/components/document/DocumentPreview.tsx
index 96d8ff8..8fae576 100644
--- a/app/components/document/DocumentPreview.tsx
+++ b/app/components/document/DocumentPreview.tsx
@@ -3,7 +3,7 @@
 import { useState, useEffect } from "react";
 import Image from "next/image";
 import { formatDate } from "@/app/components/utils";
-import { Document } from "@/app/(main)/document/page";
+import { Document } from "@/app/lib/types/document";
 import {
   RefreshIcon,
   DocumentFileIcon,
diff --git a/app/lib/types/document.ts b/app/lib/types/document.ts
new file mode 100644
index 0000000..03b40b0
--- /dev/null
+++ b/app/lib/types/document.ts
@@ -0,0 +1,9 @@
+export interface Document {
+  id: string;
+  fname: string;
+  object_store_url: string;
+  signed_url?: string;
+  file_size?: number;
+  inserted_at?: string;
+  updated_at?: string;
+}
diff --git a/app/lib/types/knowledgeBase.ts b/app/lib/types/knowledgeBase.ts
new file mode 100644
index 0000000..81810f5
--- /dev/null
+++ b/app/lib/types/knowledgeBase.ts
@@ -0,0 +1,39 @@
+import { Document } from "./document";
+
+export interface Collection {
+  id: string;
+  name?: string;
+  description?: string;
+  knowledge_base_id?: string;
+  inserted_at: string;
+  updated_at: string;
+  status?: string;
+  job_id?: string;
+  documents?: Document[];
+}
+
+export interface JobStatusData {
+  status?: string | null;
+  collection?: { id?: string; knowledge_base_id?: string };
+  collection_id?: string | null;
+}
+
+export interface CollectionResponse {
+  data?: Collection[] | Collection;
+}
+
+export interface DocumentResponse {
+  data?: Document[];
+}
+
+export interface CreateCollectionResponse {
+  data?: { job_id?: string };
+}
+
+export interface DeleteCollectionResponse {
+  data?: { job_id?: string };
+}
+
+export interface DocumentDetailResponse {
+  data?: Document;
+}

From 1b6598b7d417cae23cb6a4ff62d8cb0ec6720fc7 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Thu, 2 Apr 2026 20:50:05 +0530
Subject: [PATCH 09/10] Merge branch 'feat/admin-flow' of
 https://github.com/ProjectTech4DevAI/kaapi-frontend into
 feat/google-integration

---
 app/components/settings/onboarding/ProjectList.tsx | 2 +-
 app/components/settings/onboarding/UserList.tsx    | 7 +++++--
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/app/components/settings/onboarding/ProjectList.tsx b/app/components/settings/onboarding/ProjectList.tsx
index fced0fa..6b47d8c 100644
--- a/app/components/settings/onboarding/ProjectList.tsx
+++ b/app/components/settings/onboarding/ProjectList.tsx
@@ -51,7 +51,7 @@ export default function ProjectList({
     <div>
       <button
         onClick={onBack}
-        className="text-sm text-text-secondary hover:text-text-primary mb-6 flex items-center gap-1 transition-colors"
+        className="text-sm text-text-secondary hover:text-text-primary mb-4 flex items-center gap-1 transition-colors cursor-pointer"
       >
         <ArrowLeftIcon className="w-3.5 h-3.5" /> Back to organizations
       </button>
diff --git a/app/components/settings/onboarding/UserList.tsx b/app/components/settings/onboarding/UserList.tsx
index 52c617c..3bffccc 100644
--- a/app/components/settings/onboarding/UserList.tsx
+++ b/app/components/settings/onboarding/UserList.tsx
@@ -91,9 +91,12 @@ export default function UserList({
 
   return (
     <div>
-      <Button variant="ghost" size="sm" onClick={onBack}>
+      <button
+        onClick={onBack}
+        className="text-sm text-text-secondary hover:text-text-primary mb-4 flex items-center gap-1 transition-colors cursor-pointer"
+      >
         <ArrowLeftIcon className="w-3.5 h-3.5" /> Back to projects
-      </Button>
+      </button>
 
       <div className="flex items-center justify-between mb-4">
         <div>

From 525f850d4c19431ec48655d1dbcd81c2dbb43760 Mon Sep 17 00:00:00 2001
From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com>
Date: Thu, 2 Apr 2026 21:28:38 +0530
Subject: [PATCH 10/10] fix(*): remove the js comment

---
 app/api/auth/google/route.ts  | 10 +---------
 app/components/PageHeader.tsx |  5 -----
 2 files changed, 1 insertion(+), 14 deletions(-)

diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts
index 5da816d..7dc39ad 100644
--- a/app/api/auth/google/route.ts
+++ b/app/api/auth/google/route.ts
@@ -1,15 +1,7 @@
 import { NextResponse } from "next/server";
 import { apiClient } from "@/app/lib/apiClient";
 
-/**
- * POST /api/auth/google
- * Receives the Google credential token from the frontend (@react-oauth/google)
- * and sends it to the backend for verification and user creation/login.
- *
- * Request body: { token: string }
- * Backend sets a Set-Cookie with the access_token (HttpOnly).
- * We forward that cookie to the browser.
- */
+/** Proxy Google login token to backend. Forwards Set-Cookie headers back to the browser. */
 export async function POST(request: Request) {
   try {
     const body = await request.json();
diff --git a/app/components/PageHeader.tsx b/app/components/PageHeader.tsx
index 1f997e4..1b56118 100644
--- a/app/components/PageHeader.tsx
+++ b/app/components/PageHeader.tsx
@@ -9,15 +9,10 @@ import { LoginModal } from "@/app/components/auth";
 import { Button } from "@/app/components";
 
 interface PageHeaderProps {
-  /** Page title displayed next to the sidebar toggle. Ignored when `children` is provided. */
   title?: string;
-  /** Subtitle shown below the title. Ignored when `children` is provided. */
   subtitle?: string;
-  /** Custom left-side content that replaces the default title/subtitle. */
   children?: ReactNode;
-  /** Right-side action buttons / controls. */
   actions?: ReactNode;
-  /** When true the header is not rendered at all. */
   hidden?: boolean;
 }
 
