Seeders
Seeders populate your database with initial data — admin accounts, demo users, default categories, or any test data you need during development. Grit scaffolds a ready-to-use seed system with a dedicated command and extensible seeder functions.
Running Seeders
After running migrations, seed your database with initial data:
The seed command first ensures all tables exist (by running migrations), then executes each seeder. Seeders are idempotent — running them multiple times won't create duplicate records:
Database connected successfullyRunning migrations...All tables are up to date — nothing to migrate.Seeding database...Created admin user: admin@example.com / passwordCreated user: jane@example.com / passwordCreated user: robert@example.com / passwordCreated user: emily@example.com / passwordCreated user: michael@example.com / passwordDatabase seeded successfully.
Default Seeders
Every Grit project ships with two seeders: an admin account and a set of demo users. These are defined in internal/database/seed.go.
Admin User
Creates the default admin account for accessing the admin panel:
func seedAdminUser(db *gorm.DB) error {var count int64db.Model(&models.User{}).Where("email = ?", "admin@example.com").Count(&count)if count > 0 {log.Println("Admin user already exists, skipping...")return nil}admin := models.User{Name: "Admin",Email: "admin@example.com",Password: "password",Role: "admin",Active: true,}if err := db.Create(&admin).Error; err != nil {return fmt.Errorf("creating admin user: %w", err)}log.Println("Created admin user: admin@example.com / password")return nil}
Important: Change the default admin password immediately in production. The password is hashed via bcrypt in the BeforeCreate hook.
Demo Users
Creates sample user accounts with different roles for testing:
func seedDemoUsers(db *gorm.DB) error {users := []models.User{{Name: "Jane Cooper", Email: "jane@example.com", Password: "password", Role: "editor", Active: true},{Name: "Robert Fox", Email: "robert@example.com", Password: "password", Role: "user", Active: true},{Name: "Emily Davis", Email: "emily@example.com", Password: "password", Role: "user", Active: true},{Name: "Michael Chen", Email: "michael@example.com", Password: "password", Role: "user", Active: false},}for _, u := range users {var count int64db.Model(&models.User{}).Where("email = ?", u.Email).Count(&count)if count > 0 {continue}if err := db.Create(&u).Error; err != nil {log.Printf("Warning: failed to create user %s: %v", u.Email, err)continue}log.Printf("Created user: %s / password", u.Email)}return nil}
Blog Posts
Every Grit project includes built-in blog seed data via the seedBlogs() function. This creates 4 sample blog posts (3 published, 1 draft) to demonstrate the blog feature out of the box. Like all seeders, it is idempotent and safe to run multiple times.
func seedBlogs(db *gorm.DB) error {blogs := []models.Blog{{Title: "Getting Started with Grit", Slug: "getting-started-with-grit", Status: "published", ...},{Title: "Building APIs with Go & Gin", Slug: "building-apis-with-go-gin", Status: "published", ...},{Title: "Admin Panels Made Easy", Slug: "admin-panels-made-easy", Status: "published", ...},{Title: "Advanced Grit Patterns", Slug: "advanced-grit-patterns", Status: "draft", ...},}for _, blog := range blogs {var count int64db.Model(&models.Blog{}).Where("slug = ?", blog.Slug).Count(&count)if count > 0 {continue}if err := db.Create(&blog).Error; err != nil {log.Printf("Warning: failed to create blog %s: %v", blog.Title, err)continue}log.Printf("Created blog: %s", blog.Title)}return nil}
The blog seeder is automatically called by the Seed() function alongside the admin and demo user seeders. After running go run cmd/seed/main.go, you can immediately visit the blog pages in the web app to see the sample content.
The Seed Entrypoint
The seed command lives at cmd/seed/main.go. It runs migrations first to ensure all tables exist, then executes the Seed() function:
package mainimport ("fmt""log""os""myapp/apps/api/internal/config""myapp/apps/api/internal/database""myapp/apps/api/internal/models")func main() {cfg, err := config.Load()if err != nil {log.Fatalf("Failed to load config: %v", err)}db, err := database.Connect(cfg.DatabaseURL)if err != nil {log.Fatalf("Failed to connect to database: %v", err)}// Ensure tables exist before seedingfmt.Println("Running migrations...")if err := models.Migrate(db); err != nil {log.Fatalf("Migration failed: %v", err)}fmt.Println("Seeding database...")if err := database.Seed(db); err != nil {log.Fatalf("Seeding failed: %v", err)}fmt.Println("Database seeded successfully.")os.Exit(0)}
Creating Custom Seeders
Add your own seeders by creating new functions and registering them in the Seed() function. Each seeder should be idempotent — safe to run multiple times without creating duplicates.
Step 1: Write the seeder function
func seedCategories(db *gorm.DB) error {categories := []models.Category{{Name: "Electronics", Slug: "electronics"},{Name: "Clothing", Slug: "clothing"},{Name: "Books", Slug: "books"},{Name: "Home & Garden", Slug: "home-garden"},}for _, cat := range categories {var count int64db.Model(&models.Category{}).Where("slug = ?", cat.Slug).Count(&count)if count > 0 {continue}if err := db.Create(&cat).Error; err != nil {log.Printf("Warning: failed to create category %s: %v", cat.Name, err)continue}log.Printf("Created category: %s", cat.Name)}return nil}
Step 2: Register it in Seed()
Add your seeder call to the Seed() function. If you used grit generate resource, the generator automatically adds it via the // grit:seeders marker:
func Seed(db *gorm.DB) error {if err := seedAdminUser(db); err != nil {return fmt.Errorf("seeding admin user: %w", err)}if err := seedDemoUsers(db); err != nil {return fmt.Errorf("seeding demo users: %w", err)}if err := seedCategories(db); err != nil {return fmt.Errorf("seeding categories: %w", err)}// grit:seedersreturn nil}
Best Practices
Follow these patterns when writing seeders:
| Pattern | Why |
|---|---|
| Check before creating | Prevents duplicate records when running seeders multiple times |
| Use unique fields for lookups | Check by email, slug, or other unique identifiers — not by name |
| Log what you create | Makes it easy to verify seeding worked correctly |
| Wrap errors with context | Use fmt.Errorf("seeding X: %w", err) for clear error messages |
| Seed parent records first | If Products need Categories, seed categories before products |
Reset & Reseed
A common development workflow is to drop everything and start fresh with seed data. Combine the fresh migration with seeding:
This drops all tables, recreates them from your models, and populates them with seed data. Perfect for resetting your local development environment.