Skip to content
Back to blog
featuresmigrationscli

Ship Schema Migrations to a Running PocketBase Instance

August 8, 2026·Tom
Ship Schema Migrations to a Running PocketBase Instance

Ship Schema Migrations to a Running PocketBase Instance

PocketBase keeps your schema in code. pb_migrations/ is a directory of JavaScript files, each one a versioned change to your collections, applied in order at startup. It’s one of the best things about the project: your database shape lives in git next to the code that depends on it.

Which made the gap in PocketBase Cloud embarrassing. The archive you uploaded when creating an instance was read exactly once. Add a migration the next week and it had nowhere to go — your options were to apply the change by hand in the admin panel and let the two drift apart, or recreate the instance and lose the data.

That’s fixed. Uploading to an instance that already exists now installs both pb_migrations and pb_public.

The workflow

From the CLI, there is no new command — the existing one just works on the hundredth deploy the way it did on the first:

cd db
pb cloud pb deploy    # redeploys the linked instance

The first deploy records the instance in pb.json, so afterwards you don’t name it. Write a migration locally, commit it, run deploy, and the running instance has it.

From the portal: open the instance, go to Deployment, and upload a ZIP of your project directory.

The rules, and why they’re the rules

Four behaviours, each chosen to be the one that can’t lose your work:

pb_migrations/ is merged. Files already on the instance are kept, and yours are added alongside them. This is not the obvious choice — replacing would be simpler — but PocketBase’s own admin UI generates migration files when you change a collection through the web interface. Replacing the directory would silently delete those, and the next restart would try to apply your local history to a database that had already moved past it. Merging keeps both timelines intact.

pb_public/ is replaced. The opposite rule, for the opposite reason. This directory is a build output — whatever your bundler last emitted. If it were merged, a file you deleted from your project would keep being served forever, which is how a stale index.html outlives three deploys and a deleted asset turns into a security surprise. Replacing means what you shipped is what’s served.

The instance restarts only when migrations actually changed. Migrations apply at startup, so a schema change needs a restart. Static files don’t. A deploy that only touches pb_public doesn’t interrupt a single request.

pb_data/ and the PocketBase binary are never writable from an upload. Your database and the server itself are not addressable from a deploy, by any path. The blast radius of a bad archive stops at your own application files.

Hooks travel separately

pb_hooks/ doesn’t ride in the archive. It goes up through the hooks route instead, so that the portal’s hook editor stays in sync with what you pushed and you’re never looking at a stale copy of a file you just deployed.

Two constraints are worth knowing before you hit them:

  • Hooks are flat files. A filename can’t contain a path separator, so a subdirectory inside pb_hooks/ can’t be uploaded.
  • The CLI ships at most 30 hook files per push. That’s a guard rail rather than a platform limit — a pb_hooks directory bigger than that is almost always the wrong directory, and a clear message beats a confusing failure.

Both .js and .json files travel, not just *.pb.js — a hook’s helper modules and its data files are no use to it if they stay on your laptop.

What this unlocks

The reason this matters isn’t convenience, it’s that your deploy is now idempotent and your schema is now reviewable.

A migration is a file in a pull request. Someone can read it, comment on it, and approve it before it touches production — which is not true of a change someone made by clicking around an admin panel at 11pm. And because pb cloud pb deploy behaves identically every time, the same command works from your terminal and from CI with no special-casing for “first deploy.”

A minimal GitHub Actions step is just:

- name: Deploy
  working-directory: db
  env:
    PB_TOKEN: ${{ secrets.PB_TOKEN }}
  run: pb cloud pb deploy --no-input

A note on order

Migrations apply in filename order, and PocketBase’s generated names are timestamp-prefixed, so “order” means “when it was written.” If you and a teammate both generate a migration on the same afternoon and merge them in the other order, the timestamps still decide. Keep migrations small and independent and this never bites; write one that assumes the state left by another and it eventually will.

For the full deploy reference — what goes up, the build block in pb.json, and every flag — see Deploying PocketBase. For running it in CI, see Deploying from GitHub Actions.

Open the portal →