Overview
The Projects API manages project structures within teams. Projects organize videos and inherit permissions from their parent team.
All project functions are defined in convex/projects.ts.
Functions
create
Creates a new project within a team.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function CreateProjectButton({ teamId }) {
const createProject = useMutation(api.projects.create);
const handleCreate = async () => {
const projectId = await createProject({
teamId,
name: 'New Project',
description: 'Project description',
});
console.log('Created project:', projectId);
};
return <button onClick={handleCreate}>Create Project</button>;
}
The team to create the project in
Project description (optional)
The ID of the created project
Permissions: Requires member role in the team. Team must have an active subscription.
Implementation: convex/projects.ts:6
list
Lists all projects in a team with video counts.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectsList({ teamId }) {
const projects = useQuery(api.projects.list, { teamId });
return (
<div>
{projects?.map(project => (
<div key={project._id}>
<h3>{project.name}</h3>
<p>{project.description}</p>
<p>{project.videoCount} videos</p>
</div>
))}
</div>
);
}
The team to list projects from
Array of project objects with video countsNumber of videos in the project
Permissions: Requires team membership.
Implementation: convex/projects.ts:24
listUploadTargets
Lists all projects the user can upload to, optionally filtered by team slug.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function UploadDestinationPicker({ teamSlug }) {
const targets = useQuery(api.projects.listUploadTargets, { teamSlug });
const [selected, setSelected] = useState(null);
return (
<select value={selected} onChange={(e) => setSelected(e.target.value)}>
<option value="">Select destination...</option>
{targets?.map(target => (
<option key={target.projectId} value={target.projectId}>
{target.teamName} / {target.projectName}
</option>
))}
</select>
);
}
Filter by team slug (optional)
Array of upload target objects (excludes projects where user has viewer role)role
'member' | 'admin' | 'owner'
User’s role (viewers excluded)
Permissions: Requires authentication. Only returns projects where user has upload permissions (member or higher).
Implementation: convex/projects.ts:52
get
Retrieves a single project by ID.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectDetail({ projectId }) {
const project = useQuery(api.projects.get, { projectId });
return (
<div>
<h1>{project?.name}</h1>
<p>{project?.description}</p>
<p>Your role: {project?.role}</p>
</div>
);
}
Project object with user’s rolerole
'owner' | 'admin' | 'member' | 'viewer'
User’s role in the project’s team
Permissions: Requires access to the project (through team membership).
Implementation: convex/projects.ts:100
update
Updates project name and/or description.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function EditProjectForm({ projectId }) {
const updateProject = useMutation(api.projects.update);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = async () => {
await updateProject({ projectId, name, description });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<textarea value={description} onChange={(e) => setDescription(e.target.value)} />
<button type="submit">Save</button>
</form>
);
}
New project name (optional)
New project description (optional)
Permissions: Requires member role in the project’s team.
Implementation: convex/projects.ts:108
remove
Deletes a project and all associated data (videos, comments, share links).
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function DeleteProjectButton({ projectId }) {
const removeProject = useMutation(api.projects.remove);
const handleDelete = async () => {
if (confirm('Delete this project and all its videos? This cannot be undone!')) {
await removeProject({ projectId });
}
};
return <button onClick={handleDelete}>Delete Project</button>;
}
Permissions: Requires admin role in the project’s team.
Implementation: convex/projects.ts:125
Usage Examples
Project Selector with Video Counts
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectSelector({ teamId, onSelect }) {
const projects = useQuery(api.projects.list, { teamId });
return (
<div className="project-selector">
<h3>Select a project</h3>
{projects?.map(project => (
<button
key={project._id}
onClick={() => onSelect(project._id)}
className="project-option"
>
<div>
<h4>{project.name}</h4>
{project.description && <p>{project.description}</p>}
</div>
<span className="video-count">{project.videoCount} videos</span>
</button>
))}
</div>
);
}
Project Dashboard
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectDashboard({ projectId }) {
const project = useQuery(api.projects.get, { projectId });
const videos = useQuery(api.videos.list, { projectId });
const updateProject = useMutation(api.projects.update);
const removeProject = useMutation(api.projects.remove);
const canEdit = project?.role && ['member', 'admin', 'owner'].includes(project.role);
const canDelete = project?.role && ['admin', 'owner'].includes(project.role);
return (
<div>
<header>
<h1>{project?.name}</h1>
<p>{project?.description}</p>
{canEdit && <button onClick={() => setEditing(true)}>Edit</button>}
{canDelete && (
<button onClick={() => removeProject({ projectId })}>Delete</button>
)}
</header>
<div className="stats">
<div>Total Videos: {videos?.length || 0}</div>
<div>In Review: {videos?.filter(v => v.workflowStatus === 'review').length}</div>
<div>Completed: {videos?.filter(v => v.workflowStatus === 'done').length}</div>
</div>
<VideoGrid videos={videos} />
</div>
);
}
Multi-Team Upload Destination
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function UniversalUploader() {
const targets = useQuery(api.projects.listUploadTargets);
const createVideo = useMutation(api.videos.create);
const [selectedProject, setSelectedProject] = useState(null);
const handleUpload = async (file: File) => {
if (!selectedProject) {
alert('Please select a project');
return;
}
await createVideo({
projectId: selectedProject,
title: file.name,
fileSize: file.size,
contentType: file.type,
});
};
return (
<div>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
>
<option value="">Choose destination...</option>
{targets?.map(target => (
<option key={target.projectId} value={target.projectId}>
{target.teamName} → {target.projectName}
</option>
))}
</select>
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
</div>
);
}
Create Project with Redirect
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
import { useRouter } from 'next/navigation';
function CreateProjectForm({ teamId }) {
const createProject = useMutation(api.projects.create);
const router = useRouter();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const projectId = await createProject({ teamId, name, description });
router.push(`/project/${projectId}`);
} catch (error) {
if (error.message.includes('active subscription')) {
alert('Your team needs an active subscription to create projects');
} else {
alert('Failed to create project: ' + error.message);
}
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Project name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit">Create Project</button>
</form>
);
}
Project Organization
Projects inherit all permissions from their parent team:
- Team viewers can view projects and videos
- Team members can create and edit videos in projects
- Team admins can create, edit, and delete projects
- Team owners have full control
Projects are automatically deleted when their parent team is deleted.