pb-migrate
GitHub通过 PocketBase 集合 API 以编程方式创建或修改数据模型,自动生成 JS 迁移文件。适用于需动态调整数据库结构、添加字段或变更 API 规则的场景。
Trigger Scenarios
Install
npx skills add fmaclen/canutin --skill pb-migrate -g -y
SKILL.md
Frontmatter
{
"name": "pb-migrate",
"description": "Create or modify PocketBase collections programmatically via the collections API, generating JS migration files automatically. Use this skill when the user asks to create collections, add fields, or change API rules on PocketBase."
}
Generate PocketBase schema changes programmatically through the live collections API. The running PocketBase server has Automigrate: true with TemplateLangJS, so every collection create/update/delete request automatically writes a .js migration file to pocketbase/pb_migrations/.
Prerequisites
- PocketBase running on this checkout's port — each checkout has its own, so read it from the generated
.envrather than assuming the default (see local-servers) - Superadmin credentials (dev defaults are in the pocketbase skill)
Every example below reaches PocketBase through $PUBLIC_PB_URL, so export the checkout's .env first:
set -a; source .env; set +a
How It Works
- Authenticate as superuser and keep the token
- Send the schema change to
/api/collectionswith that token - PocketBase writes a
.jsmigration file topocketbase/pb_migrations/for the change
The key insight: the migratecmd plugin in pocketbase/main.go writes JS migrations (not Go) via TemplateLangJS, and the automigrate hooks fire on the collection request endpoints. Any HTTP client will do — what matters is that the change travels over /api/collections, because a schema edit made any other way generates no migration file.
Authentication
TOKEN=$(curl -s "$PUBLIC_PB_URL/api/collections/_superusers/auth-with-password" \
-H 'Content-Type: application/json' \
-d '{"identity":"superadmin@example.com","password":"123qweasdzxc"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
Every request below sends that token as Authorization: Bearer $TOKEN.
Inspecting the Live Schema
Collection endpoints accept a collection name or ID, but relation fields need the target's ID. List the live schema to find one:
curl -s "$PUBLIC_PB_URL/api/collections?perPage=200" \
-H "Authorization: Bearer $TOKEN"
Creating a New Collection
POST /api/collections with the full collection payload.
curl -s "$PUBLIC_PB_URL/api/collections" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "myCollection",
"type": "base",
"listRule": "owner = @request.auth.id",
"viewRule": "owner = @request.auth.id",
"createRule": "owner = @request.auth.id",
"updateRule": "owner = @request.auth.id",
"deleteRule": "owner = @request.auth.id",
"fields": [
{
"name": "myRelation",
"type": "relation",
"collectionId": "<target_collection_id>",
"required": true,
"minSelect": 0,
"maxSelect": 1,
"cascadeDelete": true
},
{ "name": "myText", "type": "text", "required": true, "min": 0, "max": 0, "pattern": "" },
{
"name": "mySelect",
"type": "select",
"required": true,
"maxSelect": 1,
"values": ["OPTION_A", "OPTION_B"]
},
{ "name": "myBool", "type": "bool", "required": false }
],
"indexes": ["CREATE UNIQUE INDEX idx_name ON myCollection (field1, field2)"]
}'
Updating an Existing Collection (e.g. Rule Changes)
PATCH /api/collections/<name_or_id> with only the properties you want to change.
curl -s "$PUBLIC_PB_URL/api/collections/myCollection" \
-X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"listRule": "owner = @request.auth.id",
"viewRule": "owner = @request.auth.id"
}'
Adding or changing a field is the same call with a fields array — but that array replaces the collection's entire field set, and PocketBase reconciles it against the live schema by field id, not by name. Fetch the collection first and send its existing fields back with their real ids alongside the new one.
A field sent without an id gets a deterministic one built from its type and a checksum of its name, so re-sending an unchanged field lands back on the original id and keeps its data. Change the name or the type and the id changes with it: PocketBase drops the old column and creates an empty one, which is how a rename written from memory silently empties a column. Carrying the real id is what makes a rename a rename.
Nothing here fails loudly. Fields left out of the array are deleted without warning, and omitted system fields are silently re-added rather than rejected, so there is no error to catch — only the field ids protect you.
Verification
After each request:
- The response is
200and echoes back the collection - A new
.jsfile appeared inpocketbase/pb_migrations/ src/lib/pocketbase.schema.tspicked up the change — the running server regenerates it whenever a migration file lands
Important
- Never hand-write a schema migration. Schema changes go over
/api/collectionsso PocketBase generates the file. Hand-written migrations are for data repairs only —pocketbase/pb_migrations/1783785600_repair_currency_rollout.jsbackfills currency rows and touches no schema. - Never use the raw
app.Save()Go API or direct DB writes for schema. They bypass the automigrate hooks, so no migration file is written and the change never reaches other environments. pocketbase migrate collectionswrites a full schema snapshot rather than an incremental diff — it is not a substitute for the HTTP path.- If PocketBase was restarted after a
main.gochange, the binary must be recompiled first (thebun run pbscript handles this).
Version History
- 28eb754 Current 2026-09-23 01:22


