Skip to main content

Overview

The projects table stores user projects with ownership tracking and import/export status management. Each project belongs to a single owner and can contain multiple files.

Schema

name
string
required
Project display name
ownerId
string
required
Stack Auth user ID of the project owner (migrated from Clerk)
userId
Id<'users'>
Reference to the users table for subscription tracking and ownership validation
updatedAt
number
required
Unix timestamp (milliseconds) when project was last modified
importStatus
union
Current import operation statusPossible values:
  • importing - Import in progress
  • completed - Import finished successfully
  • failed - Import failed with error
exportStatus
union
Current export operation statusPossible values:
  • exporting - Export in progress
  • completed - Export finished successfully
  • failed - Export failed with error
  • cancelled - Export cancelled by user
exportRepoUrl
string
GitHub repository URL when project is exported to GitHub

Indexes

by_owner
index
Query projects by owner’s Stack Auth user IDFields: [ownerId]Use case: List all projects for a user
by_user
index
Query projects by Convex user document IDFields: [userId]Use case: Subscription validation and project limit enforcement

Operations

All project operations are defined in convex/projects.ts.

Create Project

Create a new project for the authenticated user.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const createProject = useMutation(api.projects.create);

const projectId = await createProject({ name: 'My App' });
Behavior:
  • Automatically creates user record if it doesn’t exist
  • Checks project limit based on subscription tier
  • Throws error if limit reached (free users: 10 projects)
  • Sets updatedAt to current timestamp
Error messages:
// Project limit reached
"Project limit reached. You have 10 free projects. Please upgrade to Pro for unlimited projects."

// User creation failed
"Failed to create user record. Please try again."

Get Projects

Retrieve all projects for the authenticated user.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const projects = useQuery(api.projects.get);
// Returns: Project[] (ordered by updatedAt desc)

Get Partial Projects

Retrieve limited number of recent projects.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const recentProjects = useQuery(api.projects.getPartial, { limit: 5 });
// Returns: Project[] (most recent 5, ordered desc)

Get Project by ID

Retrieve a specific project with ownership validation.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const project = useQuery(api.projects.getById, { 
  id: projectId as Id<'projects'> 
});
Authorization:
  • Verifies user authentication
  • Checks project.ownerId === identity.subject
  • Throws “Unauthorized access to this project” if ownership check fails

Rename Project

Update project name.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const renameProject = useMutation(api.projects.rename);

await renameProject({ 
  id: projectId as Id<'projects'>, 
  name: 'New Name' 
});
Behavior:
  • Updates updatedAt timestamp
  • Requires ownership validation

Delete Project

Permanently delete a project and all associated data.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const deleteProject = useMutation(api.projects.deleteProject);

await deleteProject({ id: projectId as Id<'projects'> });
Cascade deletion:
  1. All files in files table (including storage files)
  2. All conversations in conversations table
  3. All messages in messages table
  4. All generation events in generationEvents table
  5. The project itself
Warning: This operation is irreversible!

Get Generation Events

Retrieve AI code generation events for a project.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const events = useQuery(api.projects.getGenerationEvents, {
  projectId: projectId as Id<'projects'>,
  limit: 100 // optional, defaults to 200
});

// Returns events in chronological order (oldest to newest)
Event types:
  • step - Generation step completed
  • file - File created/modified
  • info - Informational message
  • error - Error occurred

Example Workflows

Create project with limit check

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

function CreateProjectButton() {
  const createProject = useMutation(api.projects.create);
  const subscription = useQuery(api.users.getSubscription);
  const projectCount = useQuery(api.users.getProjectCount);
  
  const canCreate = subscription?.canCreateProject && 
    (subscription.projectLimit === -1 || 
     projectCount < subscription.projectLimit);
  
  const handleCreate = async () => {
    if (!canCreate) {
      alert('Upgrade to Pro for unlimited projects');
      return;
    }
    
    try {
      const id = await createProject({ name: 'New Project' });
      console.log('Created project:', id);
    } catch (error) {
      console.error('Failed to create project:', error);
    }
  };
  
  return (
    <button onClick={handleCreate} disabled={!canCreate}>
      Create Project
    </button>
  );
}

Safe project deletion

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

function DeleteProjectButton({ projectId }: { projectId: Id<'projects'> }) {
  const deleteProject = useMutation(api.projects.deleteProject);
  
  const handleDelete = async () => {
    if (!confirm('Delete this project? This cannot be undone.')) {
      return;
    }
    
    try {
      await deleteProject({ id: projectId });
      console.log('Project deleted');
    } catch (error) {
      console.error('Failed to delete project:', error);
    }
  };
  
  return <button onClick={handleDelete}>Delete</button>;
}

Export status tracking

import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

function ExportStatus({ projectId }: { projectId: Id<'projects'> }) {
  const project = useQuery(api.projects.getById, { id: projectId });
  
  if (!project) return null;
  
  switch (project.exportStatus) {
    case 'exporting':
      return <div>Exporting to GitHub...</div>;
    case 'completed':
      return (
        <div>
          Exported to <a href={project.exportRepoUrl}>GitHub</a>
        </div>
      );
    case 'failed':
      return <div>Export failed</div>;
    case 'cancelled':
      return <div>Export cancelled</div>;
    default:
      return <button>Export to GitHub</button>;
  }
}

Relationships

Projects → Users

// Get user from project
const project = await ctx.db.get(projectId);
const user = project.userId ? await ctx.db.get(project.userId) : null;

Projects → Files

// Get all files in project
const files = await ctx.db
  .query('files')
  .withIndex('by_project', (q) => q.eq('projectId', projectId))
  .collect();

Projects → Conversations

// Get all conversations in project
const conversations = await ctx.db
  .query('conversations')
  .withIndex('by_project', (q) => q.eq('projectId', projectId))
  .collect();

Best Practices

Always verify project ownership before performing mutations. All operations in convex/projects.ts include this check.
Use the by_owner index for efficient user project queries. It’s optimized for listing projects in descending order by updatedAt.
The ownerId field stores the Stack Auth user ID (string), while userId references the Convex users table document. Both are maintained for subscription tracking.

Build docs developers (and LLMs) love