Skip to main content

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

projectId
Id<'projects'>
required
Reference to the parent project
parentId
Id<'files'>
Reference to parent folder. undefined for root-level files/folders
name
string
required
File or folder name (e.g., index.tsx, components)
type
union
required
File system entry typePossible values:
  • file - Regular file
  • folder - Directory/folder
content
string
Text file content. Only present for text files (type === 'file'). Binary files use storageId instead.
storageId
Id<'_storage'>
Convex storage reference for binary files (images, PDFs, etc.). Mutually exclusive with content.
updatedAt
number
required
Unix timestamp (milliseconds) when file was last modified

Indexes

by_project
index
Query all files in a projectFields: [projectId]Use case: Fetch entire project file tree
by_parent
index
Query children of a specific folderFields: [parentId]Use case: List files in a directory
by_project_parent
index
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:
  1. If folder, recursively deletes all children first
  2. Deletes storage files if storageId exists
  3. Deletes the file/folder document
  4. 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();

Build docs developers (and LLMs) love