From c71acbe8aed04052a94454ce5aaacf27bca80e03 Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:32:00 +0000 Subject: [PATCH] Make integrations page public for guests (#1073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Allow logged-out users to load `/?view=integrations` without being redirected to login - Add a public integrations header with Supermemory branding and a login CTA - Run IntegrationsView in guest mode with static catalog data only, no auth-backed queries - Route all guest Connect actions to login with a redirect back to the integrations page Screenshot 2026-06-09 at 9 32 33 PM --- apps/web/app/(app)/page.tsx | 7 ++- apps/web/components/ensure-workspace.tsx | 11 ++-- apps/web/components/header.tsx | 36 ++++++++++- apps/web/components/integrations-view.tsx | 74 +++++++++++++++++++---- apps/web/middleware.ts | 5 ++ 5 files changed, 113 insertions(+), 20 deletions(-) diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 82948b2a..cd41476a 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -582,6 +582,7 @@ export default function NewPage() { viewMode === "dashboard" || (viewMode === "graph" && isMobile) const isGraphMode = viewMode === "graph" const showBottomNav = isMobile && !!session && !isChatView + const isPublicIntegrations = !session && viewMode === "integrations" return ( @@ -608,7 +609,9 @@ export default function NewPage() { /> )} - {!session && viewMode === "mcp" ? ( + {isPublicIntegrations ? ( + + ) : !session && viewMode === "mcp" ? ( ) : (
) : viewMode === "integrations" ? (
- +
) : viewMode === "mcp" ? ( { - if (isMcpPublicPage) return + if (isGuestPublicAppPage) return if (isRestoring) return if (!session) { router.replace( @@ -44,11 +47,11 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) { isRestoring, isOnboarding, router, - isMcpPublicPage, + isGuestPublicAppPage, ]) const showLoading = - !isMcpPublicPage && + !isGuestPublicAppPage && (isRestoring || (!session && !isRestoring) || (session && organizations === null) || diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index cc3afb71..e47f56c2 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -504,7 +504,41 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) { ) } -export function PublicHeader() { +export function PublicHeader({ + variant = "default", +}: { + variant?: "default" | "integrations" +}) { + if (variant === "integrations") { + return ( +
+ + +

+ supermemory +

+ + + + + +
+ ) + } + return (
(null) const [connectingProvider, setConnectingProvider] = @@ -1493,6 +1498,7 @@ export function IntegrationsView() { if (!res.ok) throw new Error("Failed to fetch plugins") return (await res.json()) as { plugins: string[] } }, + enabled: !publicMode, queryKey: ["plugins"], }) @@ -1507,7 +1513,7 @@ export function IntegrationsView() { return response.data as Connection[] }, staleTime: 30 * 1000, - enabled: hasProProduct, + enabled: !publicMode && hasProProduct, }) const { data: apiKeys = [], refetch: refetchKeys } = useQuery( @@ -1524,7 +1530,7 @@ export function IntegrationsView() { const data = (await res.json()) as { keys?: ListedApiKey[] } return data.keys ?? [] }, - enabled: !!org?.id, + enabled: !publicMode && !!org?.id, staleTime: 30 * 1000, }, ) @@ -1678,7 +1684,15 @@ export function IntegrationsView() { } } - const availablePluginIds = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG) + const redirectToLogin = useCallback(() => { + const loginUrl = new URL("/login", window.location.origin) + loginUrl.searchParams.set("redirect", window.location.href) + window.location.assign(loginUrl.toString()) + }, []) + + const availablePluginIds = publicMode + ? Object.keys(PLUGIN_CATALOG) + : (pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)) const enabledPluginIds = new Set( availablePluginIds.filter((id) => PLUGIN_CATALOG[id]), ) @@ -1714,6 +1728,7 @@ export function IntegrationsView() { const isItemConnected = useCallback( (item: Item): boolean => { + if (publicMode) return false if (item.kind === "plugin") { return activePluginById.has(item.pluginId) } @@ -1722,7 +1737,7 @@ export function IntegrationsView() { } return false }, - [activePluginById, connectionsByProvider], + [activePluginById, connectionsByProvider, publicMode], ) const counts = useMemo>( @@ -1780,6 +1795,10 @@ export function IntegrationsView() { ), ctaLabel: "Connect", onCta: () => { + if (publicMode) { + redirectToLogin() + return + } window.open(POKE_RECIPE_URL, "_blank", "noopener,noreferrer") }, }, @@ -1802,6 +1821,10 @@ export function IntegrationsView() { docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction", ctaLabel: "Connect", onCta: () => { + if (publicMode) { + redirectToLogin() + return + } void setMcpClient(null) setViewMode("mcp") }, @@ -1831,12 +1854,18 @@ export function IntegrationsView() { /> ), docsUrl: "https://supermemory.ai/docs/integrations/claude-code", - ctaLabel: claudeCodeConnected - ? "Active" - : claudeCodeNeedsPro - ? "Upgrade" - : "Connect", + ctaLabel: publicMode + ? "Connect" + : claudeCodeConnected + ? "Active" + : claudeCodeNeedsPro + ? "Upgrade" + : "Connect", onCta: () => { + if (publicMode) { + redirectToLogin() + return + } if (claudeCodeConnected) return if (claudeCodeNeedsPro) { handleUpgrade() @@ -1855,6 +1884,10 @@ export function IntegrationsView() { backdrop: , ctaLabel: "Connect", onCta: () => { + if (publicMode) { + redirectToLogin() + return + } window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer") analytics.onboardingChromeExtensionClicked({ source: "integrations" }) }, @@ -1885,6 +1918,19 @@ export function IntegrationsView() { }) const renderRight = (item: Item): ReactNode => { + if (publicMode) { + return ( + { + trackCard(item) + redirectToLogin() + }} + > + Connect + + ) + } + switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) @@ -2065,6 +2111,8 @@ export function IntegrationsView() { } const renderStatus = (item: Item): ReactNode => { + if (publicMode) return null + switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 4715c571..2094e5aa 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -31,6 +31,11 @@ export default async function proxy(request: Request) { return NextResponse.next() } + // Integrations index is public in guest mode; actions still require login. + if (url.pathname === "/" && url.searchParams.get("view") === "integrations") { + return NextResponse.next() + } + if (url.pathname.startsWith("/api/")) { if (!sessionCookie) { console.debug("[MIDDLEWARE] API route without session, returning 401")