feat: delete connection without removing the documents (#778)

- Deleting Connection without removing documents
- Updating the Docs with new param
This commit is contained in:
MaheshtheDev 2026-03-17 23:21:36 +00:00
parent 6ce7357ffb
commit c534008001
8 changed files with 421 additions and 64 deletions

View file

@ -448,7 +448,7 @@ Deleting a GitHub connection will:
- Stop all future syncs from configured repositories
- Remove all webhooks from the repositories
- Revoke the OAuth authorization
- **Permanently delete all synced documents** from your Supermemory knowledge base
- **Permanently delete all synced documents** from your Supermemory knowledge base (unless you pass `deleteDocuments=false` as a query parameter to keep them)
</Warning>
### Manual Sync

View file

@ -287,6 +287,16 @@ curl -X POST "https://api.supermemory.ai/v3/connections/list" \
### Delete Connections
The `DELETE /v3/connections/:connectionId` endpoint accepts an optional `deleteDocuments` query parameter:
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `deleteDocuments` | boolean | `true` | When `true`, all documents imported by the connection are permanently deleted. When `false`, the connection is removed but documents are kept. |
<Note>
Setting `deleteDocuments=false` is useful when you want to disconnect an integration without losing the memories that were already imported.
</Note>
<CodeGroup>
```typescript Typescript
@ -296,9 +306,14 @@ const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY!
});
// Delete by connection ID
// Delete connection and all imported documents (default)
const result = await client.connections.deleteByID(connectionId);
// Delete connection but keep imported documents
const result = await client.connections.deleteByID(connectionId, {
deleteDocuments: false
});
// Or delete by provider (requires container tags)
const result = await client.connections.deleteByProvider('notion', {
containerTags: ['user-123']
@ -314,9 +329,12 @@ import os
client = Supermemory(api_key=os.environ.get("SUPERMEMORY_API_KEY"))
# Delete by connection ID
# Delete connection and all imported documents (default)
result = client.connections.delete_by_id(connection_id)
# Delete connection but keep imported documents
result = client.connections.delete_by_id(connection_id, delete_documents=False)
# Or delete by provider (requires container tags)
result = client.connections.delete_by_provider(
provider='notion',
@ -328,9 +346,14 @@ print(f"Deleted: {result.id} {result.provider}")
```
```bash cURL
# Delete connection and all imported documents (default)
curl -X DELETE "https://api.supermemory.ai/v3/connections/conn_abc123" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY"
# Delete connection but keep imported documents
curl -X DELETE "https://api.supermemory.ai/v3/connections/conn_abc123?deleteDocuments=false" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY"
# Response: {
# "id": "conn_abc123",
# "provider": "notion"

View file

@ -153,7 +153,7 @@ The regex must contain a named capture group `(?<userId>...)` and be less than 2
</Tabs>
<Warning>
Deleting a connection removes all synced documents from Supermemory.
By default, deleting a connection removes all synced documents from Supermemory. To keep documents, pass `deleteDocuments=false` as a query parameter: `DELETE /v3/connections/:id?deleteDocuments=false`
</Warning>
### Manual Sync

View file

@ -13,6 +13,7 @@ import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
type Connection = z.infer<typeof ConnectionResponseSchema>
@ -54,6 +55,10 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
const [isUpgrading, setIsUpgrading] = useState(false)
const [removeDialog, setRemoveDialog] = useState<{
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
// Check Pro status
useEffect(() => {
@ -159,15 +164,26 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
},
})
// Disconnect mutation
const deleteConnectionMutation = useMutation({
mutationFn: async (connectionId: string) => {
await $fetch(`@delete/connections/${connectionId}`)
mutationFn: async ({
connectionId,
deleteDocuments,
}: {
connectionId: string
deleteDocuments: boolean
}) => {
await $fetch(`@delete/connections/${connectionId}`, {
query: { deleteDocuments },
})
return { deleteDocuments }
},
onSuccess: () => {
onSuccess: (_data, variables) => {
toast.success(
"Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.",
variables.deleteDocuments
? "Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes."
: "Connection removed. Your memories have been kept.",
)
setRemoveDialog({ open: false, connection: null })
queryClient.invalidateQueries({ queryKey: ["connections"] })
},
onError: (error) => {
@ -182,8 +198,8 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
addConnectionMutation.mutate(provider)
}
const handleDisconnect = (connectionId: string) => {
deleteConnectionMutation.mutate(connectionId)
const handleDisconnect = (connection: Connection) => {
setRemoveDialog({ open: true, connection })
}
const hasConnections = connections.length > 0
@ -278,7 +294,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
<Button
variant="ghost"
size="sm"
onClick={() => handleDisconnect(connection.id)}
onClick={() => handleDisconnect(connection)}
disabled={deleteConnectionMutation.isPending}
className="text-[#737373] hover:text-white hover:bg-[#1B1F24] h-8 w-8 p-0"
>
@ -351,7 +367,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
<Button
variant="ghost"
size="sm"
onClick={() => handleDisconnect(connection.id)}
onClick={() => handleDisconnect(connection)}
disabled={deleteConnectionMutation.isPending}
className="text-[#737373] hover:text-white hover:bg-[#1B1F24] h-8 w-8 p-0 shrink-0"
>
@ -428,6 +444,25 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
)}
</div>
)}
<RemoveConnectionDialog
open={removeDialog.open}
onOpenChange={(open) => {
if (!open) setRemoveDialog({ open: false, connection: null })
}}
provider={removeDialog.connection?.provider}
documentCount={
(removeDialog.connection?.metadata?.documentCount as number) ?? 0
}
onConfirm={(deleteDocuments) => {
if (removeDialog.connection) {
deleteConnectionMutation.mutate({
connectionId: removeDialog.connection.id,
deleteDocuments,
})
}
}}
isDeleting={deleteConnectionMutation.isPending}
/>
</div>
)
}

View file

@ -8,12 +8,14 @@ import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Check, Plus, Trash2, Zap } from "lucide-react"
import { useEffect } from "react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { useQueryState } from "nuqs"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { analytics } from "@/lib/analytics"
import { AddDocumentModal } from "@/components/add-document"
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
import { addDocumentParam } from "@/lib/search-params"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import type { Project } from "@lib/types"
@ -168,6 +170,11 @@ function ConnectionRow({
export function ConnectionsDetail() {
const queryClient = useQueryClient()
const autumn = useCustomer()
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
const [removeDialog, setRemoveDialog] = useState<{
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
const [, setAddDoc] = useQueryState("add", addDocumentParam)
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
@ -216,14 +223,26 @@ export function ConnectionsDetail() {
}, [connectionsError])
const deleteConnectionMutation = useMutation({
mutationFn: async (connectionId: string) => {
await $fetch(`@delete/connections/${connectionId}`)
mutationFn: async ({
connectionId,
deleteDocuments,
}: {
connectionId: string
deleteDocuments: boolean
}) => {
await $fetch(`@delete/connections/${connectionId}`, {
query: { deleteDocuments },
})
return { deleteDocuments }
},
onSuccess: () => {
onSuccess: (_data, variables) => {
analytics.connectionDeleted()
toast.success(
"Connection removal has started. Documents will be permanently deleted in the next few minutes.",
variables.deleteDocuments
? "Connection removal has started. Documents will be permanently deleted in the next few minutes."
: "Connection removed. Your memories have been kept.",
)
setRemoveDialog({ open: false, connection: null })
queryClient.invalidateQueries({ queryKey: ["connections"] })
},
onError: (error) => {
@ -331,44 +350,61 @@ export function ConnectionsDetail() {
</span>
</div>
<div className="flex flex-col gap-4">
{isLoadingConnections ? (
<div className="flex items-center justify-center py-8">
<div className="size-6 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
</div>
) : connections.length > 0 ? (
connections.map((connection) => (
<ConnectionRow
key={connection.id}
connection={connection}
onDelete={() => deleteConnectionMutation.mutate(connection.id)}
isDeleting={deleteConnectionMutation.isPending}
disabled={!hasProProduct}
projects={projects}
/>
))
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Zap className="size-6 text-[#737373] mb-2" />
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
No connections yet
</p>
<p
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373]",
)}
>
Connect a service below to import your knowledge
</p>
</div>
)}
</div>
{isLoadingConnections ? (
<div className="flex items-center justify-center py-8">
<div className="size-6 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
</div>
) : connections.length > 0 ? (
connections.map((connection) => (
<ConnectionRow
key={connection.id}
connection={connection}
onDelete={() => setRemoveDialog({ open: true, connection })}
isDeleting={deleteConnectionMutation.isPending}
disabled={!hasProProduct}
projects={projects}
/>
))
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Zap className="size-6 text-[#737373] mb-2" />
<p
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
No connections yet
</p>
<p
className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}
>
Connect a service below to import your knowledge
</p>
</div>
)}
<AddDocumentModal
isOpen={isAddDocumentOpen}
onClose={() => setIsAddDocumentOpen(false)}
/>
<RemoveConnectionDialog
open={removeDialog.open}
onOpenChange={(open) => {
if (!open) setRemoveDialog({ open: false, connection: null })
}}
provider={removeDialog.connection?.provider}
documentCount={
(removeDialog.connection?.metadata?.documentCount as number) ?? 0
}
onConfirm={(deleteDocuments) => {
if (removeDialog.connection) {
deleteConnectionMutation.mutate({
connectionId: removeDialog.connection.id,
deleteDocuments,
})
}
}}
isDeleting={deleteConnectionMutation.isPending}
/>
<button
type="button"

View file

@ -0,0 +1,226 @@
"use client"
import { useState } from "react"
import { cn } from "@lib/utils"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { Loader2, XIcon } from "lucide-react"
import { Button } from "@ui/components/button"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
} from "@repo/ui/components/dialog"
const PROVIDER_LABELS: Record<string, string> = {
"google-drive": "Google Drive",
notion: "Notion",
onedrive: "OneDrive",
gmail: "Gmail",
github: "GitHub",
"web-crawler": "Web Crawler",
s3: "S3",
}
interface RemoveConnectionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
providerName?: string
provider?: string
documentCount?: number
onConfirm: (deleteDocuments: boolean) => void
isDeleting: boolean
}
export function RemoveConnectionDialog({
open,
onOpenChange,
providerName,
provider,
documentCount = 0,
onConfirm,
isDeleting,
}: RemoveConnectionDialogProps) {
const [action, setAction] = useState<"keep" | "delete">("keep")
const displayName =
providerName || (provider ? PROVIDER_LABELS[provider] : "this connection")
const handleConfirm = () => {
onConfirm(action === "delete")
}
return (
<Dialog
open={open}
onOpenChange={(o) => {
if (!isDeleting) {
onOpenChange(o)
if (!o) setAction("keep")
}
}}
>
<DialogContent
className={cn(
"w-[90%]! max-w-[500px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-4 rounded-[22px]",
dmSansClassName(),
)}
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
showCloseButton={false}
>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-start gap-4">
<div className="pl-1 space-y-1 flex-1">
<DialogTitle
className={cn(
"font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Remove connection
</DialogTitle>
<DialogDescription className="text-[#737373] font-medium text-[16px] leading-[1.35]">
What would you like to do with the{" "}
{documentCount > 0 ? (
<>
<span className="text-[#fafafa] font-medium">
{documentCount}
</span>{" "}
memories from{" "}
</>
) : (
<>memories from </>
)}
<span className="text-[#fafafa] font-medium">
{displayName}
</span>
?
</DialogDescription>
</div>
<DialogPrimitive.Close
disabled={isDeleting}
className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
style={{
boxShadow: "inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<XIcon stroke="#737373" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
<div className="space-y-3">
<button
type="button"
onClick={() => setAction("keep")}
className={cn(
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
action === "keep"
? "bg-[#14161A] border border-[rgba(82,89,102,0.3)]"
: "bg-[#14161A]/50 border border-transparent hover:border-[rgba(82,89,102,0.2)]",
)}
style={{
boxShadow:
action === "keep"
? "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08)"
: "none",
}}
>
<div
className={cn(
"w-4 h-4 rounded-full border-2 flex items-center justify-center shrink-0",
action === "keep" ? "border-blue-500" : "border-[#737373]",
)}
>
{action === "keep" && (
<div className="w-2 h-2 rounded-full bg-blue-500" />
)}
</div>
<div className="flex flex-col">
<span className="text-[#fafafa] text-sm font-medium">
Remove connection only
</span>
<span className="text-[#737373] text-xs">
Disconnect the integration but keep all imported memories
</span>
</div>
</button>
<button
type="button"
onClick={() => setAction("delete")}
className={cn(
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
action === "delete"
? "bg-[#14161A] border border-[rgba(220,38,38,0.3)]"
: "bg-[#14161A]/50 border border-transparent hover:border-[rgba(82,89,102,0.2)]",
)}
style={{
boxShadow:
action === "delete"
? "0px 1px 2px 0px rgba(87,0,0,0.1), inset 0px 0px 0px 1px rgba(67,43,43,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08)"
: "none",
}}
>
<div
className={cn(
"w-4 h-4 rounded-full border-2 flex items-center justify-center shrink-0",
action === "delete" ? "border-red-500" : "border-[#737373]",
)}
>
{action === "delete" && (
<div className="w-2 h-2 rounded-full bg-red-500" />
)}
</div>
<div className="flex flex-col">
<span className="text-[#fafafa] text-sm font-medium">
Remove connection and memories
</span>
<span className="text-[#737373] text-xs">
Permanently delete all memories imported from this connection
</span>
</div>
</button>
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<Button
variant="ghost"
disabled={isDeleting}
onClick={() => {
onOpenChange(false)
setAction("keep")
}}
className="text-[#737373] cursor-pointer rounded-full"
>
Cancel
</Button>
<Button
variant="insideOut"
disabled={isDeleting}
onClick={handleConfirm}
className={cn(
action === "delete" &&
"bg-red-600! hover:bg-red-700! text-white",
)}
>
{isDeleting ? (
<>
<Loader2 className="size-4 animate-spin" />
Removing...
</>
) : action === "delete" ? (
"Remove & delete memories"
) : (
"Remove connection"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -16,6 +16,7 @@ import type { z } from "zod"
import { analytics } from "@/lib/analytics"
import { ConnectAIModal } from "@/components/connect-ai-modal"
import { AddDocumentModal } from "@/components/add-document"
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
import { addDocumentParam } from "@/lib/search-params"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import type { Project } from "@lib/types"
@ -337,6 +338,10 @@ export default function ConnectionsMCP() {
const autumn = useCustomer()
const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam)
const [mcpModalOpen, setMcpModalOpen] = useState(false)
const [removeDialog, setRemoveDialog] = useState<{
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
@ -394,16 +399,27 @@ export default function ConnectionsMCP() {
}
}, [connectionsError])
// Delete connection mutation
const deleteConnectionMutation = useMutation({
mutationFn: async (connectionId: string) => {
await $fetch(`@delete/connections/${connectionId}`)
mutationFn: async ({
connectionId,
deleteDocuments,
}: {
connectionId: string
deleteDocuments: boolean
}) => {
await $fetch(`@delete/connections/${connectionId}`, {
query: { deleteDocuments },
})
return { deleteDocuments }
},
onSuccess: () => {
onSuccess: (_data, variables) => {
analytics.connectionDeleted()
toast.success(
"Connection removal has started. Supermemory will permanently delete the documents in the next few minutes.",
variables.deleteDocuments
? "Connection removal has started. Supermemory will permanently delete the documents in the next few minutes."
: "Connection removed. Your memories have been kept.",
)
setRemoveDialog({ open: false, connection: null })
queryClient.invalidateQueries({ queryKey: ["connections"] })
},
onError: (error) => {
@ -480,9 +496,7 @@ export default function ConnectionsMCP() {
<ConnectionRow
key={connection.id}
connection={connection}
onDelete={() =>
deleteConnectionMutation.mutate(connection.id)
}
onDelete={() => setRemoveDialog({ open: true, connection })}
isDeleting={deleteConnectionMutation.isPending}
disabled={!hasProProduct}
projects={projects}
@ -564,6 +578,26 @@ export default function ConnectionsMCP() {
isOpen={addDoc !== null}
onClose={() => setAddDoc(null)}
/>
<RemoveConnectionDialog
open={removeDialog.open}
onOpenChange={(open) => {
if (!open) setRemoveDialog({ open: false, connection: null })
}}
provider={removeDialog.connection?.provider}
documentCount={
(removeDialog.connection?.metadata?.documentCount as number) ?? 0
}
onConfirm={(deleteDocuments) => {
if (removeDialog.connection) {
deleteConnectionMutation.mutate({
connectionId: removeDialog.connection.id,
deleteDocuments,
})
}
}}
isDeleting={deleteConnectionMutation.isPending}
/>
</div>
)
}

View file

@ -119,6 +119,9 @@ export const apiSchema = createSchema({
provider: z.string(),
}),
params: z.object({ connectionId: z.string() }),
query: z.object({
deleteDocuments: z.boolean().optional(),
}),
},
// Settings operations