Skip to main content

Overview

The Bookmarks API allows authenticated users to save and manage their favorite AI tools. All endpoints require authentication.

Queries

getBookmarks

Fetch all bookmarked tools for the authenticated user. Source: /home/daytona/workspace/source/convex/bookmarks.ts:12 Authentication: Required Parameters: None
tools
Tool[]
Array of bookmarked tools (null entries are filtered out)
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

function BookmarksPage() {
  const bookmarks = useQuery(api.bookmarks.getBookmarks, {});
  
  return (
    <div>
      <h1>My Bookmarks</h1>
      {bookmarks?.map(tool => (
        <ToolCard key={tool._id} tool={tool} />
      ))}
    </div>
  );
}
Notes:
  • Returns the full tool objects, not just bookmark records
  • Automatically filters out any null tools (if a bookmarked tool was deleted)
  • Tools are returned in the order they were bookmarked
Errors:
  • "Unauthenticated" - User is not signed in

isBookmarked

Check if a specific tool is bookmarked by the current user. Source: /home/daytona/workspace/source/convex/bookmarks.ts:56 Authentication: Optional (returns false if not authenticated)
toolId
Id<'tools'>
required
ID of the tool to check
isBookmarked
boolean
true if the tool is bookmarked, false otherwise
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

function ToolPage({ toolId }: { toolId: Id<"tools"> }) {
  const isBookmarked = useQuery(api.bookmarks.isBookmarked, { toolId });
  
  return (
    <button>
      {isBookmarked ? "Remove Bookmark" : "Add Bookmark"}
    </button>
  );
}
Notes:
  • Returns false if user is not authenticated (does not throw error)
  • Useful for showing bookmark button states

Mutations

toggleBookmark

Add or remove a bookmark for a specific tool. Source: /home/daytona/workspace/source/convex/bookmarks.ts:32 Authentication: Required
toolId
Id<'tools'>
required
ID of the tool to bookmark/unbookmark
bookmarked
boolean
true if tool was bookmarked, false if it was unbookmarked
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function BookmarkButton({ toolId }: { toolId: Id<"tools"> }) {
  const toggleBookmark = useMutation(api.bookmarks.toggleBookmark);
  const isBookmarked = useQuery(api.bookmarks.isBookmarked, { toolId });
  
  const handleToggle = async () => {
    const bookmarked = await toggleBookmark({ toolId });
    if (bookmarked) {
      console.log("Tool bookmarked!");
    } else {
      console.log("Bookmark removed");
    }
  };
  
  return (
    <button onClick={handleToggle}>
      {isBookmarked ? "🔖 Bookmarked" : "Add Bookmark"}
    </button>
  );
}
Behavior:
  • If tool is already bookmarked: removes the bookmark and returns false
  • If tool is not bookmarked: creates a bookmark and returns true
  • Uses composite index for efficient lookup
Errors:
  • "Unauthenticated" - User is not signed in

Schema

The bookmarks table has the following structure:
bookmarks: defineTable({
  userId: v.string(),     // Clerk user ID
  toolId: v.id("tools")   // Reference to tools table
})
  .index("by_userId", ["userId"])
  .index("by_toolId", ["toolId"])
  .index("by_userId_and_toolId", ["userId", "toolId"])
Indices:
  • by_userId - Fast lookup of all bookmarks for a user
  • by_toolId - Fast lookup of all users who bookmarked a tool
  • by_userId_and_toolId - Fast lookup of specific bookmark (used by toggleBookmark and isBookmarked)

Example: Complete Bookmark Feature

import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";

function ToolDetailPage({ toolId }: { toolId: Id<"tools"> }) {
  const tool = useQuery(api.tools.getToolById, { toolId });
  const isBookmarked = useQuery(api.bookmarks.isBookmarked, { toolId });
  const toggleBookmark = useMutation(api.bookmarks.toggleBookmark);
  
  const handleBookmark = async () => {
    try {
      await toggleBookmark({ toolId });
    } catch (error) {
      if (error.message === "Unauthenticated") {
        // Redirect to sign in
        window.location.href = "/sign-in";
      }
    }
  };
  
  if (!tool) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>{tool.name}</h1>
      <p>{tool.description}</p>
      <button onClick={handleBookmark}>
        {isBookmarked ? "Remove from Bookmarks" : "Bookmark This Tool"}
      </button>
    </div>
  );
}

function BookmarksPage() {
  const bookmarks = useQuery(api.bookmarks.getBookmarks, {});
  
  return (
    <div>
      <h1>My Bookmarked Tools</h1>
      {bookmarks === undefined ? (
        <div>Loading...</div>
      ) : bookmarks.length === 0 ? (
        <div>No bookmarks yet. Start exploring tools!</div>
      ) : (
        <div className="grid">
          {bookmarks.map(tool => (
            <ToolCard key={tool._id} tool={tool} />
          ))}
        </div>
      )}
    </div>
  );
}

Build docs developers (and LLMs) love