4.7 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Write JSVM Migrations as pb_migrations/*.js Files | HIGH | JSVM migrations look different from Go ones; missing the timestamp prefix or the down-callback silently breaks replay | jsvm, migrations, pb_migrations, schema, extending |
Write JSVM Migrations as pb_migrations/*.js Files
JSVM migrations live in pb_migrations/ next to the executable. Unlike Go migrations (which use init() + m.Register(...) inside a package imported from main.go), JSVM migrations are auto-discovered by filename and call the global migrate() function with an up callback and an optional down callback. --automigrate is on by default in v0.36+, so admin-UI changes generate these files for you; you also write them by hand for data migrations, seed data, and index changes that the UI can't express.
Incorrect (wrong filename format, missing down, raw SQL without cache invalidation):
// pb_migrations/add_audit.js ❌ missing <unix>_ prefix - never runs
migrate((app) => {
// ❌ Raw ALTER TABLE leaves PocketBase's internal collection cache stale
app.db().newQuery(
"ALTER TABLE posts ADD COLUMN summary TEXT DEFAULT ''"
).execute();
});
// ❌ No down callback - `migrate down` cannot revert this in dev
Correct (timestamped filename, collection API, both up and down):
// pb_migrations/1712500000_add_audit_collection.js
/// <reference path="../pb_data/types.d.ts" />
migrate(
// UP - runs on `serve` / `migrate up`
(app) => {
const collection = new Collection({
type: "base",
name: "audit",
fields: [
{ name: "action", type: "text", required: true },
{ name: "actor", type: "relation", collectionId: "_pb_users_auth_", cascadeDelete: false },
{ name: "meta", type: "json" },
{ name: "created", type: "autodate", onCreate: true },
],
indexes: [
"CREATE INDEX idx_audit_actor ON audit (actor)",
"CREATE INDEX idx_audit_created ON audit (created)",
],
});
app.save(collection);
},
// DOWN - runs on `migrate down N`
(app) => {
const collection = app.findCollectionByNameOrId("audit");
app.delete(collection);
},
);
Seed data migration (common pattern):
// pb_migrations/1712500100_seed_default_tags.js
/// <reference path="../pb_data/types.d.ts" />
migrate(
(app) => {
const tags = app.findCollectionByNameOrId("tags");
for (const name of ["urgent", "bug", "feature", "docs"]) {
const r = new Record(tags);
r.set("name", name);
app.save(r); // `app` here is the transactional app - all or nothing
}
},
(app) => {
const tags = app.findCollectionByNameOrId("tags");
for (const name of ["urgent", "bug", "feature", "docs"]) {
const r = app.findFirstRecordByFilter(
"tags",
"name = {:name}",
{ name },
);
if (r) app.delete(r);
}
},
);
CLI commands (same as Go migrations):
./pocketbase migrate create "add_audit_collection" # templated blank file
./pocketbase migrate up # apply pending
./pocketbase migrate down 1 # revert last
./pocketbase migrate history-sync # reconcile _migrations table
Rules:
- Filename format:
<unix_timestamp>_<description>.js. The timestamp sets ordering. Never renumber a committed file. - The
appargument is transactional: every migration runs inside its own transaction. Throw to roll back. Do not capture$appfrom the outer scope - use theappparameter so the work participates in the tx. - Use the collection API (
new Collection,app.save(collection)), not rawALTER TABLE. Raw SQL leaves PocketBase's in-memory schema cache stale until the next restart. - Always write the down callback in development. In production, down migrations are rare but the callback is what makes
migrate down 1work during emergency rollbacks. - Do not import from other files - goja has no ES modules, and at migration time the
pb_hooksloader has not necessarily run. Keep each migration self-contained. - Commit
pb_migrations/to version control. Never commitpb_data/. - Conflicting with Go migrations: you can run either Go or JS migrations, not a mix of both in the same project. JSVM migrations are enabled by default; Go migrations require
migratecmd.MustRegister(...)inmain.go.
Reference: Extend with JavaScript - Migrations