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 Seeding

Database Seeding

Learn how to seed your Solo Kit database with demo data for development and testing. This guide covers seeding commands, data generation strategies, and best practices for managing test data with Convex.

Seeding Overview

What is Database Seeding?

Database seeding is the process of populating your database with initial data for:

  • Development Environment: Sample data to work with during development
  • Testing: Consistent data for automated tests
  • Demo Purposes: Realistic data for demonstrations or trials
  • Initial Setup: Required baseline data for application functionality

Solo Kit Seeding Strategy

Solo Kit implements a clean development approach with optional seeding:

Fresh Database (npx convex dev)
    ↓ (optional)
Demo Data (pnpm seed:demo)
    ↓ (development)
Test Data (custom scripts)

Key Principles:

  • Clean by default: Fresh installations start with empty schemas
  • Optional demo data: Add sample data when you need it
  • Reproducible seeding: Reset and reseed at any time
  • Environment-aware: Different data for different environments

Seeding Commands

Available Commands

# Add demo data to your database
pnpm seed:demo

# Clear all data from database (keeps schema)
pnpm seed:clear

# Reset: clear all data and reseed with demo data
pnpm seed:reset

Command Locations

Root-level commands:

# These commands work from the project root
pnpm seed:demo
pnpm seed:clear
pnpm seed:reset

Using Convex functions directly:

# Run seeding via Convex CLI
npx convex run seed:seedDemo
npx convex run seed:clearData
npx convex run seed:resetData

Seeding Implementation

Convex Seeding Functions

Solo Kit uses Convex mutations for seeding:

// convex/seed.ts
import { v } from 'convex/values';
import { internalMutation } from './_generated/server';

const DEMO_USERS = [
  {
    email: 'demo@example.com',
    name: 'Demo User',
    firstName: 'Demo',
    lastName: 'User',
    displayName: 'Demo User',
    bio: 'A demo user for testing the application',
    timezone: 'UTC',
    language: 'en',
    role: 'user' as const,
  },
  {
    email: 'admin@example.com',
    name: 'Admin User',
    role: 'admin' as const,
    firstName: 'Admin',
    lastName: 'User',
    displayName: 'Admin User',
    bio: 'Administrator account for testing',
    timezone: 'UTC',
    language: 'en',
  },
];

export const seedDemo = internalMutation({
  handler: async (ctx) => {
    console.log('Seeding demo data...');

    const seededUsers = [];

    // Seed users
    console.log('Seeding demo users...');
    for (const userData of DEMO_USERS) {
      // Check if user already exists
      const existing = await ctx.db
        .query('users')
        .withIndex('by_email', (q) => q.eq('email', userData.email))
        .first();

      if (!existing) {
        const userId = await ctx.db.insert('users', {
          ...userData,
          createdAt: Date.now(),
          updatedAt: Date.now(),
        });
        seededUsers.push({ id: userId, ...userData });
      }
    }

    // Seed user preferences for each user
    console.log('Seeding user preferences...');
    for (const user of seededUsers) {
      await ctx.db.insert('userPreferences', {
        userId: user.id,
        theme: 'system',
        emailNotifications: true,
        marketingEmails: false,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
    }

    console.log(`Demo seeding completed! Created ${seededUsers.length} users.`);
    return { usersCreated: seededUsers.length };
  },
});

export const clearData = internalMutation({
  handler: async (ctx) => {
    console.log('Clearing all database data...');

    // Get all tables and clear them
    const tables = ['verificationTokens', 'subscriptions', 'userPreferences', 'users'];

    for (const table of tables) {
      const records = await ctx.db.query(table as any).collect();
      for (const record of records) {
        await ctx.db.delete(record._id);
      }
      console.log(`Cleared ${records.length} records from ${table}`);
    }

    console.log('Database cleared successfully!');
  },
});

export const resetData = internalMutation({
  handler: async (ctx) => {
    console.log('Resetting database with fresh demo data...');

    // Clear existing data
    await clearData(ctx, {});

    // Seed fresh demo data
    await seedDemo(ctx, {});

    console.log('Database reset completed!');
  },
});

Custom Seeding Implementation

Creating a Seed Script

For more complex seeding scenarios, create a dedicated script:

// scripts/seed.ts
import { ConvexHttpClient } from 'convex/browser';
import { api } from '../convex/_generated/api';

const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!;

async function main() {
  const client = new ConvexHttpClient(convexUrl);
  const command = process.argv[2];

  switch (command) {
    case 'demo':
      console.log('Seeding demo data...');
      await client.mutation(api.seed.seedDemo);
      break;
    case 'clear':
      console.log('Clearing all data...');
      await client.mutation(api.seed.clearData);
      break;
    case 'reset':
      console.log('Resetting database...');
      await client.mutation(api.seed.resetData);
      break;
    default:
      console.log('Usage: tsx seed.ts [demo|clear|reset]');
      process.exit(1);
  }

  console.log('Done!');
  process.exit(0);
}

main().catch((error) => {
  console.error('Seed script failed:', error);
  process.exit(1);
});

Environment-Aware Seeding

Different data for different environments:

// convex/seed.ts
export const seedEnvironment = internalMutation({
  handler: async (ctx) => {
    const isDevelopment = process.env.CONVEX_CLOUD_URL?.includes('dev');
    const isProduction = process.env.CONVEX_CLOUD_URL?.includes('prod');

    if (isDevelopment) {
      await seedDevelopmentData(ctx);
    } else if (isProduction) {
      // Never seed production automatically
      console.warn('Production seeding disabled for safety');
      return;
    } else {
      await seedDemo(ctx, {});
    }
  },
});

async function seedDevelopmentData(ctx: any) {
  // Rich demo data with multiple users, posts, etc.
  await seedDemo(ctx, {});
  await seedAdditionalTestData(ctx);
}

async function seedAdditionalTestData(ctx: any) {
  // Additional test data for development
  console.log('Seeding additional test data...');
  // ... add more seeding logic
}

Seeding Patterns

User-Centric Seeding

Create realistic user hierarchies:

// convex/seed.ts
const DEMO_DATA = {
  // Regular users
  users: [
    {
      email: 'user1@example.com',
      name: 'Alice Johnson',
      role: 'user' as const,
      firstName: 'Alice',
      lastName: 'Johnson',
    },
    {
      email: 'user2@example.com',
      name: 'Bob Smith',
      role: 'user' as const,
      firstName: 'Bob',
      lastName: 'Smith',
    },
  ],

  // Admin users
  admins: [
    {
      email: 'admin@example.com',
      name: 'Super Admin',
      role: 'admin' as const,
      firstName: 'Super',
      lastName: 'Admin',
    },
  ],
};

Relational Data Seeding

Maintain referential integrity:

// convex/seed.ts
export const seedRelationalData = internalMutation({
  handler: async (ctx) => {
    // 1. Create parent entities first
    const userIds: any[] = [];
    for (const userData of DEMO_USERS) {
      const userId = await ctx.db.insert('users', {
        ...userData,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
      userIds.push(userId);
    }

    // 2. Create dependent entities with proper references
    for (const userId of userIds) {
      await ctx.db.insert('userPreferences', {
        userId,
        theme: 'system',
        emailNotifications: true,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
    }

    // 3. Create business entities
    const subscriptionUsers = userIds.slice(0, 2);
    for (const userId of subscriptionUsers) {
      await ctx.db.insert('subscriptions', {
        userId,
        status: 'active',
        stripeSubscriptionId: `sub_demo_${userId}`,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
    }
  },
});

Conflict Resolution

Handle duplicate seeding gracefully:

// convex/seed.ts
export const seedWithConflictResolution = internalMutation({
  handler: async (ctx) => {
    for (const userData of DEMO_USERS) {
      // Check if user already exists
      const existing = await ctx.db
        .query('users')
        .withIndex('by_email', (q) => q.eq('email', userData.email))
        .first();

      if (existing) {
        // Update existing user
        await ctx.db.patch(existing._id, {
          ...userData,
          updatedAt: Date.now(),
        });
        console.log(`Updated existing user: ${userData.email}`);
      } else {
        // Insert new user
        await ctx.db.insert('users', {
          ...userData,
          createdAt: Date.now(),
          updatedAt: Date.now(),
        });
        console.log(`Created new user: ${userData.email}`);
      }
    }
  },
});

Testing Data Strategies

Test-Specific Seeding

Create focused test data:

// convex/testHelpers.ts
import { v } from 'convex/values';
import { internalMutation } from './_generated/server';

export const seedUserTest = internalMutation({
  handler: async (ctx) => {
    const userId = await ctx.db.insert('users', {
      email: `test-${Date.now()}@example.com`,
      name: 'Test User',
      firstName: 'Test',
      lastName: 'User',
      role: 'user',
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });

    return userId;
  },
});

export const seedSubscriptionTest = internalMutation({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    const subscriptionId = await ctx.db.insert('subscriptions', {
      userId: args.userId,
      status: 'active',
      stripeSubscriptionId: `sub_test_${Date.now()}`,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });

    return subscriptionId;
  },
});

Test Data Cleanup

Ensure clean test isolation:

// convex/testHelpers.ts
export const cleanupTestData = internalMutation({
  handler: async (ctx) => {
    // Clean up test data (identifiable by email pattern)
    const testUsers = await ctx.db
      .query('users')
      .filter((q) =>
        q.or(
          q.eq(q.field('email'), 'test-'),
          // Match pattern for test emails
          q.gte(q.field('email'), 'test-')
        )
      )
      .collect();

    // Filter to only test emails
    const toDelete = testUsers.filter((u) => u.email.startsWith('test-'));

    for (const user of toDelete) {
      await ctx.db.delete(user._id);
    }

    console.log(`Cleaned up ${toDelete.length} test users`);
  },
});

Production Considerations

Production Seeding Safety

Never automatically seed production:

// convex/seed.ts
export const seedProduction = internalMutation({
  args: { confirmationCode: v.string() },
  handler: async (ctx, args) => {
    // Require explicit confirmation for production seeding
    const expectedCode = `SEED_PROD_${new Date().toISOString().split('T')[0]}`;

    if (args.confirmationCode !== expectedCode) {
      throw new Error(
        'Production seeding requires correct confirmation code. ' +
          'This is a safety measure to prevent accidental data modification.'
      );
    }

    console.log('Production seeding confirmed, proceeding...');
    // Proceed with minimal production seeding...
  },
});

Data Migration vs Seeding

Understand the difference:

OperationPurposeWhen to UseSafety
MigrationSchema changesVersion upgradesProduction-safe
SeedingSample dataDevelopment/testingDevelopment-only

Migrations change structure, seeding adds data.

Advanced Seeding

Realistic Data Generation

Use libraries for realistic data:

// convex/seed.ts
import { faker } from '@faker-js/faker';

function generateRealisticUsers(count: number) {
  return Array.from({ length: count }, (_, i) => ({
    email: faker.internet.email(),
    name: faker.person.fullName(),
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    bio: faker.lorem.sentence(),
    avatarUrl: faker.image.avatar(),
    timezone: faker.location.timeZone(),
    language: faker.helpers.arrayElement(['en', 'es', 'fr', 'de']),
    role: 'user' as const,
  }));
}

export const seedRealisticData = internalMutation({
  args: { userCount: v.number() },
  handler: async (ctx, args) => {
    const users = generateRealisticUsers(args.userCount);

    for (const userData of users) {
      await ctx.db.insert('users', {
        ...userData,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
    }

    console.log(`Created ${users.length} realistic demo users`);
  },
});

Progressive Data Volume

Scale data based on needs:

// convex/seed.ts
const SEEDING_MODES = {
  minimal: { users: 2, subscriptions: 1 },
  standard: { users: 10, subscriptions: 5 },
  extensive: { users: 100, subscriptions: 25 },
  stress: { users: 1000, subscriptions: 200 },
};

export const seedByMode = internalMutation({
  args: {
    mode: v.union(
      v.literal('minimal'),
      v.literal('standard'),
      v.literal('extensive'),
      v.literal('stress')
    ),
  },
  handler: async (ctx, args) => {
    const config = SEEDING_MODES[args.mode];

    const users = generateRealisticUsers(config.users);
    const userIds: any[] = [];

    for (const userData of users) {
      const userId = await ctx.db.insert('users', {
        ...userData,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
      userIds.push(userId);
    }

    // Generate proportional subscriptions
    const subscriptionUserIds = userIds.slice(0, config.subscriptions);
    for (const userId of subscriptionUserIds) {
      await ctx.db.insert('subscriptions', {
        userId,
        status: 'active',
        stripeSubscriptionId: `sub_demo_${userId}`,
        createdAt: Date.now(),
        updatedAt: Date.now(),
      });
    }

    console.log(`Seeded ${config.users} users and ${config.subscriptions} subscriptions`);
  },
});

Seeding Performance

Optimize for large datasets:

// convex/seed.ts
export const seedLargeDataset = internalMutation({
  args: { totalUsers: v.number() },
  handler: async (ctx, args) => {
    const BATCH_SIZE = 100;
    const users = generateRealisticUsers(args.totalUsers);

    // Process in batches to avoid timeouts
    for (let i = 0; i < users.length; i += BATCH_SIZE) {
      const batch = users.slice(i, i + BATCH_SIZE);

      for (const userData of batch) {
        await ctx.db.insert('users', {
          ...userData,
          createdAt: Date.now(),
          updatedAt: Date.now(),
        });
      }

      console.log(
        `Seeded batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(users.length / BATCH_SIZE)}`
      );
    }
  },
});

Development Workflow

Daily Development

# Start Convex development server
npx convex dev

# Add demo data when needed
npx convex run seed:seedDemo

# Browse/modify data in Convex dashboard
# Visit: https://dashboard.convex.dev

# Reset when needed
npx convex run seed:resetData

Feature Development

# Create feature branch
git checkout -b feature/user-profiles

# Ensure clean data
npx convex run seed:resetData

# Add specific test data for your feature
# (custom seeding in your dev script)

# Test feature with realistic data
pnpm dev

Testing Workflow

# Before running tests
npx convex run seed:clearData

# Tests create their own data
pnpm test

# Or with baseline data
npx convex run seed:seedDemo
pnpm test

Next Steps

Expand your database management with these guides:

  1. Migrations - Evolve your schema safely
  2. Performance - Optimize query performance
  3. Security - Secure your data
  4. Monitoring - Monitor database health

Database seeding provides the foundation for effective development and testing with Solo Kit's Convex-powered architecture.

Database Migrations

Manage database schema evolution safely with Convex, from development to production deployments

Database Performance

Optimize database queries, implement effective indexing, and monitor performance in your Solo Kit application with Convex

On this page

Database SeedingSeeding OverviewWhat is Database Seeding?Solo Kit Seeding StrategySeeding CommandsAvailable CommandsCommand LocationsSeeding ImplementationConvex Seeding FunctionsCustom Seeding ImplementationCreating a Seed ScriptEnvironment-Aware SeedingSeeding PatternsUser-Centric SeedingRelational Data SeedingConflict ResolutionTesting Data StrategiesTest-Specific SeedingTest Data CleanupProduction ConsiderationsProduction Seeding SafetyData Migration vs SeedingAdvanced SeedingRealistic Data GenerationProgressive Data VolumeSeeding PerformanceDevelopment WorkflowDaily DevelopmentFeature DevelopmentTesting WorkflowNext Steps