This document outlines the database migration strategy and best practices for the IMS PocketBase BaaS Starter project.
Our migration system uses PocketBase's built-in migration framework with an incremental approach for adding new collections and schema changes. This ensures safe, reversible database changes in both development and production environments.
The initial migration imports the complete database schema from 0001_pb_schema.json and sets up:
- All base collections (users, roles, permissions)
- System collections (_superusers, _authOrigins, etc.)
- Application settings from environment variables
- Initial data seeding (superuser, RBAC data)
For new collections or schema changes, we use targeted migrations that only affect specific collections:
- Export only new collections from PocketBase Admin UI
- Create numbered migration files with specific changes
- Import only the new collections in the migration
- Provide precise rollback functionality
internal/database/
├── migrations/
│ ├── 0001_init.go # Initial schema and setup
│ ├── 0002_add_user_settings.go # User settings collections
│ ├── 0003_add_audit_logs.go # Example: Audit logging
│ └── utils.go # Migration helper functions
├── schema/
│ ├── 0001_pb_schema.json # Complete initial schema
│ ├── 0002_pb_schema.json # User settings collections for migration 0002
│ ├── 0003_pb_schema.json # Example: Future schema additions
│ └── README.md # Schema documentation
└── seeders/
├── rbac_seeder.go # Role-based access control seeding
└── superuser_seeder.go # Superuser creation
The project includes a CLI tool to automatically generate migration files with proper structure and naming conventions.
Generate a new migration using the makefile:
make migrate-gen name=add_user_profilesThis will:
- Scan existing migrations to determine the next sequential number
- Create a properly structured migration file (e.g.,
0003_add_user_profiles.go) - Display the expected schema file location
- Provide next steps for completing the migration
# Generate migration with underscore naming
make migrate-gen name=add_user_settings
# Generate migration with hyphen naming
make migrate-gen name=add-audit-logs
# Generate migration with mixed case (will be converted to kebab-case)
make migrate-gen name=AddNotificationSystemThe CLI generates migration files with this structure:
package migrations
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// Forward migration
schemaPath := filepath.Join("internal", "database", "schema", "0003_pb_schema.json")
// ... schema import logic
// TODO: Add any data seeding specific to these collections
return nil
}, func(app core.App) error {
// Rollback migration
collectionsToDelete := []string{
// TODO: Add collection names to delete during rollback
}
// ... rollback logic
return nil
})
}- Automatic Numbering: Scans existing migrations and assigns the next sequential number
- Name Sanitization: Converts migration names to kebab-case format
- Input Validation: Ensures migration names contain only valid characters
- Duplicate Prevention: Prevents overwriting existing migration files
- Helpful Output: Shows file paths and next steps after generation
To build a standalone binary:
make migrate-gen-buildThis creates bin/migrate-gen which can be used directly:
./bin/migrate-gen add_user_profilesUse the CLI generator to create the migration file:
make migrate-gen name=your_migration_name- Use PocketBase Admin UI to design your new collections
- Test the collections thoroughly in development
- Document the purpose and relationships
- Export only the new collections from PocketBase Admin UI
- Save as
internal/database/schema/XXXX_pb_schema.json(the CLI will tell you the exact filename) - The schema file number should match your migration number
The generated migration file includes TODO comments for customization:
// internal/database/migrations/0002_add_user_profiles.go
package migrations
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// Forward migration
schemaPath := filepath.Join("internal", "database", "schema", "0002_pb_schema.json")
schemaData, err := os.ReadFile(schemaPath)
if err != nil {
return fmt.Errorf("failed to read schema file: %w", err)
}
var collections []interface{}
if err := json.Unmarshal(schemaData, &collections); err != nil {
return fmt.Errorf("failed to parse schema JSON: %w", err)
}
collectionsData, err := json.Marshal(collections)
if err != nil {
return fmt.Errorf("failed to marshal collections: %w", err)
}
if err := app.ImportCollectionsByMarshaledJSON(collectionsData, false); err != nil {
return fmt.Errorf("failed to import collections: %w", err)
}
// Optional: Add any data seeding specific to these collections
return nil
}, func(app core.App) error {
// Rollback migration
collectionsToDelete := []string{"settings", "user_settings"}
for _, collectionName := range collectionsToDelete {
collection, err := app.FindCollectionByNameOrId(collectionName)
if err != nil {
continue // Collection might not exist
}
if err := app.Delete(collection); err != nil {
return fmt.Errorf("failed to delete collection %s: %w", collectionName, err)
}
}
return nil
})
}# Test the migration
make dev
# Check logs to ensure migration runs successfully
make dev-logs
# Test rollback if needed (in development only)- Migration files:
XXXX_descriptive_name.go(e.g.,0002_add_user_settings.go) - Schema files:
XXXX_pb_schema.json(e.g.,0002_pb_schema.json) - Collection names: Use snake_case (e.g.,
settings,user_settings)
- Always test migrations in development first
- Provide rollback functionality for every migration
- Keep migrations focused - one feature per migration
- Document breaking changes in migration comments
- Backup production data before running migrations
- Initial data only: Use seeders for essential data (roles, permissions)
- Make seeding idempotent: Check if data exists before creating
- Separate concerns: Keep schema changes and data seeding separate when possible
- Development: Migrations run automatically with
automigrate: true - Production: Run migrations manually with proper backup procedures
- Testing: Use separate test databases for migration testing
// Import new collection from JSON schema file
// Add any required initial data
// Provide rollback to delete the collection// Export the modified collection
// Import with updated schema
// Handle data migration if field types changed
// Provide rollback to previous schema// Ensure related collections exist
// Add relation fields
// Update access rules if needed
// Test cascade delete behavior- Schema conflicts: Ensure collection IDs don't conflict
- Missing dependencies: Check if related collections exist
- Permission errors: Verify access rules are correctly set
- Data type conflicts: Handle field type changes carefully
- Failed migration: Use rollback function to revert changes
- Corrupted data: Restore from backup and retry
- Schema inconsistency: Export current schema and compare with expected
Before creating a migration:
- Collections designed and tested in development
- Schema exported to numbered JSON file
- Migration file created with proper rollback
- Migration tested locally
- Documentation updated
- Backup procedures planned for production
# Generate the migration
make migrate-gen name=add_user_profiles
# Design collections in PocketBase Admin UI:
# - user_profiles (relation to users)
# - profile_settings
# - user_preferences
# Export to internal/database/schema/0003_pb_schema.json
# Update rollback function:
collectionsToDelete := []string{"user_profiles", "profile_settings", "user_preferences"}# Generate the migration
make migrate-gen name=add_audit_logs
# Design collections:
# - audit_logs (user actions, timestamps, metadata)
# - audit_settings (retention policies)
# Export to internal/database/schema/0004_pb_schema.json
# Update rollback function:
collectionsToDelete := []string{"audit_logs", "audit_settings"}# Generate the migration
make migrate-gen name=add_notification_system
# Design collections:
# - notifications (user notifications)
# - notification_templates (email/push templates)
# - notification_preferences (user preferences)
# Export to internal/database/schema/0005_pb_schema.json
# Update rollback function:
collectionsToDelete := []string{"notifications", "notification_templates", "notification_preferences"}# Generate the migration
make migrate-gen name=add_cms_collections
# Design collections:
# - articles (blog posts, content)
# - categories (content categorization)
# - tags (content tagging)
# - media (file attachments)
# Export to internal/database/schema/0006_pb_schema.json
# Update rollback function:
collectionsToDelete := []string{"articles", "categories", "tags", "media"}# Different naming styles (all converted to kebab-case)
make migrate-gen name=add_user_settings # → 0003_add-user-settings.go
make migrate-gen name=AddUserSettings # → 0003_adduserasettings.go
make migrate-gen name=add-user-settings # → 0003_add-user-settings.go
# Complex migration names
make migrate-gen name=refactor_user_authentication_system
# → 0004_refactor-user-authentication-system.go
# Simple migrations
make migrate-gen name=fix_permissions # → 0005_fix-permissions.go
make migrate-gen name=update_schema # → 0006_update-schema.goFor migration-related issues:
- Check the troubleshooting section above
- Review PocketBase migration documentation
- Test in development environment first
- Create an issue with detailed error logs