Skip to main content

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>;
}
teamId
Id<'teams'>
required
The team to create the project in
name
string
required
Project name
description
string
Project description (optional)
projectId
Id<'projects'>
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>
  );
}
teamId
Id<'teams'>
required
The team to list projects from
projects
array
Array of project objects with video counts
_id
Id<'projects'>
Project ID
teamId
Id<'teams'>
Parent team ID
name
string
Project name
description
string | undefined
Project description
videoCount
number
Number 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>
  );
}
teamSlug
string
Filter by team slug (optional)
targets
array
Array of upload target objects (excludes projects where user has viewer role)
projectId
Id<'projects'>
Project ID
projectName
string
Project name
teamId
Id<'teams'>
Team ID
teamName
string
Team name
teamSlug
string
Team slug
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>
  );
}
projectId
Id<'projects'>
required
The project to retrieve
project
object
Project object with user’s role
role
'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>
  );
}
projectId
Id<'projects'>
required
The project to update
name
string
New project name (optional)
description
string
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>;
}
projectId
Id<'projects'>
required
The project to delete
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.

Build docs developers (and LLMs) love