S
Solo Kit
DocumentationComponentsPricingChangelogRoadmapFAQContact
LoginGet Started
DocumentationComponentsPricing
LoginGet Started
Welcome to Solo Kit DocumentationIntroductionTech StackRoadmapFAQGetting Started
Database OverviewSchema DesignDatabase QueriesDatabase MigrationsDatabase SeedingDatabase PerformanceDatabase SecurityBackup & RestoreDatabase MonitoringAdvanced Database Features
Database

Database Performance

Database Performance

Learn how to optimize your Solo Kit database for maximum performance. This guide covers query optimization, indexing strategies, and performance monitoring with Convex.

Performance Overview

Solo Kit Performance Architecture

Solo Kit is designed for enterprise-grade database performance with:

  • Convex-first optimization: Automatic query optimization and caching
  • Real-time subscriptions: Efficient reactive data updates
  • Intelligent indexing: Strategic indexes on all major tables
  • Edge deployment: Global low-latency data access
  • Automatic scaling: Convex handles scaling automatically

Performance Characteristics

Target Performance Metrics:

  • Query Response Time: < 50ms for simple queries, < 200ms for complex
  • Real-time Updates: < 100ms for subscription updates
  • Concurrent Users: Unlimited with Convex's automatic scaling
  • Data Volume: Optimized for any scale with proper indexing

Query Optimization

Convex Query Performance

Solo Kit uses optimized query patterns with Convex:

// convex/users.ts
import { v } from 'convex/values';
import { query } from './_generated/server';

// Efficient single record lookup with index
export const getUserById = query({
  args: { id: v.id('users') },
  handler: async (ctx, args) => {
    // Direct ID lookup is always fast
    const user = await ctx.db.get(args.id);
    return user;
  },
});

// Paginated queries for large datasets
export const getUsers = query({
  args: {
    limit: v.optional(v.number()),
    cursor: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 50;

    let query = ctx.db.query('users').order('desc');

    const results = await query.take(limit + 1);
    const hasMore = results.length > limit;
    const users = hasMore ? results.slice(0, -1) : results;

    return {
      users,
      nextCursor: hasMore ? users[users.length - 1]._id : null,
    };
  },
});

// Efficient count queries
export const getUserCount = query({
  handler: async (ctx) => {
    const users = await ctx.db.query('users').collect();
    return users.length;
  },
});

Query Performance Best Practices

Always use indexes for filtered queries:

// GOOD: Uses index for email lookup
const user = await ctx.db
  .query('users')
  .withIndex('by_email', (q) => q.eq('email', userEmail))
  .first();

// AVOID: Full table scan without index
const user = await ctx.db
  .query('users')
  .filter((q) => q.eq(q.field('email'), userEmail))
  .first();

Optimize SELECT with field selection:

// Select only needed fields (Convex returns full documents by default)
// For large documents, consider splitting into separate tables
export const getUserProfile = query({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);
    if (!user) return null;

    // Return only needed fields
    return {
      id: user._id,
      name: user.name,
      email: user.email,
    };
  },
});

Efficient JOIN patterns with Convex:

// Optimized data fetching with related records
export const getUserWithPreferences = query({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);
    if (!user) return null;

    const preferences = await ctx.db
      .query('userPreferences')
      .withIndex('by_userId', (q) => q.eq('userId', args.userId))
      .first();

    return {
      ...user,
      preferences,
    };
  },
});

Repository Pattern Performance

Solo Kit implements the repository pattern for optimized queries:

// convex/repositories/users.ts
import { v } from 'convex/values';
import { query } from '../_generated/server';

// Optimized single user lookup
export const findById = query({
  args: { id: v.id('users') },
  handler: async (ctx, args) => {
    // Performance: Direct ID lookup is O(1)
    return await ctx.db.get(args.id);
  },
});

// Efficient paginated listing
export const list = query({
  args: {
    limit: v.optional(v.number()),
    offset: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 10;

    // Performance: Always use limits on collections
    const users = await ctx.db.query('users').order('desc').take(limit);

    return users;
  },
});

Strategic Indexing

Solo Kit Index Strategy

Solo Kit implements comprehensive indexing across all major tables:

User Table Indexes (convex/schema.ts):

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  users: defineTable({
    email: v.string(),
    name: v.string(),
    role: v.union(v.literal('admin'), v.literal('user')),
    createdAt: v.number(),
    updatedAt: v.number(),
  })
    .index('by_email', ['email']) // Login lookups
    .index('by_role', ['role']) // Role filtering
    .index('by_createdAt', ['createdAt']), // Sorting

  transactions: defineTable({
    userId: v.id('users'),
    status: v.string(),
    type: v.string(),
    amount: v.number(),
    createdAt: v.number(),
  })
    .index('by_userId', ['userId']) // User queries
    .index('by_status', ['status']) // Status filtering
    .index('by_createdAt', ['createdAt']) // Time-based queries
    .index('by_type', ['type']), // Transaction type filtering

  verificationTokens: defineTable({
    token: v.string(),
    userId: v.id('users'),
    type: v.string(),
    expiresAt: v.number(),
  })
    .index('by_token', ['token']) // Token lookup
    .index('by_expiresAt', ['expiresAt']) // Cleanup queries
    .index('by_userId_type', ['userId', 'type']), // Composite lookup
});

Index Design Principles

Single Field Indexes:

// For exact match queries
.index('by_email', ['email'])
.index('by_createdAt', ['createdAt'])

Composite Indexes:

// For multi-field queries
.index('by_userId_type', ['userId', 'type'])
.index('by_status_createdAt', ['status', 'createdAt'])

Index Usage in Queries

Using indexes effectively:

// GOOD: Uses single-field index
const user = await ctx.db
  .query('users')
  .withIndex('by_email', (q) => q.eq('email', email))
  .first();

// GOOD: Uses composite index
const tokens = await ctx.db
  .query('verificationTokens')
  .withIndex('by_userId_type', (q) => q.eq('userId', userId).eq('type', 'email_verification'))
  .collect();

// GOOD: Range query on indexed field
const recentUsers = await ctx.db
  .query('users')
  .withIndex('by_createdAt', (q) => q.gte('createdAt', cutoffDate))
  .collect();

Real-time Performance

Subscription Optimization

Convex provides automatic real-time subscriptions:

// React component with real-time updates
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function UserList() {
  // Automatically updates when data changes
  const users = useQuery(api.users.list, { limit: 10 });

  if (!users) return <Loading />;

  return (
    <ul>
      {users.map((user) => (
        <li key={user._id}>{user.name}</li>
      ))}
    </ul>
  );
}

Performance benefits:

  • Automatic caching: Convex caches query results
  • Incremental updates: Only changed data is sent
  • Deduplication: Multiple subscriptions share results

Optimizing Subscriptions

// GOOD: Specific queries for better caching
export const getUserById = query({
  args: { id: v.id('users') },
  handler: async (ctx, args) => ctx.db.get(args.id),
});

// AVOID: Large queries that change frequently
export const getAllUsersWithActivity = query({
  handler: async (ctx) => {
    // This would update whenever any user or activity changes
    const users = await ctx.db.query('users').collect();
    const activities = await ctx.db.query('activities').collect();
    // ...
  },
});

Performance Monitoring

Built-in Performance Testing

Solo Kit includes performance monitoring:

# Run performance baseline tests
pnpm perf:baseline

Performance test implementation:

// scripts/performance-baseline.ts
async function testEndpoint(url: string): Promise<PerformanceResult> {
  const times: number[] = [];
  const testRuns = 5;

  for (let i = 0; i < testRuns; i++) {
    try {
      const start = Date.now();
      const response = await fetch(url);
      const end = Date.now();

      if (response.ok) {
        times.push(end - start);
      }
    } catch (error) {
      console.error(`Error testing ${url}:`, error);
    }
  }

  return {
    endpoint: url,
    averageTime: times.reduce((a, b) => a + b) / times.length,
    status: times.length > 0 ? 'success' : 'error',
  };
}

Query Performance Analysis

Monitor query performance in Convex Dashboard:

  • Open https://dashboard.convex.dev
  • Navigate to your project
  • View "Functions" tab for execution times
  • Check "Logs" for slow query warnings

Database Cleanup Performance

Solo Kit includes automated cleanup for performance:

// convex/cleanup.ts
import { internalMutation } from './_generated/server';

export const cleanupExpiredTokens = internalMutation({
  handler: async (ctx) => {
    const now = Date.now();

    // Efficient deletion using index on expiresAt
    const expiredTokens = await ctx.db
      .query('verificationTokens')
      .withIndex('by_expiresAt', (q) => q.lt('expiresAt', now))
      .collect();

    for (const token of expiredTokens) {
      await ctx.db.delete(token._id);
    }

    if (expiredTokens.length > 0) {
      console.info(`Cleaned up ${expiredTokens.length} expired tokens`);
    }

    return expiredTokens.length;
  },
});

Advanced Optimization

Query Batching

Batch multiple queries for efficiency:

// convex/dashboard.ts
export const getUserDashboardData = query({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    // Execute queries in parallel
    const [user, preferences, subscriptions, recentTransactions] = await Promise.all([
      ctx.db.get(args.userId),
      ctx.db
        .query('userPreferences')
        .withIndex('by_userId', (q) => q.eq('userId', args.userId))
        .first(),
      ctx.db
        .query('subscriptions')
        .withIndex('by_userId', (q) => q.eq('userId', args.userId))
        .collect(),
      ctx.db
        .query('transactions')
        .withIndex('by_userId', (q) => q.eq('userId', args.userId))
        .order('desc')
        .take(10),
    ]);

    return {
      user,
      preferences,
      subscriptions,
      recentTransactions,
    };
  },
});

Pagination Optimization

Cursor-based pagination for large datasets:

// convex/users.ts
export const getUsersWithCursor = query({
  args: {
    cursor: v.optional(v.id('users')),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 20;

    let query = ctx.db.query('users').order('desc');

    if (args.cursor) {
      // Start after the cursor
      const cursorDoc = await ctx.db.get(args.cursor);
      if (cursorDoc) {
        query = query.filter((q) => q.lt(q.field('_creationTime'), cursorDoc._creationTime));
      }
    }

    const results = await query.take(limit + 1);
    const hasNextPage = results.length > limit;
    const users = hasNextPage ? results.slice(0, -1) : results;

    const nextCursor = hasNextPage ? users[users.length - 1]._id : null;

    return {
      users,
      nextCursor,
      hasNextPage,
    };
  },
});

Caching Strategy

Leverage Convex's built-in caching:

// Convex automatically caches query results
// Re-running the same query returns cached data instantly

// For expensive computations, consider storing computed values
export const updateUserStats = mutation({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    // Compute and cache statistics
    const transactions = await ctx.db
      .query('transactions')
      .withIndex('by_userId', (q) => q.eq('userId', args.userId))
      .collect();

    const stats = {
      totalTransactions: transactions.length,
      totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
      lastUpdated: Date.now(),
    };

    // Store computed stats for fast retrieval
    await ctx.db.patch(args.userId, { cachedStats: stats });
  },
});

Performance Monitoring

Database Metrics

Key metrics to monitor:

  • Query response time: Average and P95 response times
  • Subscription count: Active real-time subscriptions
  • Function execution time: Time spent in Convex functions
  • Error rate: Failed queries and mutations

Convex Dashboard monitoring:

  • Functions tab: Execution times, invocation counts
  • Logs tab: Errors, warnings, and custom logs
  • Data tab: Document counts and storage usage

Performance Alerting

Set up monitoring alerts:

// convex/monitoring.ts
export const checkDatabasePerformance = query({
  handler: async (ctx) => {
    const startTime = Date.now();

    // Test query performance
    await ctx.db.query('users').take(1);

    const responseTime = Date.now() - startTime;

    // Log slow queries
    if (responseTime > 1000) {
      console.warn(`Slow query detected: ${responseTime}ms`);
    }

    return {
      healthy: responseTime < 1000,
      responseTime,
    };
  },
});

Development Performance

Local Development Optimization

# Start Convex dev server with hot reload
npx convex dev

# Monitor function performance in dashboard
# Visit: https://dashboard.convex.dev

# Run performance baseline tests
pnpm perf:baseline

Production Performance

# Deploy optimized functions
npx convex deploy

# Monitor production health
curl /api/health/database

# Check general application health
curl /api/healthz

Next Steps

Master database optimization with these advanced guides:

  1. Security - Secure database operations
  2. Monitoring - Comprehensive database monitoring
  3. Backup & Restore - Data protection strategies
  4. Advanced - Advanced Convex features

Optimized database performance ensures your Solo Kit application scales efficiently and provides excellent user experience.

Database Seeding

Seed your database with demo data for development and testing purposes using Convex

Database Security

Secure your Solo Kit database with proper authentication, authorization, and security best practices using Convex

On this page

Database PerformancePerformance OverviewSolo Kit Performance ArchitecturePerformance CharacteristicsQuery OptimizationConvex Query PerformanceQuery Performance Best PracticesRepository Pattern PerformanceStrategic IndexingSolo Kit Index StrategyIndex Design PrinciplesIndex Usage in QueriesReal-time PerformanceSubscription OptimizationOptimizing SubscriptionsPerformance MonitoringBuilt-in Performance TestingQuery Performance AnalysisDatabase Cleanup PerformanceAdvanced OptimizationQuery BatchingPagination OptimizationCaching StrategyPerformance MonitoringDatabase MetricsPerformance AlertingDevelopment PerformanceLocal Development OptimizationProduction PerformanceNext Steps