Database Seeding¶
Overview¶
The database seeding process populates initial data required for the application to function. Seeding runs automatically after migrations in development but must be run manually in production.
Seed Script: api/prisma/seed.ts
Seed Data: - Default super admin user - Default map settings (Edmonton coordinates) - 6 page blocks for landing page builder - 4 email templates (campaign email, response verification, shift signup confirmation, shift details reminder)
Running Seed¶
Development¶
Production (Docker)¶
CI/CD¶
Seed runs automatically after prisma migrate deploy if configured in package.json:
Seed Data Details¶
1. Default Admin User¶
Email: admin@cmlite.org
Password: ChangeMe2025!
Role: SUPER_ADMIN
Status: ACTIVE
Email Verified: true
Code:
const hashedPassword = await bcrypt.hash('ChangeMe2025!', 10);
const admin = await prisma.user.upsert({
where: { email: 'admin@cmlite.org' },
update: {
password: hashedPassword,
emailVerified: true,
status: 'ACTIVE',
},
create: {
email: 'admin@cmlite.org',
password: hashedPassword,
name: 'Admin',
role: UserRole.SUPER_ADMIN,
emailVerified: true,
},
});
Security Note: Change default password immediately after first login!
2. Default Map Settings¶
ID: default (singleton)
Coordinates: Edmonton, AB (53.5461°N, 113.4938°W)
Zoom: 11
Walk Sheet: Blank titles/footers
Code:
await prisma.mapSettings.upsert({
where: { id: 'default' },
update: {},
create: {
id: 'default',
latitude: 53.5461,
longitude: -113.4938,
zoom: 11,
walkSheetTitle: 'Walk Sheet',
walkSheetSubtitle: '',
walkSheetFooter: '',
},
});
3. Page Blocks (6 blocks)¶
Hero Section¶
{
id: 'default-hero',
type: 'hero',
label: 'Hero Section',
category: 'Headers',
sortOrder: 1,
schema: {
title: { type: 'string', label: 'Title' },
subtitle: { type: 'string', label: 'Subtitle' },
backgroundImage: { type: 'string', label: 'Background Image URL' },
ctaText: { type: 'string', label: 'Button Text' },
ctaUrl: { type: 'string', label: 'Button URL' },
},
defaults: {
title: 'Welcome to Our Campaign',
subtitle: 'Join us in making a difference in your community.',
backgroundImage: '',
ctaText: 'Get Involved',
ctaUrl: '#',
},
}
Text Block¶
{
id: 'default-text',
type: 'text',
label: 'Text Block',
category: 'Content',
sortOrder: 2,
schema: {
heading: { type: 'string', label: 'Heading' },
body: { type: 'text', label: 'Body Text' },
},
defaults: {
heading: 'About Us',
body: 'Tell your story here...',
},
}
Features Grid¶
{
id: 'default-features',
type: 'features',
label: 'Features Grid',
category: 'Content',
sortOrder: 3,
schema: {
features: {
type: 'array',
label: 'Features',
items: { title: 'string', description: 'string', icon: 'string' }
},
},
defaults: {
features: [
{ title: 'Community Action', description: 'Organize local events...', icon: '' },
{ title: 'Advocacy', description: 'Email your representatives...', icon: '' },
{ title: 'Volunteer', description: 'Sign up for shifts...', icon: '' },
],
},
}
Call to Action¶
{
id: 'default-cta',
type: 'cta',
label: 'Call to Action',
category: 'Actions',
sortOrder: 4,
// ... (see seed.ts for full schema)
}
Testimonials¶
{
id: 'default-testimonials',
type: 'testimonials',
label: 'Testimonials',
category: 'Content',
sortOrder: 5,
// ... (see seed.ts for full schema)
}
Contact Form¶
{
id: 'default-contact-form',
type: 'contact-form',
label: 'Contact Form',
category: 'Actions',
sortOrder: 6,
// ... (see seed.ts for full schema)
}
4. Email Templates (4 templates)¶
Campaign Email to Representative¶
Key: campaign-email
Category: INFLUENCE
Variables: CAMPAIGN_TITLE, MESSAGE, USER_NAME, USER_EMAIL, POSTAL_CODE, RECIPIENT_NAME, RECIPIENT_LEVEL, ORGANIZATION_NAME, TIMESTAMP
File Locations:
- HTML: api/src/templates/email/campaign-email.html
- Text: api/src/templates/email/campaign-email.txt
Seeding Logic:
const templateDef = {
key: 'campaign-email',
name: 'Campaign Email to Representative',
description: 'Email sent when a constituent contacts their elected representative through a campaign',
category: EmailTemplateCategory.INFLUENCE,
subjectLine: '{{CAMPAIGN_TITLE}} - Message from {{USER_NAME}}',
isSystem: true,
variables: [
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', isRequired: true, sampleValue: 'Support Climate Action Bill C-12' },
{ key: 'MESSAGE', label: 'Message Body', isRequired: true, sampleValue: 'I urge you to support...' },
// ... 7 more variables
],
};
const htmlContent = fs.readFileSync(path.join(templatesDir, `${templateDef.key}.html`), 'utf-8');
const textContent = fs.readFileSync(path.join(templatesDir, `${templateDef.key}.txt`), 'utf-8');
const template = await prisma.emailTemplate.create({
data: {
...templateDef,
htmlContent,
textContent,
createdByUserId: admin.id,
variables: {
create: templateDef.variables,
},
},
});
Response Verification¶
Key: response-verification
Category: INFLUENCE
Variables: CAMPAIGN_TITLE, RESPONSE_TYPE, RESPONSE_TEXT, SUBMITTER_NAME, SUBMITTED_DATE, VERIFICATION_URL, REPORT_URL, ORGANIZATION_NAME, TIMESTAMP
Shift Signup Confirmation¶
Key: shift-signup-confirmation
Category: MAP
Variables: ORGANIZATION_NAME, USER_NAME, USER_EMAIL, SHIFT_TITLE, SHIFT_DATE, SHIFT_TIME, SHIFT_LOCATION, IS_NEW_USER, TEMP_PASSWORD, LOGIN_URL
Shift Details Reminder¶
Key: shift-details
Category: MAP
Variables: ORGANIZATION_NAME, USER_NAME, SHIFT_TITLE, SHIFT_DATE, SHIFT_START_TIME, SHIFT_END_TIME, SHIFT_LOCATION, SHIFT_DESCRIPTION, CURRENT_VOLUNTEERS, MAX_VOLUNTEERS, SHIFT_STATUS
Seed Script Structure¶
Main Function¶
async function main() {
console.log('Seeding database...');
// 1. Create admin user
const admin = await createAdminUser();
// 2. Create map settings
await createMapSettings();
// 3. Create page blocks
await createPageBlocks();
// 4. Seed email templates
await seedEmailTemplates(admin);
console.log('Seed complete.');
}
Upsert Pattern¶
All seed operations use upsert to be idempotent:
await prisma.pageBlock.upsert({
where: { id: block.id },
update: {}, // Don't update if exists
create: block, // Create if doesn't exist
});
Benefits:
- Safe to run multiple times
- Won't duplicate data
- Won't overwrite user changes (empty update clause)
Error Handling¶
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Customizing Seed Data¶
Change Admin Credentials¶
Edit api/prisma/seed.ts:
const hashedPassword = await bcrypt.hash('YourSecurePassword123!', 10);
const admin = await prisma.user.upsert({
where: { email: 'your-email@example.com' }, // Change email
update: {
password: hashedPassword,
emailVerified: true,
status: 'ACTIVE',
},
create: {
email: 'your-email@example.com', // Change email
password: hashedPassword,
name: 'Your Name', // Change name
role: UserRole.SUPER_ADMIN,
emailVerified: true,
},
});
Change Map Default Location¶
Edit api/prisma/seed.ts:
await prisma.mapSettings.upsert({
where: { id: 'default' },
update: {},
create: {
id: 'default',
latitude: 51.0447, // Calgary, AB
longitude: -114.0719,
zoom: 11,
walkSheetTitle: 'Calgary Canvass Walk Sheet',
walkSheetSubtitle: 'District Canvassing',
walkSheetFooter: 'Thank you for volunteering!',
},
});
Add Custom Page Blocks¶
const customBlocks = [
{
id: 'custom-video',
type: 'video',
label: 'Video Embed',
category: 'Media',
sortOrder: 7,
schema: {
videoUrl: { type: 'string', label: 'Video URL' },
caption: { type: 'string', label: 'Caption' },
},
defaults: {
videoUrl: 'https://www.youtube.com/embed/...',
caption: 'Watch our video',
},
},
];
for (const block of customBlocks) {
await prisma.pageBlock.upsert({
where: { id: block.id },
update: {},
create: block,
});
}
Verifying Seed Data¶
Check Admin User¶
Check Map Settings¶
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT * FROM map_settings;"
Check Page Blocks¶
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT id, type, label FROM page_blocks ORDER BY sort_order;"
Check Email Templates¶
docker compose exec v2-postgres psql -U changemaker_v2 changemaker_v2 -c "SELECT key, name, category FROM email_templates;"
Troubleshooting¶
Error: "Unique constraint failed on email"¶
Cause: Admin user already exists
Solution: Seed uses upsert, so this shouldn't happen. Check seed script for typos.
Error: "Template files not found"¶
Cause: Email template .html/.txt files missing
Solution: Ensure api/src/templates/email/ directory contains:
- campaign-email.html
- campaign-email.txt
- response-verification.html
- response-verification.txt
- shift-signup-confirmation.html
- shift-signup-confirmation.txt
- shift-details.html
- shift-details.txt
Error: "Cannot find module 'bcryptjs'"¶
Cause: Dependencies not installed Solution:
Seed doesn't run after migration¶
Cause: package.json missing prisma.seed config
Solution: Add to api/package.json:
Production Seeding¶
Initial Deployment¶
# 1. Deploy migrations
docker compose exec api npx prisma migrate deploy
# 2. Run seed
docker compose exec api npx prisma db seed
# 3. Change admin password immediately
# Login at https://app.cmlite.org with admin@cmlite.org / ChangeMe2025!
# Navigate to /app/profile, update password
Subsequent Deployments¶
Don't re-run seed unless adding new seed data (new page blocks, email templates, etc.). Existing seed data uses upsert with empty update clause, so it won't overwrite user changes.
Related Documentation¶
- Database Overview — Complete ER diagram
- Schema Reference — All model fields
- Migration Workflow — Prisma migrations
- Auth Models — User model details
- Settings Models — MapSettings details
- Landing Page Models — PageBlock details
- Email Template Models — EmailTemplate details