Deploy

Send your project's code to a running app from your own machine: pb_hooks, pb_migrations, pb_pages, pb_public, packages and the production node_modules tree. Only changed files are uploaded and the app swaps them in; restart applies new migrations and packages. pocketone dev keeps your folder and the app in step both ways and streams the app's logs; pull and logs work the other way.

pocketone link https://myapp.example.com   # add the remote and sign in (Remotes)
pocketone deploy                           # upload what changed
pocketone logs

Install · Configure · Use · Integrations · Example

Install

Deploy is built into pocketone and on in both directions: every app it serves accepts deploys from its superusers, and the deploy, dev, pull, logs and restart commands are there to deploy to other apps. There's nothing to install.

In your own Go build, register it before the app starts. Remotes gives the commands their remotes and logins, and Tokens lets you deploy with API tokens:

app := pocketbase.New()
tokens.MustRegister(app, tokens.Config{})   // optional: API tokens with the deploy scope
remotes.MustRegister(app, remotes.Config{}) // remotes, login, and /_pocker/whoami discovery
deploy.MustRegister(app, deploy.Config{})   // the deploy API + deploy/dev/pull/logs/restart commands
app.Start()

The module is github.com/pockethost/pocketone/ext/deploy. deploy.Config{DisableServer: true} keeps only the commands, for a binary that deploys elsewhere but must not accept deploys.

Configure

There is no pocketone.yaml section, flag or settings screen. Defaults on the receiving app:

  • Who may deploy: superusers. You sign in with pocketone login (see Remotes) or use an API token. A token needs the deploy scope (or no scopes); deploy:read allows reading files and logs only.
  • Where files land: next to pb_data, PocketBase's usual layout (pb_hooks/, pb_migrations/, pb_public/, node_modules/ beside it).
  • Upload store: uploaded files and past deploys are kept in .pb_deploy/ next to pb_data. It's outside pb_data, so backups skip it, and it's never deployed.

On your machine, the commands use the project's remotes and saved logins from Remotes. In CI, set POCKETONE_TOKEN (or POCKETONE_TOKEN_<REMOTE>) instead of signing in.

Use

All commands take an optional remote name or URL; without one they use the project's default remote. Run them from your project folder (or any folder below it).

deploy

pocketone deploy                      # to the default remote
pocketone deploy prod -m "fix signup" # to the remote "prod", with a note
pocketone deploy --restart            # and restart if migrations or packages changed (CI)
pocketone deploy --dry-run            # list what would be sent, send nothing
pocketone deploy --out /tmp/set       # write the files to a folder instead

You'll see how many files are deployed and how many are uploaded, then a line like Deployed <id>: 3 changed, 0 deleted, 41 unchanged. With nothing changed it says Up to date: no changes.

Flag What it does
--restart Restart the app after the files are in, when migrations or packages changed.
--dry-run List what would be deployed; deploy nothing.
--out <dir> Write the deploy set to a local folder instead of deploying.
-i, --include Add include patterns to the default set.
-e, --exclude Exclude patterns (pb_data is always excluded).
--node-modules Ship the production node_modules tree (the default when there's a package.json).
--no-node-modules Don't ship node_modules; leave the remote's alone.
--commit <sha> Commit to record. Default: $GITHUB_SHA, $CI_COMMIT_SHA, or git HEAD.
-m, --message A note to record with the deploy.
-v, --verbose List every file.

What's sent: files matching pb_* and pb_*/**/* except pb_data, plus package.json, bun.lock(b) and patches/**. Dotfiles are included (pb_public/.well-known); .git* files and symlinks are skipped.

Packages: with a package.json, the production node_modules tree is collected the way Node resolves it (hoisted npm and bun, pnpm's store, workspace links; never devDependencies) and shipped as plain files, so plain JS hooks can require npm packages. The remote then owns its root node_modules: packages you no longer ship are removed there.

Restarts: files are swapped in while the app runs, each rename atomic, and deploy never restarts the app on its own. pb_pages and pb_public are read from disk on each request, and hooks reload by themselves (PocketBase's --hooksWatch, Pocker's watcher). Migrations and packages apply only when the app starts, so a deploy that changes them says Migrations or packages changed and leaves the restart to you: pass --restart (the app restarts once, after the whole batch is in, and you hear if it fails to start), run pocketone restart, or type r in dev. So does the first deploy that creates pb_hooks, since a watcher only sees folders that existed when the app started.

restart

pocketone restart prod

Restarts the app so it applies new migrations and packages, and tells you if it fails to start. An app that isn't running (a sleeping Pocker instance) is left alone; it applies them when it starts.

dev

pocketone dev
pocketone dev prod --prefer local   # first sync: upload yours where both sides differ

Keeps the project folder and the remote in step, both ways, and streams the remote's logs. Stop it with Ctrl+C.

What Direction
pb_hooks, pb_migrations, pb_pages, pb_public and other pb_* folders both ways
package.json, bun.lock(b), patches/ both ways
node_modules uploaded only, when package.json or the lockfile changes
pb_data never (it's the app's live data)
  • Your edits go up half a second after you save.
  • Changes on the remote, such as the migrations the dashboard writes when you edit a collection, come down within a few seconds (--poll, default 5s). dev asks the remote for one hash and only looks further when it changed, so a large pb_public costs nothing while it's quiet.
  • Restarts: dev never restarts the app by itself. Hooks reload by themselves, pages and public files are live on the next request. After you upload a migration or new packages, dev says so: type r and Enter to restart the app and apply them. Migrations that come down are already applied on the remote.
  • Deletes go both ways too.
  • Changed on both sides: your copy wins and is uploaded; the remote's is saved in .pocketone/conflicts/ and dev says so.
  • First run: files that exist on only one side are copied to the other. If a file differs on both sides, dev lists them and asks whether to keep yours or take the remote's; --prefer local|remote answers in advance.

dev keeps what it last synced in .pocketone/sync.json (add .pocketone/ to .gitignore). List files to leave out, such as ones your hooks generate, in .pocketoneignore, one gitignore-style pattern per line.

Flag What it does
--prefer local|remote First sync: who wins where both sides differ (default: ask).
--poll <duration> How often to check the remote (default 5s).
--push-only Only upload, the way dev used to work. Also used by itself for remotes without two-way sync.
--no-logs Don't stream the remote's logs.

It takes the same file flags as deploy.

pull

pocketone pull prod                              # pb_hooks, pb_migrations, pb_pages, pb_public, package.json
pocketone pull prod pb_hooks pb_public/index.html
pocketone pull prod --dry-run

Downloads files from a remote into the project. pb_data is only reachable on an instance that's powered off, so a single app (always running while it answers) refuses it.

logs

pocketone logs prod
pocketone logs prod -n 20

Replays the last 100 lines (-n, --lines, 0 to 1000), then follows. Requests show as GET /url → 200 (1.2 ms) and errors are marked ERR. PocketBase writes its logs in batches, so lines show up a few seconds late. The stream reconnects by itself if it drops.

Deploy history

Every deploy is recorded in the _deploys collection of the receiving app: status, file counts, bytes, commit, message, source, token name, superuser and any error. Superusers can browse it in the dashboard.

Integrations

Works with

Extension How they fit
Remotes Gives the commands their remotes, saved logins and CI tokens. The commands find the deploy API through the remote's /_pocker/whoami.
OAuth Server pocketone login signs in through the browser when the remote runs it.
Tokens Deploy registers the deploy and deploy:read scopes, so you can make API tokens for CI that can only deploy or only read.
Pocker The same API runs for many instances with your own backend: build it with deploy.New (below). PocketHost does this.
PocketHost A remote without /_pocker/whoami is taken to be a PocketHost instance: the commands use /_ph/deploy/v1 and a PocketHost token (POCKETHOST_TOKEN, or the pockethost remote read from .pockethostconfig).

Building on Deploy

  • Go client. deploy.NewClient(ctx, rc) takes a Remotes client and finds its deploy API. Deploy, Pull, Collect, Watch and NewSyncer (two-way sync: Sync, RemoteChanged) are the commands' building blocks.
  • Your own host. deploy.New(deploy.Options{Prefix, Backend, Instances, PBRoot, DataRoot, PlatformPaths, Logs}) builds the API; mount api.Wrap(next). Backend decides which instance a request is for (Lookup(r)), who it acts as (Authenticate(r, inst) → Auth{UserID, Scopes, Via, TokenLabel}, an *AuthError, or ErrNoAccess for a 404) and records history. Instances is pocker's Manager (stop, swap, start). The default FileLogs tails each instance's logs/exec.log, which pocker writes. PocketHost composes it with its own token backend and keeps /_ph/deploy/v1.
  • Restarts. Your Instances should reload an instance by itself when its pb_hooks change (pocker's Manager does); deploy only restarts through it when a client asks (Client.Restart, or DeployOptions.Restart).
  • Discovery. Deploy advertises itself as the deploy feature of /_pocker/whoami, so clients find its path.

Example

example/main.go is an app that both accepts deploys and makes them: the deploy API, API tokens, remotes and the deploy commands. Its comment walks through a first deploy against itself.

// Command example is a PocketOne app that accepts deploys and can deploy: the deploy API (/_pocker/deploy/v1), API
// tokens, remotes, and the deploy/dev/pull/logs commands.
//
//	go run ./example serve --http=127.0.0.1:8090                  # the remote
//	cd myproject && mkdir pb_hooks && echo 'routerAdd("GET", "/hi", (e) => e.string(200, "hi"))' > pb_hooks/main.pb.js
//	go run ./example link http://127.0.0.1:8090 --email [email protected]   # password: password
//	go run ./example deploy
//	curl http://127.0.0.1:8090/hi
//
// Logins (ext/devusers, local testing only): dashboard [email protected] / password, app user [email protected] / password.
package main

import (
	"log"

	"ganigeorgiev.com/pocketbase/pocketbase"
	"ganigeorgiev.com/pocketbase/pocketbase/plugins/jsvm"
	"github.com/pockethost/pocketone/ext/deploy"
	"github.com/pockethost/pocketone/ext/devusers"
	"github.com/pockethost/pocketone/ext/remotes"
	"github.com/pockethost/pocketone/ext/tokens"
)

func main() {
	app := pocketbase.New()
	devusers.Register(app) // local testing: superuser [email protected] and user [email protected], password "password"

	jsvm.MustRegister(app, jsvm.Config{}) // deployed pb_hooks and pb_migrations
	tokens.MustRegister(app, tokens.Config{})
	remotes.MustRegister(app, remotes.Config{})
	deploy.MustRegister(app, deploy.Config{}) // deploys land next to pb_data

	if err := app.Start(); err != nil {
		log.Fatal(err)
	}
}

Deploy API

/_pocker/deploy/v1 (superusers, see Configure):

Route What it does
GET /whoami Who the request acts as, on which instance.
POST /check Takes a manifest, returns the hashes the server is missing.
POST /blobs Gzipped PHB1 bundles: [sha256][uint32 length][bytes]…, every hash verified, 64 MB per request.
POST /commit Stage, swap in, record. "restartNeeded": true in the answer when migrations or packages wait for a restart; "restart": true in the request restarts after the swap. With "partial": true only the listed files (and "delete") change.
POST /restart Restart the app (applies migrations and packages); restarted and any startError.
GET/DELETE /files List or delete files.
GET/PUT /files/raw Download or upload one file.
GET /tree One folder of the code set (?path=, default the root) with a hash over everything below it; hashOnly=1 for just the hash.
GET /logs The app's logs as server-sent events.

The protocol is the pockethost npm CLI's, so the same client deploys to PocketOne apps and PocketHost.

How it works

  • Go module github.com/pockethost/pocketone/ext/deploy. example/main.go is an app that accepts deploys and can deploy (logins from devusers: [email protected] / password).
  • A deploy is a hash diff: the client sends a manifest, uploads only the missing files in bundles, then commits.
  • On a single app the files are swapped in while it runs; a restart (when a client asks) is app.Restart(), which re-executes the process (Config.Restart overrides it; not on Windows).
  • Config.Dir and Config.StoreDir move the deploy folder and the upload store; Config.Prefix moves the API.
  • Logs on a single app come from its own _logs table; entries at error level and above are marked as stderr.
  • The node_modules collector is nodemodules, a port of the npm CLI's.

License

MIT