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

Advanced Database Features

Advanced Database Features

Explore advanced Convex features and sophisticated database patterns available in Solo Kit. This guide covers validators, complex queries, transactions, real-time subscriptions, and enterprise-grade database techniques.

Advanced Overview

Solo Kit Advanced Architecture

Solo Kit leverages enterprise-grade Convex features including:

  • Type-safe Validators: Comprehensive input validation
  • Real-time Subscriptions: Automatic reactive data updates
  • Internal Functions: Secure server-only operations
  • Scheduled Functions: Cron jobs and delayed execution
  • File Storage: Integrated file handling
  • Complex Query Patterns: Aggregations, joins, and analytics

Advanced Feature Categories

Schema-Level Features (Validators, Indexes, Types)
    ↓
Query-Level Features (Complex Queries, Joins, Aggregations)
    ↓
Real-time Features (Subscriptions, Optimistic Updates)
    ↓
Server Features (Internal Functions, Cron Jobs, Actions)

Advanced Validators

Type-Safe Validation

Solo Kit uses Convex validators for type safety and data integrity:

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

export default defineSchema({
  users: defineTable({
    // String validation
    email: v.string(),
    name: v.string(),

    // Union types (enums)
    role: v.union(v.literal('admin'), v.literal('user')),
    status: v.union(v.literal('active'), v.literal('inactive'), v.literal('pending')),

    // Optional fields
    bio: v.optional(v.string()),
    avatarUrl: v.optional(v.string()),

    // Numeric validation
    loginCount: v.optional(v.number()),

    // Boolean validation
    emailVerified: v.optional(v.boolean()),

    // Timestamps (numbers in milliseconds)
    createdAt: v.number(),
    updatedAt: v.number(),
    lastLoginAt: v.optional(v.number()),
  })
    .index('by_email', ['email'])
    .index('by_role', ['role'])
    .index('by_status', ['status']),
});

Complex Validators

// convex/validators.ts
import { v } from 'convex/values';

// Reusable validator definitions
export const userRoleValidator = v.union(v.literal('admin'), v.literal('user'));

export const subscriptionStatusValidator = v.union(
  v.literal('active'),
  v.literal('inactive'),
  v.literal('canceled'),
  v.literal('past_due')
);

// Nested object validators
export const addressValidator = v.object({
  street: v.string(),
  city: v.string(),
  state: v.string(),
  zipCode: v.string(),
  country: v.optional(v.string()),
});

// Array validators
export const tagsValidator = v.array(v.string());

// Complex nested structures
export const userPreferencesValidator = v.object({
  theme: v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
  notifications: v.object({
    email: v.boolean(),
    push: v.boolean(),
    sms: v.optional(v.boolean()),
  }),
  language: v.optional(v.string()),
});

Using validators in functions:

// convex/users.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
import { userRoleValidator, userPreferencesValidator } from './validators';

export const createUser = mutation({
  args: {
    email: v.string(),
    name: v.string(),
    role: userRoleValidator,
    preferences: v.optional(userPreferencesValidator),
  },
  handler: async (ctx, args) => {
    // All inputs are validated before reaching the handler
    return await ctx.db.insert('users', {
      ...args,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
  },
});

Complex Query Patterns

Advanced Data Relationships

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

// Complex join with aggregations
export const getUserDashboardData = query({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    // Fetch user and all related data in parallel
    const [user, preferences, subscriptions, transactions, posts] = 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),
      ctx.db
        .query('posts')
        .withIndex('by_userId', (q) => q.eq('userId', args.userId))
        .filter((q) => q.eq(q.field('status'), 'published'))
        .order('desc')
        .take(5),
    ]);

    if (!user) return null;

    // Calculate aggregations
    const transactionStats = transactions.reduce(
      (acc, t) => ({
        total: acc.total + 1,
        totalAmount: acc.totalAmount + (t.amount || 0),
      }),
      { total: 0, totalAmount: 0 }
    );

    return {
      user,
      preferences,
      activeSubscription: subscriptions.find((s) => s.status === 'active'),
      recentTransactions: transactions,
      transactionStats,
      recentPosts: posts,
      postCount: posts.length,
    };
  },
});

Analytics and Aggregations

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

// User activity analytics
export const getUserAnalytics = query({
  args: {
    startDate: v.number(),
    endDate: v.number(),
  },
  handler: async (ctx, args) => {
    const users = await ctx.db.query('users').collect();

    // Filter by date range
    const usersInRange = users.filter(
      (u) => u.createdAt >= args.startDate && u.createdAt <= args.endDate
    );

    // Group by role
    const byRole = usersInRange.reduce(
      (acc, user) => {
        acc[user.role] = (acc[user.role] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>
    );

    // Group by day
    const byDay = usersInRange.reduce(
      (acc, user) => {
        const day = new Date(user.createdAt).toISOString().split('T')[0];
        acc[day] = (acc[day] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>
    );

    return {
      totalUsers: usersInRange.length,
      byRole,
      byDay,
      averagePerDay: usersInRange.length / Object.keys(byDay).length || 0,
    };
  },
});

// Transaction analytics
export const getTransactionAnalytics = query({
  args: {
    userId: v.optional(v.id('users')),
    startDate: v.number(),
    endDate: v.number(),
  },
  handler: async (ctx, args) => {
    let transactions = await ctx.db.query('transactions').collect();

    // Filter by user if specified
    if (args.userId) {
      transactions = transactions.filter((t) => t.userId === args.userId);
    }

    // Filter by date range
    transactions = transactions.filter(
      (t) => t.createdAt >= args.startDate && t.createdAt <= args.endDate
    );

    // Calculate stats
    const stats = {
      count: transactions.length,
      totalAmount: transactions.reduce((sum, t) => sum + (t.amount || 0), 0),
      averageAmount:
        transactions.length > 0
          ? transactions.reduce((sum, t) => sum + (t.amount || 0), 0) / transactions.length
          : 0,
      maxAmount: Math.max(...transactions.map((t) => t.amount || 0), 0),
      minAmount: Math.min(...transactions.map((t) => t.amount || 0), 0),
    };

    // Group by status
    const byStatus = transactions.reduce(
      (acc, t) => {
        acc[t.status] = (acc[t.status] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>
    );

    return { stats, byStatus };
  },
});

Real-time Subscriptions

Automatic Real-time Updates

Convex provides automatic real-time subscriptions:

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

function UserDashboard({ userId }: { userId: string }) {
  // Automatically updates when data changes
  const dashboard = useQuery(api.dashboard.getUserDashboardData, { userId });
  const updateProfile = useMutation(api.users.updateProfile);

  if (!dashboard) return <Loading />;

  return (
    <div>
      <h1>Welcome, {dashboard.user.name}</h1>

      {/* These values update automatically when database changes */}
      <p>Posts: {dashboard.postCount}</p>
      <p>Transactions: {dashboard.transactionStats.total}</p>

      <button
        onClick={() =>
          updateProfile({
            userId,
            updates: { name: 'New Name' },
          })
        }
      >
        Update Profile
      </button>
    </div>
  );
}

Optimistic Updates

// Optimistic updates for better UX
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function PostList({ posts }: { posts: Post[] }) {
  const createPost = useMutation(api.posts.create);

  const handleCreatePost = async () => {
    // Optimistic update happens automatically
    // UI updates immediately, then syncs with server
    await createPost({
      title: 'New Post',
      content: 'Content here...',
    });
  };

  return (
    <div>
      {posts.map((post) => (
        <div key={post._id}>{post.title}</div>
      ))}
      <button onClick={handleCreatePost}>Create Post</button>
    </div>
  );
}

Internal Functions

Secure Server-Only Operations

// convex/internal.ts
import { v } from 'convex/values';
import { internalMutation, internalQuery } from './_generated/server';

// These functions can only be called from other Convex functions
// Never exposed to the client

export const adminDeleteUser = internalMutation({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    // Delete all related data
    const relatedTables = ['userPreferences', 'sessions', 'subscriptions', 'transactions'];

    for (const table of relatedTables) {
      const records = await ctx.db
        .query(table as any)
        .withIndex('by_userId', (q) => q.eq('userId', args.userId))
        .collect();

      for (const record of records) {
        await ctx.db.delete(record._id);
      }
    }

    // Finally delete the user
    await ctx.db.delete(args.userId);

    return { success: true };
  },
});

export const getSystemStats = internalQuery({
  handler: async (ctx) => {
    const [users, sessions, subscriptions] = await Promise.all([
      ctx.db.query('users').collect(),
      ctx.db.query('sessions').collect(),
      ctx.db.query('subscriptions').collect(),
    ]);

    return {
      totalUsers: users.length,
      activeSessions: sessions.filter((s) => s.expiresAt > Date.now()).length,
      activeSubscriptions: subscriptions.filter((s) => s.status === 'active').length,
    };
  },
});

Calling Internal Functions

// convex/admin.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
import { internal } from './_generated/api';

export const deleteUserAsAdmin = mutation({
  args: {
    adminId: v.id('users'),
    targetUserId: v.id('users'),
  },
  handler: async (ctx, args) => {
    // Verify admin status
    const admin = await ctx.db.get(args.adminId);
    if (!admin || admin.role !== 'admin') {
      throw new Error('Unauthorized: Admin access required');
    }

    // Call internal function
    await ctx.runMutation(internal.internal.adminDeleteUser, {
      userId: args.targetUserId,
    });

    return { success: true };
  },
});

Scheduled Functions (Cron Jobs)

Setting Up Cron Jobs

// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';

const crons = cronJobs();

// Run every hour
crons.interval('cleanup-expired-sessions', { hours: 1 }, internal.cleanup.expiredSessions);

// Run daily at midnight UTC
crons.daily('daily-analytics', { hourUTC: 0 }, internal.analytics.generateDaily);

// Run weekly on Sunday
crons.weekly('weekly-report', { dayOfWeek: 'sunday', hourUTC: 9 }, internal.reports.generateWeekly);

// Run monthly on the 1st
crons.monthly('monthly-cleanup', { day: 1, hourUTC: 3 }, internal.cleanup.monthlyMaintenance);

export default crons;

Scheduled Task Implementations

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

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

    const expiredSessions = await ctx.db
      .query('sessions')
      .filter((q) => q.lt(q.field('expiresAt'), now))
      .collect();

    for (const session of expiredSessions) {
      await ctx.db.delete(session._id);
    }

    console.log(`Cleaned up ${expiredSessions.length} expired sessions`);
    return { cleaned: expiredSessions.length };
  },
});

export const monthlyMaintenance = internalMutation({
  handler: async (ctx) => {
    const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;

    // Clean old audit logs
    const oldLogs = await ctx.db
      .query('auditLogs')
      .filter((q) => q.lt(q.field('createdAt'), thirtyDaysAgo))
      .collect();

    for (const log of oldLogs) {
      await ctx.db.delete(log._id);
    }

    // Clean old metrics
    const oldMetrics = await ctx.db
      .query('metrics')
      .filter((q) => q.lt(q.field('timestamp'), thirtyDaysAgo))
      .collect();

    for (const metric of oldMetrics) {
      await ctx.db.delete(metric._id);
    }

    console.log(`Monthly cleanup: ${oldLogs.length} logs, ${oldMetrics.length} metrics`);
  },
});

Actions (External API Calls)

HTTP Actions

// convex/actions.ts
import { v } from 'convex/values';
import { action } from './_generated/server';
import { internal } from './_generated/api';

// Action for external API calls
export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  handler: async (ctx, args) => {
    // Make external API call
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: args.to }] }],
        from: { email: 'noreply@example.com' },
        subject: args.subject,
        content: [{ type: 'text/plain', value: args.body }],
      }),
    });

    if (!response.ok) {
      throw new Error(`Email failed: ${response.statusText}`);
    }

    // Log the action
    await ctx.runMutation(internal.audit.logAction, {
      action: 'email_sent',
      details: { to: args.to, subject: args.subject },
    });

    return { success: true };
  },
});

// Stripe integration action
export const createStripeCustomer = action({
  args: {
    userId: v.id('users'),
    email: v.string(),
  },
  handler: async (ctx, args) => {
    const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

    const customer = await stripe.customers.create({
      email: args.email,
      metadata: { userId: args.userId },
    });

    // Update user with Stripe customer ID
    await ctx.runMutation(internal.users.setStripeCustomerId, {
      userId: args.userId,
      stripeCustomerId: customer.id,
    });

    return { customerId: customer.id };
  },
});

Advanced Security Patterns

Row-Level Security

// convex/security.ts
import { v } from 'convex/values';
import { query, mutation } from './_generated/server';

// Get current user from auth context
async function getCurrentUser(ctx: any) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) return null;

  return await ctx.db
    .query('users')
    .withIndex('by_email', (q) => q.eq('email', identity.email))
    .first();
}

// Row-level security pattern
export const getUserData = query({
  args: { targetUserId: v.id('users') },
  handler: async (ctx, args) => {
    const currentUser = await getCurrentUser(ctx);

    if (!currentUser) {
      throw new Error('Authentication required');
    }

    // Admin can access any user
    if (currentUser.role === 'admin') {
      return await ctx.db.get(args.targetUserId);
    }

    // Users can only access their own data
    if (currentUser._id === args.targetUserId) {
      return await ctx.db.get(args.targetUserId);
    }

    throw new Error('Access denied');
  },
});

// Secure mutation with audit trail
export const updateUserSecure = mutation({
  args: {
    targetUserId: v.id('users'),
    updates: v.object({
      name: v.optional(v.string()),
      bio: v.optional(v.string()),
    }),
  },
  handler: async (ctx, args) => {
    const currentUser = await getCurrentUser(ctx);

    if (!currentUser) {
      throw new Error('Authentication required');
    }

    const canUpdate = currentUser.role === 'admin' || currentUser._id === args.targetUserId;

    if (!canUpdate) {
      throw new Error('Access denied');
    }

    // Get old values for audit
    const oldUser = await ctx.db.get(args.targetUserId);

    // Perform update
    await ctx.db.patch(args.targetUserId, {
      ...args.updates,
      updatedAt: Date.now(),
    });

    // Log audit trail
    await ctx.db.insert('auditLogs', {
      userId: currentUser._id,
      action: 'UPDATE',
      entity: 'users',
      entityId: args.targetUserId,
      changes: JSON.stringify({
        old: oldUser,
        new: args.updates,
      }),
      createdAt: Date.now(),
    });

    return { success: true };
  },
});

Performance Optimization

Efficient Pagination

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

export const getPaginatedUsers = query({
  args: {
    cursor: v.optional(v.id('users')),
    limit: v.optional(v.number()),
    direction: v.optional(v.union(v.literal('forward'), v.literal('backward'))),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 20;
    const direction = args.direction ?? 'forward';

    let query = ctx.db.query('users').order(direction === 'forward' ? 'desc' : 'asc');

    if (args.cursor) {
      const cursorDoc = await ctx.db.get(args.cursor);
      if (cursorDoc) {
        query = query.filter((q) =>
          direction === 'forward'
            ? q.lt(q.field('_creationTime'), cursorDoc._creationTime)
            : q.gt(q.field('_creationTime'), cursorDoc._creationTime)
        );
      }
    }

    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,
      hasMore,
    };
  },
});

Batch Operations

// convex/batch.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';

export const batchUpdateUsers = mutation({
  args: {
    updates: v.array(
      v.object({
        id: v.id('users'),
        data: v.object({
          name: v.optional(v.string()),
          bio: v.optional(v.string()),
        }),
      })
    ),
  },
  handler: async (ctx, args) => {
    const results = [];

    for (const update of args.updates) {
      await ctx.db.patch(update.id, {
        ...update.data,
        updatedAt: Date.now(),
      });
      results.push({ id: update.id, success: true });
    }

    return results;
  },
});

Next Steps

You've completed the comprehensive database documentation! Here are your next areas to explore:

  1. Authentication - Implement secure authentication
  2. API Development - Build robust APIs
  3. UI & Components - Create beautiful interfaces
  4. Deployment - Deploy to production

With Solo Kit's advanced database features, you have the foundation to build sophisticated, scalable, and maintainable applications that leverage the full power of Convex.

Database Monitoring

Monitor database health, performance metrics, and operational status with comprehensive observability tools and practices using Convex

On this page

Advanced Database FeaturesAdvanced OverviewSolo Kit Advanced ArchitectureAdvanced Feature CategoriesAdvanced ValidatorsType-Safe ValidationComplex ValidatorsComplex Query PatternsAdvanced Data RelationshipsAnalytics and AggregationsReal-time SubscriptionsAutomatic Real-time UpdatesOptimistic UpdatesInternal FunctionsSecure Server-Only OperationsCalling Internal FunctionsScheduled Functions (Cron Jobs)Setting Up Cron JobsScheduled Task ImplementationsActions (External API Calls)HTTP ActionsAdvanced Security PatternsRow-Level SecurityPerformance OptimizationEfficient PaginationBatch OperationsNext Steps