diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx
deleted file mode 100644
index da1755835..000000000
--- a/surfsense_web/app/auth/callback/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-
-import { Suspense } from "react";
-import TokenHandler from "@/components/TokenHandler";
-
-export default function AuthCallbackPage() {
- // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI
- // TokenHandler uses useGlobalLoadingEffect to show the loading screen
- return (
-
-
-
- );
-}
diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx
index 46182f40e..30aedcce5 100644
--- a/surfsense_web/app/layout.tsx
+++ b/surfsense_web/app/layout.tsx
@@ -5,6 +5,7 @@ import { Roboto } from "next/font/google";
import Script from "next/script";
import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider";
import { DesktopUpdateToast } from "@/components/desktop/desktop-update-toast";
+import { AuthCutoverPurge } from "@/components/providers/AuthCutoverPurge";
import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { PostHogProvider } from "@/components/providers/PostHogProvider";
@@ -164,6 +165,7 @@ export default function RootLayout({
+
{children}
diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx
deleted file mode 100644
index 64e99487e..000000000
--- a/surfsense_web/components/TokenHandler.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
-import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
-import { getAndClearRedirectPath } from "@/lib/auth-utils";
-import { buildBackendUrl } from "@/lib/env-config";
-import { trackLoginSuccess } from "@/lib/posthog/events";
-
-interface TokenHandlerProps {
- redirectPath?: string; // Default path to redirect after storing token (if no saved path)
- tokenParamName?: string; // Deprecated: tokens are no longer read from URLs
-}
-
-/**
- * Client component that finalizes a cookie session after OAuth/local login.
- * After confirming the session, it redirects the user back to the page they were on before
- * being redirected to login (if available), or to the default redirectPath.
- *
- * @param redirectPath - Default path to redirect after storing token (default: '/dashboard')
- * @param tokenParamName - Name of the URL parameter containing the token (default: 'token')
- */
-const TokenHandler = ({
- redirectPath = "/dashboard",
- tokenParamName: _tokenParamName = "token",
-}: TokenHandlerProps) => {
- // Always show loading for this component - spinner animation won't reset
- useGlobalLoadingEffect(true);
-
- useEffect(() => {
- if (typeof window === "undefined") return;
-
- const run = async () => {
- try {
- const sessionResponse = await fetch(buildBackendUrl("/auth/session"), {
- credentials: "include",
- });
- if (!sessionResponse.ok) {
- window.location.href = "/login";
- return;
- }
-
- const alreadyTracked = sessionStorage.getItem("login_success_tracked");
- if (!alreadyTracked) {
- trackLoginSuccess("google");
- }
- sessionStorage.removeItem("login_success_tracked");
-
- // Auto-set active search space in desktop if not already set
- if (window.electronAPI?.getActiveSearchSpace) {
- try {
- const stored = await window.electronAPI.getActiveSearchSpace();
- if (!stored) {
- const spaces = await searchSpacesApiService.getSearchSpaces();
- if (spaces?.length) {
- await window.electronAPI.setActiveSearchSpace?.(String(spaces[0].id));
- }
- }
- } catch {
- // non-critical
- }
- }
-
- const savedRedirectPath = getAndClearRedirectPath();
- const finalRedirectPath = savedRedirectPath || redirectPath;
- window.location.href = finalRedirectPath;
- } catch (error) {
- console.error("Error finalizing session:", error);
- window.location.href = redirectPath;
- }
- };
-
- run();
- }, [redirectPath]);
-
- // Return null - the global provider handles the loading UI
- return null;
-};
-
-export default TokenHandler;
diff --git a/surfsense_web/components/providers/AuthCutoverPurge.tsx b/surfsense_web/components/providers/AuthCutoverPurge.tsx
new file mode 100644
index 000000000..db028cb39
--- /dev/null
+++ b/surfsense_web/components/providers/AuthCutoverPurge.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import { useEffect } from "react";
+
+const CUTOVER_FLAG_KEY = "surfsense_auth_cutover_v1_complete";
+const LEGACY_BEARER_TOKEN_KEY = "surfsense_bearer_token";
+const LEGACY_REFRESH_TOKEN_KEY = "surfsense_refresh_token";
+
+export function AuthCutoverPurge() {
+ useEffect(() => {
+ try {
+ if (localStorage.getItem(CUTOVER_FLAG_KEY) === "true") return;
+ localStorage.removeItem(LEGACY_BEARER_TOKEN_KEY);
+ localStorage.removeItem(LEGACY_REFRESH_TOKEN_KEY);
+ localStorage.setItem(CUTOVER_FLAG_KEY, "true");
+ } catch {
+ // Storage can be unavailable in private mode; cookie auth still works.
+ }
+ }, []);
+
+ return null;
+}