@pathmx/deploy updates writable PathMX sites over SSH. It adopts an existing
macOS user LaunchAgent running checkout-local pmx serve.
The authoring and server checkouts must share a canonical network Git repository. Discovery requires SSH access and Python 3; activation requires a compatible project-local deployment package and Bun on the server.
First complete manual server bootstrap and verify its running LaunchAgent and health. Return to the authoring checkout to install/configure the deployment target:
bun run pmx plugins add deploy
bun run pmx deploy init --host user@server --directory /absolute/checkout
In a terminal, omit host and directory to enter them when prompted. Setup verifies repository identity, selects the checkout's service, and saves local configuration. It does not install packages or change configuration on the server. Bootstrap the server with compatible project dependencies before its first deployment. Commit and push local configuration before deploying. Success prints “Saved production in package.json” and the dry-run command. If discovery finds no eligible service, return to the bootstrap and health checks instead of assuming init created one.
{
"pathmx": {
"plugins": {
"@pathmx/deploy": {
"targets": {
"production": {
"adapter": "server",
"host": "user@server",
"directory": "/absolute/checkout",
"service": "net.example.site",
"branch": "main",
"liveSources": ["paths/responses"],
"backupPaths": ["uploads"]
}
}
}
}
}
}
Use --name staging to create another target; existing targets are not overwritten.
--service resolves ambiguous discovery. --branch selects the authoring branch,
defaulting to the local current branch. --live-source declares directories where
the site saves work; only those paths may change in production, and checkpoints
commit them. --backup-path adds durable files or directories outside those
Sources and the conventional runtime state.
The service must be enabled and running in the user's launchd GUI domain, using
its original plist and direct checkout-local pmx serve. Configure host and port
through the service environment or repository .env. Linked worktrees, symlinked
durable paths, and hidden Git index changes are refused. Ignore .pathmx/ and
.env in Git.
The server checkout stays on the authoring branch, usually main. It needs Git
2.38 or later, a commit identity (git config user.name and user.email), and
push access to the shared repository, because the site lands saved work there.
The server's remote for that repository must have exactly one push URL, and it
must be the shared repository; deployment refuses a different or additional
pushurl.
When HTTPS terminates at a reverse proxy, set PATHMX_ORIGIN to the exact public
origin, for example https://course.example. The upstream connection may use
http://127.0.0.1:3000; browser Actions are checked against the configured public
origin, including its scheme and port. Forwarded host/protocol headers alone do
not configure that origin. Preserve the browser's Origin header at the proxy.
When one reverse proxy connects to PathMX, set PATHMX_TRUSTED_PROXY to that
proxy's exact IP address. The server then accepts one valid IP in
X-Forwarded-For from that peer for credential rate limiting. It ignores forwarding
headers from every other peer and rejects ambiguous multi-hop values. Without this
setting, the direct socket peer is the client address. If a trusted proxy omits or
malforms the header, requests share the limiter's unknown-client bucket rather than
mistaking the proxy socket for a unique visitor.
Deployment installs a checkpoint LaunchAgent named after the service, for example
net.example.site.checkpoint. It runs when loaded and then every minute:
- It copies each SQLite database in the service's database directory to
.pathmx/deploy/backups/latest/and keeps an hourly copy for 30 days under.pathmx/deploy/backups/hourly/. Copies are consistent snapshots checked withPRAGMA quick_check, taken while the site keeps running. - It commits changed files under
liveSourcesas “Checkpoint saved work”. - It lands that commit on the authoring branch of the shared repository. When
authors have pushed since the last deployment, it pushes a merge commit built
without changing the running checkout, so the site keeps serving its deployed
revision until the next
pmx deploy.
Authors receive saved work with git pull. While learners are saving, the shared
branch usually moves every minute, so pull before pushing. Anyone who can read the
shared repository can read the saved work in it; keep course repositories private.
If an author and the site changed the same file, the checkpoint keeps its commit
on the server and stops landing. pmx deploy --dry-run lists the files as a
blocker. Make those files on the authoring branch match the server checkout, for
example from git -C /absolute/checkout show HEAD:<path> on the server, then push;
the next checkpoint lands. Each run records its outcome in
.pathmx/deploy/checkpoint/status.json.
The checkpoint and deployment share the directory lock .git/pathmx-checkpoint.lock
and record their owner in it. A checkpoint that finds the lock held skips that
minute. It reclaims a lock left by a checkpoint process that no longer exists, but
never removes a deployment's lock or a lock without an owner record. Those are
recorded in status.json, and the dry run reports one held for more than 10
minutes as a blocker. If a commit fails, for example because the commit identity
is missing, the saved work stays staged and the next checkpoint commits it.
checkpointJobs declares other LaunchAgents that touch the checkout, such as a
standby synchronization job. Deployment's own checkpoint needs no entry. Each
listed job must acquire .git/pathmx-checkpoint.lock before writing and release
it afterward. Configuring its label alone does not make an arbitrary job
cooperate. Verify that contract before adding it with --checkpoint-job <label> or
editing the manifest.
Deployment acquires the lock, pauses job scheduling, and waits for active jobs to finish before stopping the site. It retains the lock through restoring their original loaded/enabled state. Unconfigured related jobs prevent activation. Direct filesystem editors must also remain idle during maintenance.
bun run pmx deploy --dry-run
bun run pmx deploy
bun run pmx deploy staging
One target is selected automatically. With several, supply a name or configure
default beside targets. Preflight checks repository identity, local cleanliness,
selected commit availability, that the server checkout is on the authoring branch,
its Git version, unlanded saved-work conflicts, service discovery, related jobs,
and runner protocol compatibility. It changes no remote application, state,
services, or infrastructure. If remote branch ancestry cannot be established from
local objects, fetch that branch locally and retry.
The dry-run report starts with Deployment preflight: production and identifies
the selected revision, server branch, last checkpoint, service, and any Blocked:
entries. It also
states that service health and backups have not yet been verified. Resolve every
blocker before running activation.
Deployment fetches the exact requested commit and verifies readiness. Before
pausing jobs or stopping the service, it merges that revision with a copy of current
live Sources in a disposable checkout, installs locked dependencies there, and
lints the combined candidate. Conflicts, installation failures and validation
errors at this stage leave the running site and learner files unchanged.
The check uses the project's .env with a fresh temporary database directory;
it does not copy or migrate active production databases.
The runner then coordinates writers, stops the service, checkpoints live Sources, verifies a backup, merges, installs locked dependencies, and lints again before restarting and checking readiness. The report records the live revision, merged tree and time checked by preliminary validation. Learners can save while that check runs, so final validation against the drained checkpoint is still required; a preliminary pass is not a guarantee that activation will succeed.
The backup includes db/, .env, live Source directories (including ignored
files inside them), and backupPaths. The supervisor resolves PATHMX_DB_DIR
from its service environment before project .env; relative values are based on
the project root. Configured external SQLite files and their WAL/SHM companions
are copied into the backup's database/ directory, with the original location and
verified hashes recorded in manifest.json. Keep that directory private and
coordinate its writers before deployment. Symlinks and changing backup contents
are refused. Backups and original supervisor configuration stay private under
.pathmx/deploy/operations/<operation>/.
After readiness, deployment writes the checkpoint configuration, installs or updates the checkpoint job, and resumes jobs. The checkpoint runs at once and lands the deployed revision's saved work.
Success starts with Deployment complete: production and records the deployed
revision, backup, and service/job state. Compare the revision with your intended
commit and open the public site to check representative content and signed-in
behavior. Keep the saved report with deployment evidence.
The installed runner completes the operation even when its own package is replaced. No server-wide daemon is required.
The CLI saves a report under .pathmx/deploy/reports/ and prints its location.
The server also records timestamped operation state. Reports identify revisions,
conflicts, backup locations, verified service state, and exact inspection commands
without including credentials or Source contents.
A preliminary failure leaves the running service unchanged. If newer live edits cause a merge conflict after shutdown, that merge is aborted and the previous application is restarted and checked. Failures after installing dependencies into the live checkout leave maintenance active; Git rollback alone cannot restore runtime state. The operation retains deployment/checkpoint locks when recovery is uncertain. Inspect the saved supervisor configuration and backups with the maintainer before restoring service/jobs or removing locks. Never remove an active runner's locks or reset away server history.
A lost SSH connection means the outcome is unknown, even if the remote runner continues. Inspect the operation report and live service before retrying. These distinctions make the report usable as a self-contained handoff to a privileged maintainer or agent.
Servers set up with an earlier @pathmx/deploy keep production on a separate
branch, often live. Move one once, while no deployment is running.
Upgrade the authoring checkout to the selected release first. A protocol-2 CLI
cannot activate through a protocol-1 runner. Bootstrap only the self-contained
runner from the installed authoring package; leave the running application's
manifest, lockfile and other dependencies in place until activation:
# In the authoring checkout, after installing the selected release:
scp node_modules/@pathmx/deploy/runner.mjs user@server:/absolute/checkout/node_modules/@pathmx/deploy/runner.next.mjs
# On the server, using its configured Bun executable:
cd /absolute/checkout
bun --no-env-file node_modules/@pathmx/deploy/runner.next.mjs --protocol
# Continue only when this prints {"protocol":2}.
cp node_modules/@pathmx/deploy/runner.mjs node_modules/@pathmx/deploy/runner.previous.mjs
mv node_modules/@pathmx/deploy/runner.next.mjs node_modules/@pathmx/deploy/runner.mjs
The runner is one bundled file and needs no imports from the old application. An interrupted upload leaves the installed runner unchanged. Keep the previous runner until the upgrade succeeds. Do not replace a runner while a deployment or checkpoint is in progress.
Then migrate the checkout:
- On the server, stop and remove any custom checkpoint LaunchAgent. Remove its
label from
checkpointJobs, and deletereplicatefrom the target. - In the authoring checkout, merge the server's branch, for example
git fetch user@server:/absolute/checkout liveandgit merge FETCH_HEAD. Resolve conflicts in saved-work files with the server's version (git checkout --theirs -- <path>), then push. - On the server, run
git switch -C mainin the checkout. Files and uncommitted saved work stay as they are. - Review
liveSourcesagainst every place the site can save content, including custom Plugin grants and public pages editable through Studio. Add those exact files/directories to the target. Undeclared saved edits block deployment and remain preserved; Deploy does not infer writable paths from arbitrary Plugins. - Run
bun run pmx deploy --dry-run, thenbun run pmx deploy. Deployment installs the checkpoint job, which lands saved work from then on.