Skip to main content

findDuplicateCard

Checks if a card with the given URL already exists for the authenticated user. Useful for preventing duplicate links and showing existing cards before creating new ones.
This query only finds non-deleted cards. Deleted cards in the trash are not considered duplicates.

Parameters

url
string
required
The URL to check for duplicates

Returns

card
Card | null
The existing card if found, or null if no duplicate exists

Usage

Check Before Creating

import { useQuery, useMutation } from "convex/react";
import { api } from "@teak/convex";

function SaveLinkButton({ url }: { url: string }) {
  const existingCard = useQuery(api.card.findDuplicateCard, { url });
  const createCard = useMutation(api.card.createCard);
  
  const handleSave = async () => {
    if (existingCard) {
      // Show existing card instead of creating duplicate
      alert("This link is already saved!");
      return;
    }
    
    await createCard({ content: url, type: "link" });
  };
  
  return (
    <button onClick={handleSave}>
      {existingCard ? "Already Saved" : "Save Link"}
    </button>
  );
}

Prevent Duplicates in Browser Extension

// Browser extension background script
import { ConvexHttpClient } from "convex/browser";

const client = new ConvexHttpClient(CONVEX_URL);

async function saveCurrentTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const url = tab.url;
  
  // Check for duplicates
  const duplicate = await client.query(api.card.findDuplicateCard, { url });
  
  if (duplicate) {
    chrome.notifications.create({
      type: "basic",
      title: "Already Saved",
      message: `This page was saved on ${new Date(duplicate.createdAt).toLocaleDateString()}`
    });
    return;
  }
  
  // Create new card
  await client.mutation(api.card.createCard, {
    content: url,
    type: "link"
  });
}

Implementation Details

Index Usage

The query uses the by_user_url_deleted index for fast lookups:
.withIndex("by_user_url_deleted", (q) =>
  q.eq("userId", userId)
   .eq("url", url)
   .eq("isDeleted", undefined)
)
This ensures O(log n) performance even with large card collections.

URL Matching

  • Exact URL match only (case-sensitive)
  • Does not normalize URLs (e.g., http:// vs https://)
  • Does not match URL fragments or query parameters
Normalize URLs on the client side before checking for duplicates if you want to treat https://example.com and http://example.com as the same URL.

File URLs

If the duplicate card has associated files, the response includes signed URLs that expire after 1 hour. Always call findDuplicateCard again when displaying the card to get fresh URLs.

Error Handling

Unauthenticated

If the user is not authenticated, the query returns null instead of throwing an error.
// Unauthenticated users always get null
const duplicate = await client.query(api.card.findDuplicateCard, { url });
// duplicate === null

Create Card

Create a new card after checking for duplicates

Get Cards

Query existing cards with filters

Build docs developers (and LLMs) love