Skip to main content
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

./pocketbase migrate up
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:
1

Initialize migrations table

Creates the _migrations table if it doesn’t exist.
2

Check applied status

Queries the _migrations table to see if each migration has been applied.
3

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
    })
})
4

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

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);
});
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
Before applying to production:
  1. Create migration in development
  2. Test migrate up
  3. Verify application functionality
  4. Test migrate down
  5. Test migrate up again
  6. Commit migration file
  7. Deploy to production
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
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