Skip to main content

Overview

The Share Links API enables secure video sharing with optional password protection and expiration dates. Share links generate temporary access grants for authenticated access. All share link functions are defined in convex/shareLinks.ts.

Functions

create

Creates a new share link for a video.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function CreateShareLink({ videoId }) {
  const createLink = useMutation(api.shareLinks.create);
  
  const handleCreate = async () => {
    const { token } = await createLink({
      videoId,
      expiresInDays: 7,
      allowDownload: true,
      password: 'secret123',
    });
    
    const shareUrl = `${window.location.origin}/share/${token}`;
    navigator.clipboard.writeText(shareUrl);
    alert('Link copied to clipboard!');
  };
  
  return <button onClick={handleCreate}>Create Share Link</button>;
}
videoId
Id<'videos'>
required
The video to share
expiresInDays
number
Number of days until link expires (optional, no expiration if not provided)
allowDownload
boolean
Whether to allow video downloads (optional, defaults to false)
password
string
Password to protect the link (optional)
result
object
token
string
The share link token
Permissions: Requires member role in the video’s team. Implementation: convex/shareLinks.ts:84

list

Lists all share links for a video.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function ShareLinksList({ videoId }) {
  const links = useQuery(api.shareLinks.list, { videoId });
  
  return (
    <div>
      {links?.map(link => (
        <div key={link._id}>
          <code>{link.token}</code>
          <span>{link.viewCount} views</span>
          {link.hasPassword && <span>🔒 Password protected</span>}
          {link.isExpired && <span>⚠️ Expired</span>}
          <p>Created by {link.creatorName}</p>
        </div>
      ))}
    </div>
  );
}
videoId
Id<'videos'>
required
The video to list share links for
Array of share link objects
_id
Id<'shareLinks'>
Share link ID
token
string
The share token
createdByName
string
Name of user who created the link
expiresAt
number | undefined
Expiration timestamp (undefined if never expires)
allowDownload
boolean
Whether downloads are allowed
viewCount
number
Number of times the link has been accessed
hasPassword
boolean
Whether the link is password protected
isExpired
boolean
Whether the link has expired
Permissions: Requires access to the video. Implementation: convex/shareLinks.ts:121

getByToken

Checks the status of a share link by token.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function ShareLinkGate({ token }) {
  const status = useQuery(api.shareLinks.getByToken, { token });
  
  if (status?.status === 'missing') return <div>Link not found</div>;
  if (status?.status === 'expired') return <div>Link expired</div>;
  if (status?.status === 'requiresPassword') return <PasswordPrompt token={token} />;
  if (status?.status === 'ok') return <VideoPlayer token={token} />;
  
  return <div>Loading...</div>;
}
token
string
required
The share link token to check
result
object
status
'missing' | 'expired' | 'requiresPassword' | 'ok'
The status of the share link
Permissions: Public - no authentication required. Implementation: convex/shareLinks.ts:205

issueAccessGrant

Issues a temporary access grant for a share link (with optional password).
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function ShareLinkAccess({ token }) {
  const issueGrant = useMutation(api.shareLinks.issueAccessGrant);
  const [password, setPassword] = useState('');
  
  const handleAccess = async () => {
    const result = await issueGrant({ token, password });
    
    if (result.ok && result.grantToken) {
      // Store grant token and access video
      localStorage.setItem('grantToken', result.grantToken);
      window.location.href = `/watch?grant=${result.grantToken}`;
    } else {
      alert('Access denied - check password or rate limit');
    }
  };
  
  return (
    <div>
      <input 
        type="password" 
        value={password} 
        onChange={(e) => setPassword(e.target.value)} 
      />
      <button onClick={handleAccess}>Access Video</button>
    </div>
  );
}
token
string
required
The share link token
password
string
Password if the link is password-protected (optional)
result
object
ok
boolean
Whether access was granted
grantToken
string | null
Temporary access grant token (valid for 24 hours) or null if access denied
Permissions: Public - no authentication required. Rate limited to prevent abuse. Rate Limits:
  • 600 requests per minute globally
  • 120 requests per minute per token
  • 10 password failures per minute per token
  • After 5 failed password attempts, link is locked for 10 minutes
Implementation: convex/shareLinks.ts:234

update

Updates an existing share link’s settings.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function EditShareLink({ linkId }) {
  const updateLink = useMutation(api.shareLinks.update);
  
  const handleUpdate = async () => {
    await updateLink({
      linkId,
      expiresInDays: 30,
      allowDownload: false,
      password: null, // Remove password
    });
  };
  
  return <button onClick={handleUpdate}>Update Link</button>;
}
The share link to update
expiresInDays
number | null
New expiration (optional, null to remove expiration)
allowDownload
boolean
Whether to allow downloads (optional)
password
string | null
New password (optional, null to remove password)
Permissions: Requires member role in the video’s team. Implementation: convex/shareLinks.ts:163

remove

Deletes a share link and all associated access grants.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function DeleteShareLink({ linkId }) {
  const removeLink = useMutation(api.shareLinks.remove);
  
  const handleDelete = async () => {
    if (confirm('Delete this share link?')) {
      await removeLink({ linkId });
    }
  };
  
  return <button onClick={handleDelete}>Delete</button>;
}
The share link to delete
Permissions: Requires member role in the video’s team. Implementation: convex/shareLinks.ts:150

Access Grant Flow

Share links use a two-step access process:
  1. Check link status with getByToken() to determine if password is required
  2. Issue access grant with issueAccessGrant() (with password if needed)
  3. Use grant token to access the video via videos.getByShareGrant() and comments.getThreadedForShareGrant()
Access grants are temporary tokens that expire after 24 hours.

Usage Examples

import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function ShareLinkManager({ videoId }) {
  const links = useQuery(api.shareLinks.list, { videoId });
  const createLink = useMutation(api.shareLinks.create);
  const removeLink = useMutation(api.shareLinks.remove);
  const [showCreate, setShowCreate] = useState(false);
  
  return (
    <div>
      <h2>Share Links</h2>
      
      <button onClick={() => setShowCreate(true)}>Create New Link</button>
      
      {showCreate && (
        <CreateLinkForm 
          videoId={videoId} 
          onCreate={createLink}
          onClose={() => setShowCreate(false)}
        />
      )}
      
      <div className="links-list">
        {links?.map(link => (
          <div key={link._id} className={link.isExpired ? 'expired' : ''}>
            <div className="link-info">
              <code>{link.token}</code>
              <button onClick={() => {
                const url = `${window.location.origin}/share/${link.token}`;
                navigator.clipboard.writeText(url);
              }}>Copy Link</button>
            </div>
            
            <div className="link-stats">
              <span>👁️ {link.viewCount} views</span>
              {link.hasPassword && <span>🔒 Password</span>}
              {link.allowDownload && <span>⬇️ Downloads</span>}
              {link.expiresAt && (
                <span>
                  Expires: {new Date(link.expiresAt).toLocaleDateString()}
                </span>
              )}
            </div>
            
            <div className="link-actions">
              <button onClick={() => removeLink({ linkId: link._id })}>Delete</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
import { useState, useEffect } from 'react';

function SharePage({ token }) {
  const status = useQuery(api.shareLinks.getByToken, { token });
  const issueGrant = useMutation(api.shareLinks.issueAccessGrant);
  const [password, setPassword] = useState('');
  const [grantToken, setGrantToken] = useState(null);
  const [error, setError] = useState('');
  
  // Try to access without password first
  useEffect(() => {
    if (status?.status === 'ok') {
      issueGrant({ token }).then(result => {
        if (result.ok && result.grantToken) {
          setGrantToken(result.grantToken);
        }
      });
    }
  }, [status]);
  
  const handlePasswordSubmit = async (e) => {
    e.preventDefault();
    const result = await issueGrant({ token, password });
    
    if (result.ok && result.grantToken) {
      setGrantToken(result.grantToken);
      setError('');
    } else {
      setError('Incorrect password or too many attempts');
    }
  };
  
  if (status?.status === 'missing') {
    return <div>Share link not found</div>;
  }
  
  if (status?.status === 'expired') {
    return <div>This share link has expired</div>;
  }
  
  if (status?.status === 'requiresPassword' && !grantToken) {
    return (
      <form onSubmit={handlePasswordSubmit}>
        <h2>This video is password protected</h2>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Enter password"
        />
        <button type="submit">Access Video</button>
        {error && <p className="error">{error}</p>}
      </form>
    );
  }
  
  if (grantToken) {
    return <VideoPlayer grantToken={grantToken} />;
  }
  
  return <div>Loading...</div>;
}

Quick Share Button

import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function QuickShareButton({ videoId }) {
  const createLink = useMutation(api.shareLinks.create);
  const [copied, setCopied] = useState(false);
  
  const handleShare = async () => {
    // Create a simple 7-day link
    const { token } = await createLink({
      videoId,
      expiresInDays: 7,
      allowDownload: false,
    });
    
    const url = `${window.location.origin}/share/${token}`;
    await navigator.clipboard.writeText(url);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  
  return (
    <button onClick={handleShare}>
      {copied ? '✓ Copied!' : '🔗 Share'}
    </button>
  );
}
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
import { useState } from 'react';

function AdvancedShareForm({ videoId }) {
  const createLink = useMutation(api.shareLinks.create);
  const [expiresInDays, setExpiresInDays] = useState<number | null>(null);
  const [allowDownload, setAllowDownload] = useState(false);
  const [password, setPassword] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const { token } = await createLink({
      videoId,
      expiresInDays: expiresInDays || undefined,
      allowDownload,
      password: password || undefined,
    });
    
    const url = `${window.location.origin}/share/${token}`;
    alert(`Share link created: ${url}`);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h3>Create Share Link</h3>
      
      <label>
        Expiration:
        <select 
          value={expiresInDays || ''} 
          onChange={(e) => setExpiresInDays(e.target.value ? Number(e.target.value) : null)}
        >
          <option value="">Never expires</option>
          <option value="1">1 day</option>
          <option value="7">7 days</option>
          <option value="30">30 days</option>
          <option value="90">90 days</option>
        </select>
      </label>
      
      <label>
        <input
          type="checkbox"
          checked={allowDownload}
          onChange={(e) => setAllowDownload(e.target.checked)}
        />
        Allow downloads
      </label>
      
      <label>
        Password (optional):
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Leave blank for no password"
        />
      </label>
      
      <button type="submit">Create Link</button>
    </form>
  );
}

Security Features

  • Password hashing - Passwords are hashed using bcrypt
  • Rate limiting - Prevents brute force attacks
  • Temporary grants - Access grants expire after 24 hours
  • Link expiration - Optional expiration dates
  • Failed attempt tracking - Locks link after 5 failed password attempts
  • View counting - Tracks how many times link is accessed

Build docs developers (and LLMs) love