Skip to main content

Overview

ClipSync provides real-time clipboard synchronization across all devices in a session. Content sent from any device instantly appears on all other connected devices using Supabase’s realtime database subscriptions.

Real-Time Synchronization

How It Works

ClipSync uses Supabase’s PostgreSQL realtime capabilities to instantly sync clipboard updates:
src/App.jsx:385-410
useEffect(() => {
    if (!sessionCode) return;
    const channel = supabase
        .channel("clipboard")
        .on("postgres_changes", { 
            event: "*", 
            schema: "public", 
            table: "clipboard" 
        }, (payload) => {
            if (payload.new.session_code === sessionCode && payload.eventType === "INSERT") {
                setHistory((prev) => [payload.new, ...prev]);
                setClipboard("");
            }

            if (payload.eventType === "DELETE") {
                if (deleteOne) {
                    setHistory([]);
                } else {
                    setHistory((prev) => prev.filter((item) => item.id !== payload.old.id));
                }
            }
        })
        .subscribe();

    return () => {
        supabase.removeChannel(channel);
    };
}, [sessionCode, isOffline]);
The realtime subscription only listens for updates matching your current session code, ensuring privacy between sessions.

Sending Content to Clipboard

Text Content

To send text content:
  1. Type or paste content into the textarea
  2. Optionally mark content as sensitive
  3. Click “Send to Clipboard”
src/App.jsx:167-205
const updateClipboard = async () => {
    // if fileURL exists, then it is a file so text can be empty
    if (!clipboard && !fileUrl) return toast.error("Please enter some text to update clipboard");
    if (clipboard.length > 15000) return toast.error("Clipboard content is too long. Please keep it under 15000 characters.");

    let firstTime = false;

    if (!sessionCode) {
        await createSession(setSessionCode);
        firstTime = true;
    }
    const code = localStorage.getItem("sessionCode");
    await supabase.from("clipboard").insert([{
        session_code: code,
        content: clipboard,
        fileUrl: fileUrl ? fileUrl.url : null,
        file: fileUrl ? fileUrl : null,
        sensitive: isSensitive,
    }]);

    if (history.length == 0 && firstTime) {
        // Manually fetch latest history to update UI immediately
        const { data, error: fetchError } = await supabase
            .from("clipboard")
            .select("*")
            .eq("session_code", code)
            .order("created_at", { ascending: false });

        if (!fetchError) {
            setHistory(data);
        }
    }

    setFileUrl(null);
    setClipboard("");
    sessionStorage.removeItem("clipboard");
    setIsSensitive(false);
    toast.success("Clipboard updated successfully!");
};

Paste from System Clipboard

ClipSync can read directly from your system clipboard:
src/App.jsx:212-223
const addClipboardText = () => {
    navigator.clipboard.readText().then((text) => {
        if (text.trim()) {
            setClipboard(text);
            toast.success("Clipboard text pasted successfully!");
        } else {
            alert("Clipboard is empty or contains unsupported data.");
        }
    }).catch(() => {
        alert("An error occurred while reading clipboard");
    });
};
Your browser may prompt for clipboard access permission the first time you use this feature.

Character Limits

ClipSync enforces a 15,000 character limit per clipboard entry to ensure performance and database efficiency. From App.jsx:170:
src/App.jsx:170
if (clipboard.length > 15000) 
    return toast.error("Clipboard content is too long. Please keep it under 15000 characters.");
Content exceeding 15,000 characters will be rejected. Consider splitting large content into multiple entries or using file sharing for longer text.

Copying from History

Each clipboard entry in the history can be copied back to your system clipboard with one click:
src/App.jsx:207-210
const copyToClipboard = (content) => {
    navigator.clipboard.writeText(content);
    toast.success("Text copied to clipboard!");
};
The copy button is available for each history item:
src/App.jsx:679
<button 
    aria-label="Copy Content" 
    className="text-blue-500 active:text-blue-700 active:scale-95" 
    onClick={() => copyToClipboard(item.content)}>
    <Copy size={19} />
</button>

Sensitive Content Masking

ClipSync includes a sensitive content feature to protect private information when sharing your screen or device.

How It Works

  1. Check the “Sensitive” checkbox before sending content
  2. The content is stored normally in the database
  3. When displayed in the history, it’s replaced with asterisks

Implementation

From utils/index.js:1-11:
src/utils/index.js:1-11
export const convertLinksToAnchor = (text, item) => {
    if(item.sensitive) {
        // replace it with ******
        return "**********************";
    }
    const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/g;
    return text.replace(urlRegex, (url) => {
        let hyperlink = url.startsWith("www.") ? `https://${url}` : url;
        return `<a href="${hyperlink}" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">${url}</a>`;
    });
};
UI checkbox from App.jsx:531-536:
src/App.jsx:531-536
<div className="flex absolute items-center gap-1 w-fit right-3 bottom-3 z-10">
    <label htmlFor="is-sensitive">Sensitive</label>
    <input
        checked={isSensitive}
        onChange={(e) => setIsSensitive(e.target.checked)}
        type="checkbox" id="is-sensitive" className="ml-2" />
</div>
Use sensitive mode for passwords, API keys, personal information, or any content you want to keep private from onlookers.
Clipboard content is automatically scanned for URLs, which are converted to clickable links:
src/App.jsx:650-656
<p
    onClick={() => toggleExpand(item.id)}
    className={`text-sm flex-1 word-wrap link-wrap cursor-pointer truncate text-wrap w-fit`}
    dangerouslySetInnerHTML={{
        __html: expandedId === item.id
            ? convertLinksToAnchor(item.content, item)
            : item.content.length > 180
                ? convertLinksToAnchor(item.content.substring(0, 180), item) + "..."
                : convertLinksToAnchor(item.content.substring(0, 180), item)
    }}
></p>

Clipboard History

Viewing History

All clipboard entries are stored and displayed in chronological order (newest first):
src/App.jsx:31-38
const fetchClipboardHistory = async () => {
    if (!sessionCode) return;
    let { data, error } = await supabase
        .from("clipboard")
        .select("*")
        .eq("session_code", sessionCode);
    if (!error) setClipboard(data || []);
};

Searching History

ClipSync includes a search feature to filter clipboard history:
src/App.jsx:412-420
const handleSearch = (e) => {
    setSearchKeyword(e.target.value);
    if (e.target.value.trim() === "") {
        setSearchResults(history);
        return;
    }
    const results = history.filter((item) => 
        item.content.toLowerCase().includes(e.target.value.toLowerCase()));
    setSearchResults(results);
}

Editing History Items

You can edit previously sent content:
src/App.jsx:249-266
const handleEdit = async (id) => {
    setDeleteOne(true);
    // fetch item from database
    const content = history.find((item) => item.id === id).content;
    setClipboard(content);
    // delete item from database
    await supabase.from("clipboard").delete().eq("id", id);

    // set file if exists
    setFileUrl(history.find((item) => item.id === id).file);

    // delete item from history
    const newHistory = history.filter((item) => item.id !== id);
    setHistory(newHistory);
    setDeleteOne(false);

    toast.success("Clipboard content added to editor!");
}
Editing removes the original entry and loads it back into the editor. You’ll need to send it again after making changes.

Clearing History

Delete all clipboard entries from your session:
src/App.jsx:225-243
const deleteAll = async () => {
    const response = confirm("Are you sure you want to clear clipboards?");
    if (!response) return;
    if (history.length == 0) return toast.error("No items in your clipboard history");
    
    // delete all items from database with session code if file exists delete from storage
    history.forEach(async (item) => {
        if (item.file) {
            await supabase.storage.from("clipboard").remove([item.file.name]);
        }
    });

    const { error } = await supabase.from("clipboard").delete().eq("session_code", sessionCode);
    if (error) {
        toast.error("An error occurred while deleting clipboard history");
        return;
    }
    setHistory([]);
    toast.success("Clipboard history deleted successfully!");
};
Deleting history is permanent and affects all devices in the session. Associated files are also removed from storage.

Build docs developers (and LLMs) love