Overview
Polaris IDE uses Convex as its real-time database and backend. Convex provides instant synchronization, reactive queries, and a type-safe TypeScript API that makes building collaborative features effortless.
Why Convex?
Real-time Sync Changes sync instantly across all connected clients with zero configuration
Optimistic Updates UI updates immediately while mutations run in the background
Type Safety Full TypeScript support with auto-generated types
Serverless No infrastructure to manage - scales automatically
File Storage Built-in file storage for binary files and images
Stack Auth Integration Native JWT authentication with Stack Auth
Setup
Install Convex
Create a New Project
Click “Create a project” and name it (e.g., “polaris-ide”).
Get Deployment URL
Copy your deployment URL from the dashboard (e.g., https://happy-animal-123.convex.cloud).
Add to Environment
Add to .env.local: NEXT_PUBLIC_CONVEX_URL = https://your-deployment.convex.cloud
POLARIS_CONVEX_INTERNAL_KEY = your-random-secret-key
Start Convex Dev Server
Run the Convex development server: This watches for schema changes and deploys automatically.
Environment Variables
# Convex
NEXT_PUBLIC_CONVEX_URL = https://your-deployment.convex.cloud
CONVEX_DEPLOYMENT = your-deployment-name # Auto-set by Convex CLI
POLARIS_CONVEX_INTERNAL_KEY = random-secret-for-internal-api
POLARIS_CONVEX_INTERNAL_KEY is used for internal API mutations from Inngest background jobs. Generate a strong random string.
Database Schema
Convex schema defines all database tables and their structure (convex/schema.ts:7):
Users Table
Stores user profiles and subscription information:
users : defineTable ({
stackUserId: v . string (), // Stack Auth user ID
email: v . string (),
autumnCustomerId: v . optional ( v . string ()), // Billing integration
subscriptionStatus: v . optional (
v . union (
v . literal ( "free" ),
v . literal ( "trialing" ),
v . literal ( "active" ),
v . literal ( "paused" ),
v . literal ( "canceled" ),
v . literal ( "past_due" )
)
),
subscriptionTier: v . optional (
v . union (
v . literal ( "free" ),
v . literal ( "pro_monthly" ),
v . literal ( "pro_yearly" )
)
),
projectLimit: v . number (), // -1 for unlimited
trialEndsAt: v . optional ( v . number ()),
createdAt: v . number (),
updatedAt: v . number (),
})
. index ( "by_stack_user" , [ "stackUserId" ])
. index ( "by_autumn_customer" , [ "autumnCustomerId" ])
Indexes:
by_stack_user - Lookup user by Stack Auth ID
by_autumn_customer - Lookup user by billing customer ID
Projects Table
Stores IDE projects:
projects : defineTable ({
name: v . string (),
ownerId: v . string (), // Stack Auth user ID
userId: v . optional ( v . id ( "users" )), // Link to users table
updatedAt: v . number (),
importStatus: v . optional (
v . union (
v . literal ( "importing" ),
v . literal ( "completed" ),
v . literal ( "failed" )
)
),
exportStatus: v . optional (
v . union (
v . literal ( "exporting" ),
v . literal ( "completed" ),
v . literal ( "failed" ),
v . literal ( "cancelled" )
)
),
exportRepoUrl: v . optional ( v . string ()),
})
. index ( "by_owner" , [ "ownerId" ])
. index ( "by_user" , [ "userId" ])
Indexes:
by_owner - Query projects by owner (Stack Auth user ID)
by_user - Query projects by Convex user record
Files Table
Stores project files and folders:
files : defineTable ({
projectId: v . id ( "projects" ),
parentId: v . optional ( v . id ( "files" )), // Parent folder
name: v . string (),
type: v . union ( v . literal ( "file" ), v . literal ( "folder" )),
content: v . optional ( v . string ()), // Text files only
storageId: v . optional ( v . id ( "_storage" )), // Binary files
updatedAt: v . number (),
})
. index ( "by_project" , [ "projectId" ])
. index ( "by_parent" , [ "parentId" ])
. index ( "by_project_parent" , [ "projectId" , "parentId" ])
Indexes:
by_project - Get all files in a project
by_parent - Get children of a folder
by_project_parent - Efficient folder navigation
Conversations Table
Stores AI conversation metadata:
conversations : defineTable ({
projectId: v . id ( "projects" ),
title: v . string (),
updatedAt: v . number (),
})
. index ( "by_project" , [ "projectId" ])
Messages Table
Stores conversation messages:
messages : defineTable ({
conversationId: v . id ( "conversations" ),
projectId: v . id ( "projects" ),
role: v . union ( v . literal ( "user" ), v . literal ( "assistant" )),
content: v . string (),
status: v . optional (
v . union (
v . literal ( "processing" ),
v . literal ( "completed" ),
v . literal ( "cancelled" ),
v . literal ( "failed" )
)
),
triggerRunId: v . optional ( v . string ()), // Inngest job ID
toolCalls: v . optional (
v . array (
v . object ({
id: v . string (),
name: v . string (),
args: v . any (),
result: v . optional ( v . any ()),
})
)
),
})
. index ( "by_conversation" , [ "conversationId" ])
. index ( "by_project_status" , [ "projectId" , "status" ])
Generation Events Table
Tracks AI generation progress:
generationEvents : defineTable ({
projectId: v . id ( "projects" ),
type: v . union (
v . literal ( "step" ),
v . literal ( "file" ),
v . literal ( "info" ),
v . literal ( "error" )
),
message: v . string (),
filePath: v . optional ( v . string ()),
preview: v . optional ( v . string ()),
createdAt: v . number (),
})
. index ( "by_project_created_at" , [ "projectId" , "createdAt" ])
Queries
Queries read data from the database and automatically re-run when data changes.
Example: Get User Projects
export const get = query ({
args: {},
handler : async ( ctx ) => {
const identity = await verifyAuth ( ctx );
return await ctx . db
. query ( "projects" )
. withIndex ( "by_owner" , ( q ) => q . eq ( "ownerId" , identity . subject ))
. order ( "desc" )
. collect ();
},
});
File: convex/projects.ts:79
Using Queries in React
import { useQuery } from "convex/react" ;
import { api } from "@/convex/_generated/api" ;
function ProjectsList () {
const projects = useQuery ( api . projects . get );
if ( projects === undefined ) {
return < div > Loading ...</ div > ;
}
return (
< ul >
{ projects . map ( project => (
< li key = {project. _id } > {project. name } </ li >
))}
</ ul >
);
}
Parameterized Queries
export const getById = query ({
args: { id: v . id ( "projects" ) },
handler : async ( ctx , args ) => {
const identity = await verifyAuth ( ctx );
const project = await ctx . db . get ( args . id );
if ( ! project ) {
throw new Error ( "Project not found" );
}
if ( project . ownerId !== identity . subject ) {
throw new Error ( "Unauthorized" );
}
return project ;
},
});
Usage:
const project = useQuery ( api . projects . getById , { id: projectId });
Mutations
Mutations modify database state.
Example: Create Project
export const create = mutation ({
args: {
name: v . string (),
},
handler : async ( ctx , args ) => {
const identity = await verifyAuth ( ctx );
const stackUserId = identity . subject ;
// Get or create user
let user = await ctx . db
. query ( "users" )
. withIndex ( "by_stack_user" , ( q ) => q . eq ( "stackUserId" , stackUserId ))
. first ();
if ( ! user ) {
const userId = await ctx . db . insert ( "users" , {
stackUserId ,
email: identity . email || "" ,
subscriptionStatus: "free" ,
subscriptionTier: "free" ,
projectLimit: 10 ,
createdAt: Date . now (),
updatedAt: Date . now (),
});
user = await ctx . db . get ( userId );
}
// Check project limit
if ( user ! . projectLimit !== - 1 ) {
const existingProjects = await ctx . db
. query ( "projects" )
. withIndex ( "by_owner" , ( q ) => q . eq ( "ownerId" , stackUserId ))
. collect ();
if ( existingProjects . length >= user ! . projectLimit ) {
throw new Error ( "Project limit reached" );
}
}
// Create project
const projectId = await ctx . db . insert ( "projects" , {
name: args . name ,
ownerId: stackUserId ,
userId: user ! . _id ,
updatedAt: Date . now (),
});
return projectId ;
},
});
File: convex/projects.ts:7
Using Mutations in React
import { useMutation } from "convex/react" ;
import { api } from "@/convex/_generated/api" ;
function CreateProjectButton () {
const createProject = useMutation ( api . projects . create );
const handleCreate = async () => {
try {
const projectId = await createProject ({ name: "My Project" });
console . log ( "Created project:" , projectId );
} catch ( error ) {
console . error ( "Failed to create project:" , error );
}
};
return < button onClick ={ handleCreate }> Create Project </ button > ;
}
File Operations
Polaris uses Convex for both text and binary file storage.
Text Files
Stored directly in the content field:
// Write text file
export const writeFile = mutation ({
args: {
projectId: v . id ( "projects" ),
parentId: v . optional ( v . id ( "files" )),
name: v . string (),
content: v . string (),
},
handler : async ( ctx , args ) => {
const identity = await verifyAuth ( ctx );
const fileId = await ctx . db . insert ( "files" , {
projectId: args . projectId ,
parentId: args . parentId ,
name: args . name ,
type: "file" ,
content: args . content ,
updatedAt: Date . now (),
});
return fileId ;
},
});
Binary Files
Stored in Convex file storage:
// Upload binary file
export const uploadFile = mutation ({
args: {
projectId: v . id ( "projects" ),
parentId: v . optional ( v . id ( "files" )),
name: v . string (),
storageId: v . id ( "_storage" ),
},
handler : async ( ctx , args ) => {
const identity = await verifyAuth ( ctx );
const fileId = await ctx . db . insert ( "files" , {
projectId: args . projectId ,
parentId: args . parentId ,
name: args . name ,
type: "file" ,
storageId: args . storageId ,
updatedAt: Date . now (),
});
return fileId ;
},
});
File: convex/files.ts
Path-based File Access
Internal API for file operations by path (convex/system.ts):
export const writeFileByPath = mutation ({
args: {
internalKey: v . string (),
projectId: v . id ( "projects" ),
path: v . string (),
content: v . string (),
},
handler : async ( ctx , args ) => {
validateInternalKey ( args . internalKey );
const pathParts = args . path . split ( '/' ). filter ( Boolean );
let parentId : Id < "files" > | undefined = undefined ;
// Create parent folders if needed
for ( let i = 0 ; i < pathParts . length - 1 ; i ++ ) {
const folderName = pathParts [ i ];
let folder = await ctx . db
. query ( "files" )
. withIndex ( "by_project_parent" , ( q ) =>
q . eq ( "projectId" , args . projectId ). eq ( "parentId" , parentId )
)
. filter (( q ) => q . eq ( q . field ( "name" ), folderName ))
. first ();
if ( ! folder ) {
const folderId = await ctx . db . insert ( "files" , {
projectId: args . projectId ,
parentId ,
name: folderName ,
type: "folder" ,
updatedAt: Date . now (),
});
folder = await ctx . db . get ( folderId );
}
parentId = folder ! . _id ;
}
// Create or update file
const fileName = pathParts [ pathParts . length - 1 ];
const existingFile = await ctx . db
. query ( "files" )
. withIndex ( "by_project_parent" , ( q ) =>
q . eq ( "projectId" , args . projectId ). eq ( "parentId" , parentId )
)
. filter (( q ) => q . eq ( q . field ( "name" ), fileName ))
. first ();
if ( existingFile ) {
await ctx . db . patch ( existingFile . _id , {
content: args . content ,
updatedAt: Date . now (),
});
return existingFile . _id ;
} else {
return await ctx . db . insert ( "files" , {
projectId: args . projectId ,
parentId ,
name: fileName ,
type: "file" ,
content: args . content ,
updatedAt: Date . now (),
});
}
},
});
Internal API
The internal API (convex/system.ts) provides privileged operations for background jobs:
Validation
function validateInternalKey ( key : string ) {
const internalKey = process . env . POLARIS_CONVEX_INTERNAL_KEY ;
if ( ! internalKey ) {
throw new Error ( "Internal key not configured" );
}
if ( key !== internalKey ) {
throw new Error ( "Invalid internal key" );
}
}
Available Operations
writeFileByPath - Write file by path (creates parent folders)
getAllProjectFiles - Get all files in a project
updateProjectImportStatus - Update GitHub import status
updateProjectExportStatus - Update GitHub export status
Internal API mutations bypass authentication. Only call from trusted server-side code (Inngest jobs).
Real-time Subscriptions
Convex queries are reactive - they automatically re-run when data changes:
function LiveProjectsList () {
// Automatically updates when projects change
const projects = useQuery ( api . projects . get );
return (
< ul >
{ projects ?. map ( project => (
< li key = {project. _id } >
{ project . name }
< span > Updated : { new Date (project.updatedAt). toLocaleString ()}</ span >
</ li >
))}
</ ul >
);
}
When another client creates, updates, or deletes a project:
Convex detects the change
Re-runs affected queries
Sends new data to subscribed clients
React re-renders with updated data
All of this happens automatically with zero configuration!
Optimistic Updates
Mutations update the UI immediately while running in the background:
const createProject = useMutation ( api . projects . create );
const handleCreate = async () => {
// UI updates immediately with optimistic ID
const projectId = await createProject ({ name: "New Project" });
// Navigate immediately - don't wait for server
router . push ( `/projects/ ${ projectId } ` );
};
If the mutation fails, Convex automatically reverts the optimistic update.
Type Safety
Convex generates TypeScript types for all queries and mutations:
import { api } from "@/convex/_generated/api" ;
import type { Id } from "@/convex/_generated/dataModel" ;
// Full type safety
const projects = useQuery ( api . projects . get );
// ^? const projects: { _id: Id<"projects">, name: string, ... }[] | undefined
const createProject = useMutation ( api . projects . create );
// ^? (args: { name: string }) => Promise<Id<"projects">>
Convex includes several performance optimizations:
Automatic Indexing - Queries use indexes for fast lookups
Query Caching - Results are cached and reused across components
Batched Updates - Multiple mutations are batched into a single network request
Lazy Loading - Only fetch data when components render
Pagination - Use .take() to limit results:
const recentProjects = await ctx . db
. query ( "projects" )
. withIndex ( "by_owner" , ( q ) => q . eq ( "ownerId" , userId ))
. order ( "desc" )
. take ( 10 ); // Only fetch 10 most recent
Next Steps
Stack Auth Learn how authentication works with Convex
GitHub Integration Import and export projects to GitHub