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
The URL to check for duplicates
Returns
The existing card if found, or null if no duplicate exists Card type (text, link, image, etc.)
Associated URL if this is a link card
Signed URL for the file (expires after 1 hour)
Signed URL for the thumbnail (expires after 1 hour)
Link preview and other metadata
Creation timestamp (Unix ms)
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