PocketBase uses a migration system to track and apply database schema changes. Migrations ensure that database modifications are versioned, reproducible, and can be rolled back if needed.
Understanding migrations
PocketBase has two types of migrations:
System migrations : Built into PocketBase core, these handle framework schema updates
App migrations : User-defined migrations for your application’s collections and data
Migrations are stored in the _migrations table and executed in order based on their filename.
Migration file structure
Migrations can be written in JavaScript or Go:
JavaScript migrations
Located in pb_migrations/ by default:
/// < reference path = "../pb_data/types.d.ts" />
migrate (( app ) => {
// Add up queries...
const collection = app . findCollectionByNameOrId ( "posts" );
collection . fields . addAt ( 0 , new Field ({
"name" : "title" ,
"type" : "text" ,
"required" : true
}));
return app . save ( collection );
}, ( app ) => {
// Add down queries...
const collection = app . findCollectionByNameOrId ( "posts" );
collection . fields . removeById ( "field_id" );
return app . save ( collection );
});
Go migrations
Located in migrations/ by default:
package migrations
import (
" github.com/pocketbase/pocketbase/core "
m " github.com/pocketbase/pocketbase/migrations "
)
func init () {
m . Register ( func ( app core . App ) error {
// Add up queries...
collection , err := app . FindCollectionByNameOrId ( "posts" )
if err != nil {
return err
}
// Add field
field := & core . TextField {
Name : "title" ,
Required : true ,
}
collection . Fields . Add ( field )
return app . Save ( collection )
}, func ( app core . App ) error {
// Add down queries...
collection , err := app . FindCollectionByNameOrId ( "posts" )
if err != nil {
return err
}
collection . Fields . RemoveById ( "field_id" )
return app . Save ( collection )
})
}
Setting up migrations
Enable migrations in your PocketBase application:
package main
import (
" log "
" github.com/pocketbase/pocketbase "
" github.com/pocketbase/pocketbase/plugins/migratecmd "
)
func main () {
app := pocketbase . New ()
// Register the migrate command
migratecmd . MustRegister ( app , app . RootCmd , migratecmd . Config {
TemplateLang : migratecmd . TemplateLangJS , // or TemplateLangGo
Automigrate : true ,
Dir : "./pb_migrations" , // custom migrations directory
})
if err := app . Start (); err != nil {
log . Fatal ( err )
}
}
Configuration options
TemplateLang : migratecmd.TemplateLangJS or migratecmd.TemplateLangGo (default: Go)
Automigrate : Enable automatic migration generation on collection changes (default: false)
Dir : Custom migrations directory (default: pb_migrations/ for JS, migrations/ for Go)
Creating migrations
Manual migration creation
Create a blank migration template:
./pocketbase migrate create add_user_bio
This generates a file like 1234567890_add_user_bio.js:
/// < reference path = "../pb_data/types.d.ts" />
migrate (( app ) => {
// add up queries...
}, ( app ) => {
// add down queries...
});
Collections snapshot
Generate a migration with the current collections state:
./pocketbase migrate collections
This creates a migration like 1234567890_collections_snapshot.js:
/// < reference path = "../pb_data/types.d.ts" />
migrate (( app ) => {
const snapshot = [
{
"id" : "abc123" ,
"name" : "users" ,
"type" : "auth" ,
"fields" : [
{
"id" : "field_abc" ,
"name" : "username" ,
"type" : "text" ,
"required" : true
}
]
}
];
return app . importCollections ( snapshot , false );
}, ( app ) => {
return null ;
});
Automigrations
When Automigrate: true is enabled, PocketBase automatically creates migrations when you modify collections through the admin UI.
For example, creating a new collection generates:
// 1234567890_created_posts.js
migrate (( app ) => {
const collection = new Collection ({
"name" : "posts" ,
"type" : "base" ,
"fields" : [
{
"name" : "title" ,
"type" : "text"
}
]
});
return app . save ( collection );
}, ( app ) => {
const collection = app . findCollectionByNameOrId ( "collection_id" );
return app . delete ( collection );
});
Automigrations are prefixed with action names: created_, updated_, or deleted_.
Applying migrations
Apply all pending migrations
Output:
Applied 1234567890_add_user_bio.js
Applied 1234567891_create_posts.js
No new migrations to apply.
Migration execution process
From core/migrations_runner.go:122-173, the Up() method:
Initialize migrations table
Creates the _migrations table if it doesn’t exist.
Check applied status
Queries the _migrations table to see if each migration has been applied.
Execute in transaction
Runs unapplied migrations within a database transaction for safety. err := app . RunInTransaction ( func ( txApp App ) error {
return txApp . AuxRunInTransaction ( func ( txApp App ) error {
// Execute migration Up function
if err := m . Up ( txApp ); err != nil {
return fmt . Errorf ( "failed to apply migration %s : %w " , m . File , err )
}
return nil
})
})
Record migration
Saves the migration record with timestamp to _migrations table.
Migrations run in both the main database and auxiliary database transactions. Any error will rollback all changes.
Reverting migrations
Revert last migration
./pocketbase migrate down
This prompts for confirmation:
1234567891_create_posts.js
Do you really want to revert the last 1 applied migration(s)? (y/N)
Revert multiple migrations
./pocketbase migrate down 3
Reverts the last 3 migrations.
Revert all migrations
./pocketbase migrate down -1
Reverting migrations can cause data loss if the down function drops collections or fields. Always back up before reverting.
Migration best practices
Write reversible migrations
Always implement both up and down functions: migrate (( app ) => {
// Up: Add field
const collection = app . findCollectionByNameOrId ( "users" );
collection . fields . addAt ( 0 , new Field ({
"name" : "bio" ,
"type" : "text"
}));
return app . save ( collection );
}, ( app ) => {
// Down: Remove field
const collection = app . findCollectionByNameOrId ( "users" );
const field = collection . fields . getByName ( "bio" );
collection . fields . removeById ( field . id );
return app . save ( collection );
});
Use descriptive migration names
Good migration names describe the change:
✅ add_user_bio_field.js
✅ create_comments_collection.js
✅ migrate_old_posts_to_new_schema.js
❌ migration1.js
❌ update.js
Test migrations in development first
Before applying to production:
Create migration in development
Test migrate up
Verify application functionality
Test migrate down
Test migrate up again
Commit migration file
Deploy to production
Keep migrations small and focused
Create separate migrations for different changes: # Good: Separate migrations
./pocketbase migrate create add_user_avatar
./pocketbase migrate create add_post_tags
# Avoid: One large migration doing everything
./pocketbase migrate create update_schema
Handle data migrations carefully
When migrating data, handle edge cases: migrate (( app ) => {
const posts = app . findCollectionByNameOrId ( "posts" );
// Fetch all records
const records = $app . findRecordsByFilter (
"posts" ,
"" , // no filter
"-created" ,
500
);
// Update each record
records . forEach (( record ) => {
// Handle null values
if ( ! record . get ( "old_field" )) {
record . set ( "new_field" , "default_value" );
} else {
record . set ( "new_field" , record . get ( "old_field" ));
}
app . save ( record );
});
return null ;
}, ( app ) => {
// Revert data changes
return null ; // or implement reverse migration
});
Advanced migration techniques
Conditional migrations with ReapplyCondition
From core/migrations_list.go:9-14, migrations support conditional reapplication:
type Migration struct {
Up func ( txApp App ) error
Down func ( txApp App ) error
File string
ReapplyCondition func ( txApp App , runner * MigrationsRunner , fileName string ) ( bool , error )
}
Use case: Reapply a migration when certain conditions change:
m . Register ( func ( app core . App ) error {
// Migration logic
return nil
}, func ( app core . App ) error {
// Rollback logic
return nil
})
// Add reapply condition
lastMigration := appMigrations . Item ( appMigrations . Items (). Len () - 1 )
lastMigration . ReapplyCondition = func ( txApp core . App , runner * core . MigrationsRunner , fileName string ) ( bool , error ) {
// Reapply if a specific setting changed
return txApp . Settings (). Meta . AppName != "expected_name" , nil
}
Programmatic migration execution
Run migrations from Go code:
import (
" github.com/pocketbase/pocketbase/core "
)
// Create migrations list
var list core . MigrationsList
list . Copy ( core . SystemMigrations )
list . Copy ( core . AppMigrations )
// Create runner
runner := core . NewMigrationsRunner ( app , list )
// Apply all migrations
applied , err := runner . Up ()
if err != nil {
log . Fatal ( err )
}
for _ , file := range applied {
log . Printf ( "Applied %s " , file )
}
// Revert migrations
reverted , err := runner . Down ( 2 ) // Revert last 2
if err != nil {
log . Fatal ( err )
}
Custom migrations table
Change the default migrations table name:
runner := core . NewMigrationsRunner ( app , list )
runner . tableName = "custom_migrations"
Migration hooks
You can hook into migration events, though this is typically done through the migration files themselves. The migration system triggers events during execution:
// Migrations run inside these hooks automatically
app . OnBeforeServe (). BindFunc ( func ( e * core . ServeEvent ) error {
// System migrations are applied here
return e . Next ()
})
Troubleshooting
Migration already applied
Issue : Migration shows as applied but changes aren’t visible.
Solution : Check the _migrations table:
SELECT * FROM _migrations ORDER BY applied DESC ;
Remove the entry to reapply:
DELETE FROM _migrations WHERE file = '1234567890_migration_name.js' ;
Then run migrate up again.
Migration file not found
Issue : _migrations table references files that don’t exist.
Solution : Sync the migrations table:
./pocketbase migrate history-sync
This removes references to deleted migration files.
Transaction deadlock
Issue : Migration times out or deadlocks.
Solution :
Ensure migrations don’t have long-running operations
Split large data migrations into smaller batches
Avoid external API calls in migrations
Import errors in Go migrations
Issue : undefined: Collection or similar errors.
Solution : Import the correct packages:
import (
" encoding/json "
" github.com/pocketbase/pocketbase/core "
m " github.com/pocketbase/pocketbase/migrations "
)
Next steps
Going to production Learn production deployment best practices
Backups Set up automated backups for your data