Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/browser-extension/entrypoints/content/t3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,7 @@ async function getRelatedMemoriesForT3(actionSource: string) {
}

if (textareaElement) {
textareaElement.dataset.supermemories =
`\n\nSupermemories of user (only for the reference): ${response.data}`
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`

iconElement.dataset.memoriesData = response.data

Expand Down
1 change: 1 addition & 0 deletions apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
"integrations/agent-framework",
"integrations/mastra",
"integrations/voltagent",
"integrations/convex",
"integrations/langchain",
"integrations/crewai",
"integrations/agno",
Expand Down
208 changes: 208 additions & 0 deletions apps/docs/integrations/convex.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: "Convex"
sidebarTitle: "Convex"
description: "Add persistent memory to Convex apps with Supermemory"
icon: "database"
---

Convex apps don't have built-in memory for AI. Supermemory fixes that. You get a memory layer that stores conversations, builds user profiles, and gives your AI context about who it's talking to.

## What you can do

- Store user interactions and retrieve them in future sessions
- Build automatic user profiles from conversations
- Search memories to give your AI relevant context
- Keep everything in your Convex database for full visibility

## Setup

Install the packages:

```bash
npm install supermemory convex
```

For the AI chat example, also install the AI SDK packages:

```bash
npm install @supermemory/tools @ai-sdk/openai ai
```

Set up your environment variable in Convex:

```bash
npx convex env set SUPERMEMORY_API_KEY your-supermemory-api-key
```

<Note>Get your Supermemory API key from [console.supermemory.ai](https://console.supermemory.ai).</Note>

## Basic integration

Create simple helper functions for each Supermemory operation:

```typescript
// convex/memory.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import Supermemory from "supermemory";

const memory = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY });

// Get user profile and relevant memories
export const getProfile = action({
args: { userId: v.string(), query: v.optional(v.string()) },
handler: async (ctx, { userId, query }) => {
return await memory.profile({
containerTag: userId,
q: query,
});
},
});

// Add a memory
export const addMemory = action({
args: { userId: v.string(), content: v.string() },
handler: async (ctx, { userId, content }) => {
return await memory.add({
content,
containerTag: userId,
});
},
});

// Search memories
export const searchMemories = action({
args: { userId: v.string(), query: v.string(), limit: v.optional(v.number()) },
handler: async (ctx, { userId, query, limit }) => {
return await memory.search.memories({
q: query,
containerTag: userId,
searchMode: "hybrid",
limit: limit ?? 10,
});
},
});
```

---

## Example: AI chat with memory

A chat endpoint using the Supermemory AI SDK middleware. It automatically injects context and saves memories.

```typescript
// convex/chat.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { withSupermemory } from "@supermemory/tools/ai-sdk";

export const chat = action({
args: { userId: v.string(), message: v.string() },
handler: async (ctx, { userId, message }) => {
// Wrap the model - automatically injects context and saves memories
const model = withSupermemory(openai("gpt-4o-mini"), {
containerTag: userId,
customId: `convex-chat-${userId}`,
mode: "full",
addMemory: "always",
});

const { text } = await generateText({
model,
system: "You are a helpful assistant.",
prompt: message,
});

return text;
},
});
```

---

## Storing memories in Convex tables

Keep a local copy of memories in your Convex database for full visibility:

```typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
memories: defineTable({
userId: v.string(),
content: v.string(),
createdAt: v.number(),
}).index("by_user", ["userId"]),
});
```

```typescript
// convex/memory.ts
import { action, mutation, query } from "./_generated/server";
import { api } from "./_generated/api";
import { v } from "convex/values";
import Supermemory from "supermemory";

const memory = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY });

// Store in Convex
export const storeMemory = mutation({
args: { userId: v.string(), content: v.string() },
handler: async (ctx, { userId, content }) => {
return await ctx.db.insert("memories", {
userId,
content,
createdAt: Date.now(),
});
},
});

// Add memory to both Supermemory and Convex
export const addMemory = action({
args: { userId: v.string(), content: v.string() },
handler: async (ctx, { userId, content }) => {
// Add to Supermemory
await memory.add({ content, containerTag: userId });

// Store in Convex
// Note: in production, handle partial failures — if the Convex mutation
// fails after the Supermemory write succeeds, the two stores will be out of sync.
await ctx.runMutation(api.memory.storeMemory, { userId, content });
},
});

// List memories from Convex
export const listMemories = query({
args: { userId: v.string() },
handler: async (ctx, { userId }) => {
return await ctx.db
.query("memories")
.withIndex("by_user", q => q.eq("userId", userId))
.order("desc")
.take(50);
},
});
```

---

## Related docs

<CardGroup cols={2}>
<Card title="User profiles" icon="user" href="/user-profiles">
How automatic profiling works
</Card>
<Card title="Search" icon="search" href="/search">
Filtering and search modes
</Card>
<Card title="Vercel AI SDK" icon="triangle" href="/integrations/ai-sdk">
Memory middleware for Next.js
</Card>
<Card title="LangChain" icon="link" href="/integrations/langchain">
Memory for LangChain apps
</Card>
</CardGroup>
12 changes: 10 additions & 2 deletions apps/web/app/(app)/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
Loader2,
} from "lucide-react"
import { analytics } from "@/lib/analytics"
import { consumePendingConnectUrl } from "@/lib/constants"

type DetectedSource = "x" | "linkedin" | "resume" | null
type Status = "idle" | "processing" | "done" | "error"
Expand Down Expand Up @@ -556,6 +557,12 @@ export default function OnboardingPage() {
const skippingRef = useRef(false)
const [spotlightCategory, setSpotlightCategory] =
useState<SpotlightCategoryId>("productivity")

/** Navigate home, or back to the plugin connect page if one is pending. */
const goHomeOrPendingConnect = useCallback(() => {
const pendingPath = consumePendingConnectUrl()
router.push(pendingPath ?? "/")
}, [router])
const [pauseSpotlight, setPauseSpotlight] = useState(false)

const spotlightCatalog = useMemo(
Expand Down Expand Up @@ -612,7 +619,8 @@ export default function OnboardingPage() {
skippingRef.current = true
try {
await ensureOrg()
router.push("/")
const pendingPath = consumePendingConnectUrl()
router.push(pendingPath ?? "/")
} catch (err) {
console.error(err)
skippingRef.current = false
Expand Down Expand Up @@ -1273,7 +1281,7 @@ export default function OnboardingPage() {
</button>
<button
type="button"
onClick={() => router.push("/")}
onClick={goHomeOrPendingConnect}
className="text-sm text-[#3A4A5E] hover:text-[#6B7A8D] transition-colors cursor-pointer"
>
Go to home
Expand Down
45 changes: 40 additions & 5 deletions apps/web/app/auth/connect/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { dmSans125ClassName } from "@/lib/fonts"
import { useCustomer } from "autumn-js/react"
import { ArrowRight, Loader, XCircle } from "lucide-react"
import Image from "next/image"
import { useSearchParams } from "next/navigation"
import { Suspense, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react"

import { PENDING_CONNECT_URL_KEY } from "@/lib/constants"

const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
Expand Down Expand Up @@ -105,8 +107,9 @@ const cardClass = cn(

function AuthConnectContent() {
const params = useSearchParams()
const router = useRouter()
const { data: session, isPending } = useSession()
const { org } = useAuth()
const { org, organizations, isRestoring } = useAuth()
const autumn = useCustomer()
const [status, setStatus] = useState<Status>("loading")
const [error, setError] = useState<string | null>(null)
Expand All @@ -118,6 +121,29 @@ function AuthConnectContent() {
const displayName = validClient ? getPluginName(validClient) : "External Tool"
const pluginInfo = validClient ? PLUGIN_INFO[validClient] : null

// Redirect new users (logged in but no organization) to onboarding.
// Store the current connect URL so onboarding can redirect back here.
const shouldRedirectToOnboarding =
!isPending &&
!isRestoring &&
!!session &&
Array.isArray(organizations) &&
organizations.length === 0

useEffect(() => {
if (isPending || isRestoring) return
if (!session) return
if (organizations === null) return // orgs query still pending
if (organizations.length > 0) return // has orgs, nothing to do

try {
sessionStorage.setItem(PENDING_CONNECT_URL_KEY, window.location.href)
} catch (e) {
console.warn("Failed to access sessionStorage for pending connect URL", e)
}
router.replace("/onboarding")
}, [isPending, isRestoring, session, organizations, router])

async function handleConnect() {
if (!callback) {
setStatus("error")
Expand All @@ -129,7 +155,13 @@ function AuthConnectContent() {
setError("Invalid callback URL.")
return
}
if (!session || !org) return
if (!session || !org) {
setStatus("error")
setError(
"Your account is not fully set up yet. Please complete onboarding first.",
)
return
}

try {
setStatus("creating")
Expand Down Expand Up @@ -178,7 +210,10 @@ function AuthConnectContent() {
}
}

if (isPending) {
// Show a spinner while session/org data is loading or while we're about
// to redirect to onboarding (prevents a brief flash of the connect card).
const isAuthLoading = isPending || isRestoring || organizations === null
if (isAuthLoading || shouldRedirectToOnboarding) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="size-6 border-2 border-[#4BA0FA] border-t-transparent rounded-full animate-spin" />
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default function RootLayout({
"https://api.supermemory.ai"
}
includeCredentials={true}
headers={{ "X-App-Source": "nova" }}
>
<QueryProvider>
<AuthProvider>
Expand Down
Loading
Loading