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, connection pooling, and performance monitoring with PostgreSQL and Drizzle ORM.

⚡ Performance Overview

Solo Kit Performance Architecture

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

  • PostgreSQL-first optimization: Native PostgreSQL features and indexes
  • Intelligent connection pooling: Neon serverless + postgres.js dual approach
  • Query optimization: Prepared statements and efficient query patterns
  • Strategic indexing: Performance indexes on all major tables
  • Connection management: Automatic retry logic and health monitoring

Performance Characteristics

Target Performance Metrics:

  • Query Response Time: < 50ms for simple queries, < 200ms for complex
  • Connection Establishment: < 100ms with connection pooling
  • Concurrent Users: 1000+ with proper connection management
  • Data Volume: Optimized for millions of records with strategic indexing

🎯 Query Optimization

Drizzle Query Performance

Solo Kit uses optimized query patterns with Drizzle ORM:

// ✅ Optimized query patterns (from packages/database/queries/users.ts)
import { eq, count } from 'drizzle-orm';
import { getDb } from '../lib/connection';
import { users } from '../schema/users';

// Efficient single record lookup with limit
export async function getUserById(id: string): Promise<User | null> {
  const database = await getRequiredDb();
  const result = await database
    .select()
    .from(users)
    .where(eq(users.id, id))
    .limit(1); // Always limit single record queries
  return result[0] || null;
}

// Paginated queries for large datasets
export async function getUsers(limit = 50, offset = 0): Promise<User[]> {
  const database = await getRequiredDb();
  return await database
    .select()
    .from(users)
    .limit(limit) // Always limit large queries
    .offset(offset);
}

// Efficient count queries
export async function getUserCount(): Promise<number> {
  const database = await getRequiredDb();
  const result = await database
    .select({ count: count(users.id) })
    .from(users);
  return result[0].count;
}

Query Performance Best Practices

Always use prepared statements (automatic with Drizzle):

// ✅ Prepared statements (automatic)
const user = await database
  .select()
  .from(users)
  .where(eq(users.email, userEmail))
  .limit(1);

// ❌ Raw SQL (avoid unless necessary)
const user = await database.execute(sql`
  SELECT * FROM users WHERE email = ${userEmail}
`);

Optimize SELECT clauses:

// ✅ Select only needed columns
const userProfile = await database
  .select({
    id: users.id,
    name: users.name,
    email: users.email,
  })
  .from(users)
  .where(eq(users.id, userId));

// ❌ Select all columns
const user = await database
  .select()
  .from(users)
  .where(eq(users.id, userId));

Efficient JOIN patterns:

// ✅ Optimized JOIN with selected columns
const userWithPreferences = await database
  .select({
    userId: users.id,
    userName: users.name,
    userEmail: users.email,
    theme: userPreferences.theme,
    emailNotifications: userPreferences.emailNotifications,
  })
  .from(users)
  .leftJoin(userPreferences, eq(users.id, userPreferences.userId))
  .where(eq(users.id, userId))
  .limit(1);

Repository Pattern Performance

Solo Kit implements the repository pattern for optimized queries:

// packages/database/lib/repositories/users.ts
export class UserRepository {
  constructor(private db: DatabaseInstance) {}

  /**
   * Optimized single user lookup with caching potential
   */
  async findById(id: string): Promise<User | null> {
    const [user] = await this.db
      .select()
      .from(users)
      .where(eq(users.id, id))
      .limit(1); // Performance: always limit single lookups
    return user || null;
  }

  /**
   * Efficient paginated listing
   */
  async list(limit: number = 10, offset: number = 0): Promise<User[]> {
    return await this.db
      .select()
      .from(users)
      .limit(limit) // Performance: always limit collections
      .offset(offset);
  }
}

🏗️ Strategic Indexing

Solo Kit Index Strategy

Solo Kit implements comprehensive indexing across all major tables:

User Table Indexes (schema/users.ts):

export const users = pgTable(
  'users',
  {
    // ... column definitions
  },
  (table) => ({
    // Performance indexes
    emailIdx: index('users_email_idx').on(table.email), // Login lookups
    createdAtIdx: index('users_created_at_idx').on(table.createdAt), // Sorting
    updatedAtIdx: index('users_updated_at_idx').on(table.updatedAt), // Recent activity
  })
);

Transaction Table Indexes (schema/transactions.ts):

export const transactions = pgTable(
  'transactions', 
  {
    // ... columns
  },
  (table) => ({
    // Query-optimized indexes
    userIdIdx: index('transactions_user_id_idx').on(table.userId), // User queries
    statusIdx: index('transactions_status_idx').on(table.status), // Status filtering
    createdAtIdx: index('transactions_created_at_idx').on(table.createdAt), // Time-based queries
    typeIdx: index('transactions_type_idx').on(table.type), // Transaction type filtering
  })
);

Verification Token Indexes (schema/verification-tokens.ts):

export const verificationTokens = pgTable(
  'verification_tokens',
  {
    // ... columns
  },
  (table) => ({
    // Performance indexes
    tokenIdx: index('verification_tokens_token_idx').on(table.token), // Token lookup
    expiresAtIdx: index('verification_tokens_expires_at_idx').on(table.expiresAt), // Cleanup queries
    userIdTypeIdx: index('verification_tokens_user_id_type_idx').on(
      table.userId,
      table.type
    ), // Composite lookup
  })
);

Index Design Principles

Single Column Indexes:

-- For exact match queries
CREATE INDEX users_email_idx ON users (email);
CREATE INDEX users_created_at_idx ON users (created_at);

Composite Indexes:

-- For multi-column WHERE clauses
CREATE INDEX verification_tokens_user_id_type_idx 
ON verification_tokens (user_id, type);

-- For ORDER BY with WHERE
CREATE INDEX transactions_user_id_created_at_idx 
ON transactions (user_id, created_at DESC);

Partial Indexes:

-- For filtered queries (PostgreSQL feature)
CREATE INDEX active_users_email_idx 
ON users (email) WHERE is_active = true;

-- For non-null values only
CREATE INDEX users_bio_idx 
ON users (bio) WHERE bio IS NOT NULL;

Index Maintenance

Monitor index usage:

-- Check index usage statistics
SELECT 
  indexrelname as index_name,
  idx_scan as index_scans,
  idx_tup_read as tuples_read,
  idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes 
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;

Find missing indexes:

-- Identify slow queries without indexes
SELECT 
  query,
  calls,
  total_time,
  mean_time,
  rows
FROM pg_stat_statements 
WHERE mean_time > 100 -- queries taking > 100ms
ORDER BY mean_time DESC 
LIMIT 10;

🔗 Connection Management

Dual-Driver Architecture

Solo Kit uses an intelligent connection strategy:

// packages/database/lib/connection.ts
export async function getDb(): Promise<NeonDatabase | PostgresJsDatabase | null> {
  try {
    if (!databaseUrl) {
      return null; // Graceful degradation
    }

    if (databaseUrl.includes('neon.tech')) {
      // Neon serverless for production
      const client = neon(databaseUrl);
      return drizzle(client);
    } else {
      // postgres.js for development/other providers
      const client = postgres(databaseUrl, {
        max: 10, // Connection pool size
        idle_timeout: 20, // Close idle connections
        connect_timeout: 30, // Connection timeout
        prepare: false, // Better compatibility
      });
      return drizzle(client);
    }
  } catch (error) {
    console.error('Database connection failed:', error);
    return null;
  }
}

Connection Pooling Configuration

postgres.js Configuration:

const client = postgres(databaseUrl, {
  max: 10, // Maximum 10 concurrent connections
  idle_timeout: 20, // Close idle connections after 20 seconds
  connect_timeout: 30, // 30 second connection timeout
  prepare: false, // Disable prepared statements for compatibility
  transform: postgres.camel, // Transform snake_case to camelCase
});

Neon Serverless Benefits:

  • Auto-scaling: Connections scale with demand
  • Zero idle cost: No cost when inactive
  • Global edge: Reduced latency worldwide
  • Built-in pooling: Automatic connection management

Health Monitoring

Solo Kit includes database health monitoring:

// API route: app/api/health/database/route.ts
export async function GET() {
  try {
    const startTime = Date.now();
    const database = await getDb();
    
    if (!database) {
      return Response.json(
        { status: 'unhealthy', message: 'Database not configured' },
        { status: 503 }
      );
    }

    // Test query performance
    await database.execute(sql`SELECT 1`);
    const responseTime = Date.now() - startTime;

    return Response.json({
      status: 'healthy',
      responseTime: `${responseTime}ms`,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    return Response.json(
      { status: 'unhealthy', error: error.message },
      { status: 503 }
    );
  }
}

📊 Performance Monitoring

Built-in Performance Testing

Solo Kit includes performance baseline testing:

# 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

Analyze query performance with EXPLAIN:

-- Analyze query execution plan
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';

-- Check index usage
EXPLAIN (ANALYZE, BUFFERS) 
SELECT u.*, up.theme 
FROM users u 
LEFT JOIN user_preferences up ON u.id = up.user_id 
WHERE u.email = 'user@example.com';

Monitor slow queries:

-- Enable pg_stat_statements extension (usually enabled by default)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Find slowest queries
SELECT 
  query,
  calls,
  total_time,
  mean_time,
  stddev_time,
  rows
FROM pg_stat_statements 
ORDER BY mean_time DESC 
LIMIT 10;

Database Cleanup Performance

Solo Kit includes automated cleanup for performance:

// packages/database/lib/auth-cleanup.ts
export async function cleanupExpiredTokens(): Promise<number> {
  try {
    const now = new Date();
    const database = await db;
    
    if (!database) {
      throw new Error('Database not available');
    }
    
    // Efficient deletion with index on expiresAt
    const result = await database
      .delete(verificationTokens)
      .where(lt(verificationTokens.expiresAt, now))
      .returning({ id: verificationTokens.id });

    const deletedCount = result.length;

    if (deletedCount > 0) {
      console.info(`🧹 Cleaned up ${deletedCount} expired verification tokens`);
    }

    return deletedCount;
  } catch (error) {
    console.error('Failed to cleanup expired tokens:', error);
    throw error;
  }
}

🚀 Advanced Optimization

Query Batching

Batch multiple queries for efficiency:

// ✅ Batch related queries
async function getUserDashboardData(userId: string) {
  const database = await getRequiredDb();
  
  // Execute queries in parallel
  const [user, preferences, subscriptions, recentTransactions] = await Promise.all([
    database.select().from(users).where(eq(users.id, userId)).limit(1),
    database.select().from(userPreferences).where(eq(userPreferences.userId, userId)).limit(1),
    database.select().from(subscriptions).where(eq(subscriptions.userId, userId)),
    database.select().from(transactions)
      .where(eq(transactions.userId, userId))
      .orderBy(desc(transactions.createdAt))
      .limit(10),
  ]);

  return {
    user: user[0],
    preferences: preferences[0],
    subscriptions,
    recentTransactions,
  };
}

Pagination Optimization

Cursor-based pagination for large datasets:

// ✅ Cursor-based pagination (more efficient for large datasets)
async function getUsersWithCursor(cursor?: string, limit = 20) {
  const database = await getRequiredDb();
  
  let query = database
    .select()
    .from(users)
    .orderBy(users.createdAt, users.id) // Stable sort order
    .limit(limit + 1); // Get one extra to check for next page

  if (cursor) {
    query = query.where(gt(users.createdAt, new Date(cursor)));
  }

  const results = await query;
  const hasNextPage = results.length > limit;
  const users = hasNextPage ? results.slice(0, -1) : results;
  
  const nextCursor = hasNextPage 
    ? users[users.length - 1].createdAt.toISOString()
    : null;

  return {
    users,
    nextCursor,
    hasNextPage,
  };
}

Caching Strategy

Implement query result caching:

// Example caching wrapper (implement with Redis or similar)
class CachedUserRepository {
  constructor(
    private userRepo: UserRepository,
    private cache: Cache // Redis, Upstash, etc.
  ) {}

  async findById(id: string): Promise<User | null> {
    const cacheKey = `user:${id}`;
    
    // Try cache first
    const cached = await this.cache.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Fallback to database
    const user = await this.userRepo.findById(id);
    
    if (user) {
      // Cache for 5 minutes
      await this.cache.setex(cacheKey, 300, JSON.stringify(user));
    }

    return user;
  }
}

📈 Performance Monitoring

Database Metrics

Key metrics to monitor:

  • Query response time: Average and P95 response times
  • Connection count: Active connections vs pool size
  • Index hit ratio: Should be > 99%
  • Transaction throughput: Transactions per second
  • Lock contention: Blocked queries and deadlocks

PostgreSQL monitoring queries:

-- Connection statistics
SELECT 
  state,
  count(*) as connections
FROM pg_stat_activity 
WHERE datname = current_database()
GROUP BY state;

-- Index hit ratio (should be > 0.99)
SELECT 
  schemaname,
  tablename,
  attname,
  n_distinct,
  correlation
FROM pg_stats 
WHERE schemaname = 'public'
ORDER BY n_distinct DESC;

-- Table sizes and bloat
SELECT 
  tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables 
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

Performance Alerting

Set up monitoring alerts:

// Example health check with performance thresholds
export async function checkDatabasePerformance() {
  const startTime = Date.now();
  
  try {
    const database = await getRequiredDb();
    await database.execute(sql`SELECT 1`);
    
    const responseTime = Date.now() - startTime;
    
    // Alert if response time > 1 second
    if (responseTime > 1000) {
      console.warn(`🐌 Slow database response: ${responseTime}ms`);
      // Send alert to monitoring service
    }
    
    return { healthy: true, responseTime };
  } catch (error) {
    console.error('🚨 Database health check failed:', error);
    // Send critical alert
    return { healthy: false, error: error.message };
  }
}

🔧 Development Performance

Local Development Optimization

# Use db:push for fast development iteration
pnpm db:push          # Direct schema sync (no migration files)

# Monitor query performance in development
pnpm db:studio        # Visual query analysis

# Run performance baseline tests
pnpm perf:baseline    # Test API endpoint performance

Production Performance

# Use migrations for production deployments
pnpm db:migrate       # Safe, versioned schema changes

# Monitor production health
curl /api/health/database  # Check database health
curl /api/healthz          # General application health

🎯 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 PostgreSQL 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

Database Security

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

On this page

Database Performance⚡ Performance OverviewSolo Kit Performance ArchitecturePerformance Characteristics🎯 Query OptimizationDrizzle Query PerformanceQuery Performance Best PracticesRepository Pattern Performance🏗️ Strategic IndexingSolo Kit Index StrategyIndex Design PrinciplesIndex Maintenance🔗 Connection ManagementDual-Driver ArchitectureConnection Pooling ConfigurationHealth Monitoring📊 Performance MonitoringBuilt-in Performance TestingQuery Performance AnalysisDatabase Cleanup Performance🚀 Advanced OptimizationQuery BatchingPagination OptimizationCaching Strategy📈 Performance MonitoringDatabase MetricsPerformance Alerting🔧 Development PerformanceLocal Development OptimizationProduction Performance🎯 Next Steps