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>;
}
Number of days until link expires (optional, no expiration if not provided)
Whether to allow video downloads (optional, defaults to false)
Password to protect the link (optional)
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>
);
}
The video to list share links for
Array of share link objectsName of user who created the link
Expiration timestamp (undefined if never expires)
Whether downloads are allowed
Number of times the link has been accessed
Whether the link is password protected
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>;
}
The share link token to check
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>
);
}
Password if the link is password-protected (optional)
Whether access was granted
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>;
}
New expiration (optional, null to remove expiration)
Whether to allow downloads (optional)
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>;
}
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:
- Check link status with
getByToken() to determine if password is required
- Issue access grant with
issueAccessGrant() (with password if needed)
- 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
Complete Share Link Manager
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>
);
}
Share Link Access Page
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>
);
}
Advanced Share Link Form
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