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:resetCommand Locations
Root-level commands:
# These commands work from the project root
pnpm seed:demo
pnpm seed:clear
pnpm seed:resetUsing Convex functions directly:
# Run seeding via Convex CLI
npx convex run seed:seedDemo
npx convex run seed:clearData
npx convex run seed:resetDataSeeding 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:
| Operation | Purpose | When to Use | Safety |
|---|---|---|---|
| Migration | Schema changes | Version upgrades | Production-safe |
| Seeding | Sample data | Development/testing | Development-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:resetDataFeature 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 devTesting 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 testNext Steps
Expand your database management with these guides:
- Migrations - Evolve your schema safely
- Performance - Optimize query performance
- Security - Secure your data
- Monitoring - Monitor database health
Database seeding provides the foundation for effective development and testing with Solo Kit's Convex-powered architecture.