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
Stack Auth user ID of the project owner (migrated from Clerk)
Reference to the users table for subscription tracking and ownership validation
Unix timestamp (milliseconds) when project was last modified
Current import operation statusPossible values:
importing - Import in progress
completed - Import finished successfully
failed - Import failed with error
Current export operation statusPossible values:
exporting - Export in progress
completed - Export finished successfully
failed - Export failed with error
cancelled - Export cancelled by user
GitHub repository URL when project is exported to GitHub
Indexes
Query projects by owner’s Stack Auth user IDFields: [ownerId]Use case: List all projects for a user
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:
- All files in
files table (including storage files)
- All conversations in
conversations table
- All messages in
messages table
- All generation events in
generationEvents table
- 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.