From 19d7f122b214795243971b4f1898592f773a4bd2 Mon Sep 17 00:00:00 2001 From: Gautam Sharma <148205237+GautamSharma99@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:32:24 +0530 Subject: [PATCH 1/2] fix(web): use backend URL fallback for direct fetches (#1212) Co-authored-by: Dhravya Shah Co-authored-by: Claude Fable 5 --- apps/web/app/(auth)/login/page.tsx | 4 +-- apps/web/components/app-experience.tsx | 40 +++++++++++------------- apps/web/components/chat/index.tsx | 17 +++++----- apps/web/hooks/use-document-mutations.ts | 14 ++++----- apps/web/lib/url-helpers.ts | 8 +++++ 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index c7723724..5f9bf8cc 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -18,7 +18,7 @@ import { motion } from "motion/react" import { dmSansClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { Logo } from "@ui/assets/Logo" -import { resolveAuthRedirectUrl } from "@/lib/url-helpers" +import { getBackendUrl, resolveAuthRedirectUrl } from "@/lib/url-helpers" import { Loader2 } from "lucide-react" function isMcpOAuthAuthorizeContext(sp: Pick): boolean { @@ -276,7 +276,7 @@ export default function LoginPage() { const token = formData.get("token") as string const callbackURL = getCallbackURL() router.push( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`, + `${getBackendUrl()}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`, ) } diff --git a/apps/web/components/app-experience.tsx b/apps/web/components/app-experience.tsx index 63daad88..259584ee 100644 --- a/apps/web/components/app-experience.tsx +++ b/apps/web/components/app-experience.tsx @@ -67,6 +67,7 @@ import { } from "@/lib/search-params" import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" import { getToolDocumentSpace } from "@/lib/plugin-space" +import { getBackendUrl } from "@/lib/url-helpers" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -134,6 +135,7 @@ export function AppExperience() { const { viewMode, setViewMode } = useViewMode() useLegacyViewRedirect() const isCompanyBrain = useHasCompanyBrain() + const backendUrl = getBackendUrl() // Slack OAuth redirects back here with ?slack=connected — toast then clean up. useEffect(() => { @@ -331,7 +333,7 @@ export function AppExperience() { queryFn: async (): Promise => { const spaceId = selectedProject || "sm_project_default" const forceRefresh = highlightsForceAt > 0 - const cacheKey = `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights?spaceId=${spaceId}` + const cacheKey = `${backendUrl}/v3/space-highlights?spaceId=${spaceId}` if (!forceRefresh) { const cache = await caches.open(HIGHLIGHTS_CACHE_NAME) @@ -345,22 +347,19 @@ export function AppExperience() { } } - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - spaceId, - highlightsCount: 3, - questionsCount: 4, - includeHighlights: true, - includeQuestions: true, - forceRefresh, - }), - }, - ) + const response = await fetch(`${backendUrl}/v3/space-highlights`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + spaceId, + highlightsCount: 3, + questionsCount: 4, + includeHighlights: true, + includeQuestions: true, + forceRefresh, + }), + }) if (!response.ok) { throw new Error("Failed to fetch space highlights") @@ -404,10 +403,9 @@ export function AppExperience() { if (stored) return JSON.parse(stored) as MemoryOfDay } catch {} - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/memory-of-day`, - { credentials: "include" }, - ) + const response = await fetch(`${backendUrl}/v3/memory-of-day`, { + credentials: "include", + }) if (!response.ok) return null const data = (await response.json()) as MemoryOfDay | null if (data) { diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index aae5920b..c78df7f7 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -1111,7 +1111,7 @@ export function ChatSidebar({ const params = new URLSearchParams({ projectId: chatProject }) if (historyScope === "all") params.set("scope", "all") const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads?${params.toString()}`, + `${chatApiBase}/chat/threads?${params.toString()}`, { credentials: "include" }, ) if (response.ok) { @@ -1123,7 +1123,7 @@ export function ChatSidebar({ } finally { setIsLoadingThreads(false) } - }, [chatProject, historyScope]) + }, [chatApiBase, chatProject, historyScope]) useEffect(() => { if (!isHistoryOpen) return @@ -1134,10 +1134,9 @@ export function ChatSidebar({ const loadThread = useCallback( async (id: string) => { try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads/${id}`, - { credentials: "include" }, - ) + const response = await fetch(`${chatApiBase}/chat/threads/${id}`, { + credentials: "include", + }) if (response.ok) { const data = await response.json() const uiMessages = data.messages.map( @@ -1179,7 +1178,7 @@ export function ChatSidebar({ console.error("Failed to load thread:", error) } }, - [setThreadId], + [chatApiBase, setThreadId], ) // Auto-restore thread from URL on mount (e.g. reload or direct link) @@ -1197,7 +1196,7 @@ export function ChatSidebar({ async (threadId: string) => { try { const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads/${threadId}`, + `${chatApiBase}/chat/threads/${threadId}`, { method: "DELETE", credentials: "include" }, ) if (response.ok) { @@ -1213,7 +1212,7 @@ export function ChatSidebar({ setConfirmingDeleteId(null) } }, - [currentChatId, handleNewChat], + [chatApiBase, currentChatId, handleNewChat], ) const formatRelativeTime = (isoString: string): string => { diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts index b9a08cc6..c0b1c864 100644 --- a/apps/web/hooks/use-document-mutations.ts +++ b/apps/web/hooks/use-document-mutations.ts @@ -10,6 +10,7 @@ import { $fetch } from "@lib/api" import { useAuth } from "@lib/auth-context" import { analytics } from "@/lib/analytics" import { fetchSpaceSettings, spaceSettingsKey } from "@/hooks/use-space-context" +import { getBackendUrl } from "@/lib/url-helpers" /** Pull the human-readable message out of a $fetch error (handles `{error}`/`{message}`/string). */ function fetchErrorMessage(err: unknown, fallback: string): string { @@ -500,14 +501,11 @@ export function useDocumentMutations({ } formData.append("metadata", JSON.stringify({ sm_source: "consumer" })) - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`, - { - method: "POST", - body: formData, - credentials: "include", - }, - ) + const response = await fetch(`${getBackendUrl()}/v3/documents/file`, { + method: "POST", + body: formData, + credentials: "include", + }) if (!response.ok) { let message = "Failed to upload file" diff --git a/apps/web/lib/url-helpers.ts b/apps/web/lib/url-helpers.ts index 84e67b3e..e9f1dbc8 100644 --- a/apps/web/lib/url-helpers.ts +++ b/apps/web/lib/url-helpers.ts @@ -1,7 +1,15 @@ const PROXY_LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]) +const DEFAULT_BACKEND_URL = "https://api.supermemory.ai" const DEV_APP_ORIGIN = "https://app.dev.supermemory.ai" const PROD_APP_ORIGIN = "https://app.supermemory.ai" +export function getBackendUrl(): string { + return (process.env.NEXT_PUBLIC_BACKEND_URL ?? DEFAULT_BACKEND_URL).replace( + /\/+$/, + "", + ) +} + export function getAppOriginForCurrentEnvironment(hostname?: string): string { const currentHostname = hostname ?? (typeof window !== "undefined" ? window.location.hostname : "") From 2cebe81512a56272a453afe08d8c00b6888d2b7b Mon Sep 17 00:00:00 2001 From: Gautam Sharma <148205237+GautamSharma99@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:41:48 +0530 Subject: [PATCH 2/2] fix(web): bypass auth proxy for local dev (#1213) Co-authored-by: Dhravya Shah Co-authored-by: Dhravya Shah Co-authored-by: Claude Fable 5 --- CONTRIBUTING.md | 11 +---------- apps/web/middleware.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e7c29ec..feb1ed41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,16 +36,7 @@ Before you begin, ensure you have the following installed: # You'll need to add your API keys and database URLs ``` -4. **Change proxy for local development** - - Add this in your `proxy.ts`(apps/web) before retrieving the cookie (`getSessionCookie(request)`): - - ```ts - if (url.hostname === "localhost") { - return NextResponse.next(); - } - -5. **Start the Development Server** +4. **Start the Development Server** ```bash bun run dev:local diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index b0e7879f..ea7b1ec1 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -2,6 +2,8 @@ import { getSessionCookie } from "better-auth/cookies" import { NextResponse } from "next/server" import { getPublicRequestUrl } from "@/lib/url-helpers" +const LOCAL_DEV_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]) + function getAuthSessionCookie(request: Request): string | null { return ( getSessionCookie(request) ?? @@ -16,6 +18,18 @@ export default async function proxy(request: Request) { console.debug("[PROXY] Path:", url.pathname) console.debug("[PROXY] Method:", request.method) + // Development builds only: getPublicRequestUrl trusts x-forwarded-host, so + // a hostname check alone could be spoofed in production to skip the /api + // 401 gate below. NODE_ENV is inlined at build time, making this dead code + // in production bundles. + if ( + process.env.NODE_ENV === "development" && + LOCAL_DEV_HOSTS.has(url.hostname) + ) { + console.debug("[PROXY] Local dev host, allowing access") + return NextResponse.next() + } + const sessionCookie = getAuthSessionCookie(request) console.debug("[PROXY] Session cookie exists:", !!sessionCookie)