Overview
The files table implements a hierarchical file system for projects. It supports both text files (stored inline) and binary files (stored in Convex storage). Files and folders are organized in a parent-child tree structure.
Schema
Reference to the parent project
Reference to parent folder. undefined for root-level files/folders
File or folder name (e.g., index.tsx, components)
File system entry typePossible values:
file - Regular file
folder - Directory/folder
Text file content. Only present for text files (type === 'file'). Binary files use storageId instead.
Convex storage reference for binary files (images, PDFs, etc.). Mutually exclusive with content.
Unix timestamp (milliseconds) when file was last modified
Indexes
Query all files in a projectFields: [projectId]Use case: Fetch entire project file tree
Query children of a specific folderFields: [parentId]Use case: List files in a directory
Composite index for efficient folder content queriesFields: [projectId, parentId]Use case: Get folder contents with single query
Operations
All file operations are defined in convex/files.ts.
Get All Files
Retrieve all files in a project.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
const files = useQuery(api.files.getFiles, {
projectId: projectId as Id<'projects'>
});
// Returns: Doc<'files'>[] (all files in project)
Get Single File
Retrieve a specific file by ID.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
const file = useQuery(api.files.getFile, {
id: fileId as Id<'files'>
});
Authorization:
- Verifies project ownership
- Throws error if unauthorized
Get File Path
Build full path to a file by traversing parent chain.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
const path = useQuery(api.files.getFilePath, {
id: fileId as Id<'files'>
});
// Returns: [{ _id, name: 'src' }, { _id, name: 'components' }, { _id, name: 'button.tsx' }]
// Use case: Breadcrumbs navigation (src > components > button.tsx)
Get Folder Contents
Retrieve files and subfolders in a directory.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
// Root-level files
const rootFiles = useQuery(api.files.getFolderContents, {
projectId: projectId as Id<'projects'>,
parentId: undefined
});
// Specific folder
const folderContents = useQuery(api.files.getFolderContents, {
projectId: projectId as Id<'projects'>,
parentId: folderId as Id<'files'>
});
Sorting:
- Folders listed first
- Files listed second
- Alphabetical within each group
Create File
Create a new text file.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
const createFile = useMutation(api.files.createFile);
await createFile({
projectId: projectId as Id<'projects'>,
parentId: folderId as Id<'files'> | undefined,
name: 'index.tsx',
content: 'export default function Home() { return <div>Hello</div>; }'
});
Validation:
- Checks for duplicate file names in same folder
- Throws “File already exists” if duplicate found
- Updates project
updatedAt timestamp
Create Folder
Create a new folder.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
const createFolder = useMutation(api.files.createFolder);
await createFolder({
projectId: projectId as Id<'projects'>,
parentId: folderId as Id<'files'> | undefined,
name: 'components'
});
Validation:
- Checks for duplicate folder names in same parent
- Throws “Folder already exists” if duplicate found
Rename File/Folder
Rename a file or folder.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
const renameFile = useMutation(api.files.renameFile);
await renameFile({
id: fileId as Id<'files'>,
newName: 'new-name.tsx'
});
Validation:
- Checks for name conflicts in same folder
- Throws error if new name already exists
- Updates
updatedAt timestamp
Update File Content
Modify text file content.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
const updateFile = useMutation(api.files.updateFile);
await updateFile({
id: fileId as Id<'files'>,
content: 'export default function Updated() { return <div>New</div>; }'
});
Behavior:
- Updates file
updatedAt timestamp
- Updates project
updatedAt timestamp
- Only works for text files (not binary storage files)
Delete File/Folder
Delete a file or folder (recursive for folders).
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
const deleteFile = useMutation(api.files.deleteFile);
await deleteFile({ id: fileId as Id<'files'> });
Recursive deletion:
- If folder, recursively deletes all children first
- Deletes storage files if
storageId exists
- Deletes the file/folder document
- Updates project
updatedAt timestamp
Warning: This operation is irreversible!
Example Workflows
Build file tree UI
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
function FileTree({ projectId, parentId }: {
projectId: Id<'projects'>;
parentId?: Id<'files'>
}) {
const contents = useQuery(api.files.getFolderContents, {
projectId,
parentId
});
if (!contents) return <div>Loading...</div>;
return (
<ul>
{contents.map(item => (
<li key={item._id}>
{item.type === 'folder' ? '📁' : '📄'} {item.name}
{item.type === 'folder' && (
<FileTree projectId={projectId} parentId={item._id} />
)}
</li>
))}
</ul>
);
}
Create nested file structure
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';
async function createProjectStructure(projectId: Id<'projects'>) {
const createFolder = useMutation(api.files.createFolder);
const createFile = useMutation(api.files.createFile);
// Create src folder
const srcId = await createFolder({
projectId,
name: 'src'
});
// Create components folder inside src
const componentsId = await createFolder({
projectId,
parentId: srcId,
name: 'components'
});
// Create button.tsx in components
await createFile({
projectId,
parentId: componentsId,
name: 'button.tsx',
content: 'export default function Button() { return <button>Click</button>; }'
});
}
Display breadcrumb navigation
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';
function Breadcrumbs({ fileId }: { fileId: Id<'files'> }) {
const path = useQuery(api.files.getFilePath, { id: fileId });
if (!path) return null;
return (
<nav>
{path.map((segment, i) => (
<span key={segment._id}>
{i > 0 && ' > '}
<a href={`#${segment._id}`}>{segment.name}</a>
</span>
))}
</nav>
);
}
Safe file deletion with confirmation
import { api } from '@/convex/_generated/api';
import { useMutation, useQuery } from 'convex/react';
function DeleteFileButton({ fileId }: { fileId: Id<'files'> }) {
const deleteFile = useMutation(api.files.deleteFile);
const file = useQuery(api.files.getFile, { id: fileId });
const handleDelete = async () => {
if (!file) return;
const message = file.type === 'folder'
? `Delete folder "${file.name}" and all its contents?`
: `Delete file "${file.name}"?`;
if (!confirm(message)) return;
try {
await deleteFile({ id: fileId });
} catch (error) {
console.error('Failed to delete:', error);
}
};
return <button onClick={handleDelete}>Delete</button>;
}
File System Patterns
Text vs Binary Files
// Text file (stored inline)
{
type: 'file',
name: 'app.tsx',
content: 'export default function App() { ... }',
storageId: undefined
}
// Binary file (stored in Convex storage)
{
type: 'file',
name: 'logo.png',
content: undefined,
storageId: storage_abc123
}
Folder Structure
project/
├── src/ (parentId: undefined)
│ ├── app.tsx (parentId: src._id)
│ └── lib/ (parentId: src._id)
│ └── utils.ts (parentId: lib._id)
└── README.md (parentId: undefined)
Best Practices
Always use recursive deletion for folders. The deleteFile mutation handles this automatically.
Use the by_project_parent composite index for efficient folder content queries. It’s faster than filtering by both fields separately.
Files with storageId should not have content, and vice versa. They are mutually exclusive storage mechanisms.
Relationships
Files → Projects
// Get project from file
const file = await ctx.db.get(fileId);
const project = await ctx.db.get(file.projectId);
Files → Files (Parent-Child)
// Get parent folder
const file = await ctx.db.get(fileId);
const parent = file.parentId ? await ctx.db.get(file.parentId) : null;
// Get children
const children = await ctx.db
.query('files')
.withIndex('by_project_parent', (q) =>
q.eq('projectId', projectId).eq('parentId', fileId)
)
.collect();