AiVault uses Convex as its database with three main tables: tools, bookmarks, and reviews.
Schema Overview
The schema is defined in convex/schema.ts using Convex’s type-safe schema builder.
import { defineSchema , defineTable } from "convex/server" ;
import { v } from "convex/values" ;
export default defineSchema ({
tools: defineTable ({ ... }) ,
bookmarks: defineTable ({ ... }) ,
reviews: defineTable ({ ... })
}) ;
Stores all AI tools submitted to the directory.
Fields
tools : defineTable ({
// Core fields
name: v . string (),
slug: v . string (),
description: v . string (),
longDescription: v . optional ( v . string ()),
category: v . string (),
tags: v . array ( v . string ()),
websiteUrl: v . string (),
logoUrl: v . optional ( v . string ()),
// Pricing
pricing: v . string (), // "Free", "Freemium", "Paid"
pricingDetails: v . optional ( v . string ()),
// Metadata
upvotes: v . number (),
submittedBy: v . string (), // userId
approved: v . boolean (),
createdAt: v . number (),
featured: v . optional ( v . boolean ()),
isNew: v . optional ( v . boolean ()),
// Rich content
features: v . optional ( v . array ( v . string ())),
useCases: v . optional ( v . array ( v . string ())),
pros: v . optional ( v . array ( v . string ())),
cons: v . optional ( v . array ( v . string ())),
platforms: v . optional ( v . array ( v . string ())),
lastUpdated: v . optional ( v . string ()),
// Social links
twitterUrl: v . optional ( v . string ()),
githubUrl: v . optional ( v . string ()),
discordUrl: v . optional ( v . string ()),
})
Indices
Convex uses indices for efficient querying:
. index ( "by_slug" , [ "slug" ])
. index ( "by_category" , [ "category" ])
. index ( "by_approved" , [ "approved" ])
. index ( "by_submittedBy" , [ "submittedBy" ])
. index ( "by_upvotes" , [ "upvotes" ])
. index ( "by_createdAt" , [ "createdAt" ])
Indices are required for efficient filtering in Convex. Every query should use an index as the starting point.
Key Patterns
Field Type Purpose slugstringUnique URL-friendly identifier approvedbooleanAdmin approval status (moderation) submittedBystringClerk user ID of submitter upvotesnumberPopularity metric createdAtnumberUnix timestamp (milliseconds) pricingstring”Free”, “Freemium”, or “Paid” platformsstring[]”Web”, “iOS”, “Android”, “Desktop”, “API”, “Chrome Extension”
Bookmarks Table
Tracks user-saved tools.
bookmarks : defineTable ({
userId: v . string (),
toolId: v . id ( "tools" ),
})
. index ( "by_userId" , [ "userId" ])
. index ( "by_toolId" , [ "toolId" ])
. index ( "by_userId_and_toolId" , [ "userId" , "toolId" ])
Relationships
userId → Clerk user identifier
toolId → Foreign key reference to tools table
Query Patterns
// Get all bookmarks for a user
await ctx . db
. query ( "bookmarks" )
. withIndex ( "by_userId" , q => q . eq ( "userId" , userId ))
. collect ();
// Check if user bookmarked a specific tool
await ctx . db
. query ( "bookmarks" )
. withIndex ( "by_userId_and_toolId" , q =>
q . eq ( "userId" , userId ). eq ( "toolId" , toolId )
)
. first ();
The composite index by_userId_and_toolId enables efficient bookmark existence checks without scanning all user bookmarks.
Reviews Table
User-submitted ratings and comments.
reviews : defineTable ({
userId: v . string (),
toolId: v . id ( "tools" ),
rating: v . number (),
comment: v . string (),
createdAt: v . number (),
})
. index ( "by_toolId" , [ "toolId" ])
. index ( "by_userId" , [ "userId" ])
Fields
Field Type Description userIdstringClerk user ID toolIdId<"tools">Reference to tool ratingnumber1-5 star rating commentstringReview text createdAtnumberTimestamp
Query Examples
// Get all reviews for a tool
const reviews = await ctx . db
. query ( "reviews" )
. withIndex ( "by_toolId" , q => q . eq ( "toolId" , toolId ))
. collect ();
// Calculate average rating
const avgRating = reviews . reduce (( sum , r ) => sum + r . rating , 0 ) / reviews . length ;
Relationships Diagram
Convex doesn’t enforce foreign key constraints at the database level. Referential integrity is maintained through application logic.
Type Safety
Convex automatically generates TypeScript types from the schema:
import { Doc , Id } from "./_generated/dataModel" ;
// Get full tool type
type Tool = Doc < "tools" >;
// Get tool ID type
type ToolId = Id < "tools" >;
// Use in functions
function displayTool ( tool : Tool ) {
console . log ( tool . name ); // ✓ Type-safe
console . log ( tool . invalid ); // ✗ Type error
}
Schema Migrations
Convex handles schema changes automatically:
Update schema.ts
Push changes: npx convex dev
Convex validates and migrates data
Adding required fields to existing tables requires providing default values or backfilling data first.
Best Practices
Start every query with .withIndex(). Table scans are slow and may hit Convex limits. // ✓ Good
await ctx . db . query ( "tools" ). withIndex ( "by_category" , q => q . eq ( "category" , cat )). collect ();
// ✗ Bad - table scan
await ctx . db . query ( "tools" ). filter ( q => q . eq ( q . field ( "category" ), cat )). collect ();
Use optional fields for nullable data
Mark fields that may not always be present as optional with v.optional(). logoUrl : v . optional ( v . string ()) // May be missing
Store timestamps as numbers
Use Date.now() for timestamps (Unix milliseconds) for efficient sorting and filtering. createdAt : Date . now () // 1709467200000
Create composite indices for multi-field queries
If you frequently query by multiple fields, create a composite index. . index ( "by_userId_and_toolId" , [ "userId" , "toolId" ])
Next Steps
Convex Backend Learn how to query and mutate data
Authentication Understand user identity and permissions