Branch-based deployment for ProcessWire installations via GitHub. Standalone companion to GitSync.
SiteSync deploys the whole /site/ of a ProcessWire installation from a single GitHub repository – the one currently configured. The admin lists every branch in that repository, a single click switches /site/ atomically to the selected branch, a snapshot of the pre-deploy state is taken before any change, and rollback or restore is one click away. Pushes can be auto-deployed via webhook, with safety guards against silently overwriting local edits.
Purpose
SiteSync turns a single ProcessWire installation into a git-deployed site: every change to /site/ – template, stylesheet, schema migration, content seed – lives on a branch in GitHub, and a Switch / Sync now / webhook delivery brings that branch into production atomically. The DB stays where it is; only the filesystem is mirrored, and only the paths SiteSync was asked to manage.
Single-target deploy is the point: only one repository is active at a time, so the active commit's tree IS the deployed state. Rollback is "switch to a previous SHA", staging is "branch + dedicated SiteSync install", and a code review on GitHub is also a deployment review. Pointing the same installation at a different repository is just a config change – nothing locks the site to a permanent pairing.
What you can build with it
Combined with RockMigrations (schema-as-config under site/RockMigrations/) and the editorial-seed convention under site/content/ – both scaffolded on install – the same git-deploy flow covers far more than file syncing:
- Schema deploys. Fields, templates, roles, and permissions are config files in the repo. A branch that adds
blog_index+blog_articletemplates plus their fields, when switched, applies all of that to the DB on the next module refresh. The diff IS the migration; no imperative scripts. - First-run bootstrap. A fresh PW install with an empty DB, plus the right branch + RockMigrations +
site/content/seeds, produces a complete site after one Switch. - Editorial patches that ride with code. When a content fix really needs to travel in the repo (renaming a section, correcting a seeded copy-block), a cache-gated patch in
site/content/99-patch-…-vN.phpruns once and skips on every subsequent deploy. - AI-assisted authoring, end to end. Direct a coding agent (Claude Code, Cursor, …) from wherever you happen to be: ask it to add a template, design a stylesheet, write a migration, draft article content. The agent pushes its proposal to a feature branch; the branch surfaces in the SiteSync admin as updates available; reviewing the diff on GitHub and tapping Switch finishes the loop. In practice this has produced a complete blog – schema, templates, styling, functionality, and seed articles – built end-to-end on a test installation from the iOS Claude app alone, without a laptop in the loop.
- Safe hotfixes. A direct edit on the server (an emergency typo fix, a one-line debug log) is tracked: the next webhook push to the same path is held back with a clear admin banner until the operator pushes the local change. No silent overwrites.
- Non-developer operators. Branch switching, rollback, and snapshot restore all happen through the PW admin UI. Git knowledge is optional for the people running the site day-to-day.
Working with coding agents
SiteSync ships agent-instruction templates that turn an AI coding agent (Claude Code, Cursor, OpenAI Codex, …) into a viable site collaborator: the agent reads the conventions, edits files on a feature branch, you deploy via the SiteSync admin when the diff looks right.
What gets installed
On module install SiteSync copies two templates from agent-templates/ to the ProcessWire installation root, skipping any file that already exists there:
AGENTS.md– universal agent instructions: branch discipline (never push to the active branch), RockMigrations schema-as-config undersite/RockMigrations/, editorial seeds and patches undersite/content/, and ProcessWire idioms (namespace, sanitizer, image sizing, hooks,Pageimagevs.Pageimages).CLAUDE.md– Claude Code-specific addendum that defers toAGENTS.mdand lists SiteSync's state surfaces (admin Setup page,sitesync.txtlog channel,managed-files.json).
Commit both with the rest of the site repository. From then on any agent opening the repo finds them automatically.
What you can hand to an agent
Anything that maps to files in /site/:
- Schema work – "Add a
blog_articletemplate with hero image, body images, subtitle, and tags." The agent writes config files undersite/RockMigrations/fields/andsite/RockMigrations/templates/; the next Switch applies them on the refresh hook. - Templates and styling – "Build a single-column article view with a full-width header image." The agent writes the PHP and CSS files.
- Content seeding – "Create three seed articles about X, Y, Z under
site/content/." Idempotent guards in the seeds mean a re-deploy never duplicates them. - Editorial patches – "Rename the 'Blog' page to 'Articles'." The agent writes a cache-gated
99-patch-…-vN.phpthat runs once and then skips. - Bug fixes and refactors – same loop, just smaller diffs.
The loop
- Brief the agent in plain language. The agent reads
AGENTS.md/CLAUDE.mdfrom the repo root. - Agent works on a feature branch (
claude/<task>,feature/<name>, or a branch you name explicitly – never the active deploy branch). First push opens a draft PR againstmain, or whichever PR target you've set. - The branch surfaces in the SiteSync admin in the Branches table.
- You review the diff on GitHub. Phone or laptop – same PR review you'd do for any change.
- Click Switch in the admin. SiteSync deploys atomically, RockMigrations applies schema changes on the module refresh, content seeds run if they're new and pass their idempotency guards. The Recent deploys row then shows the commit title alongside the short SHA.
- Webhook auto-deploys subsequent pushes to that branch once it's the active one and
auto_deploy_on_pushis enabled.
Updating the conventions
agent-templates/ is versioned with the module. The install-time copy is one-shot (skips existing files at the PW root), so updates aren't auto-merged into your committed AGENTS.md / CLAUDE.md. To pull in changes, diff your committed versions against the current agent-templates/ in this repository and cherry-pick what's relevant.
Staging environments
For a workflow where a deploy needs review before it reaches live visitors, run two ProcessWire installations against the same GitHub repository: one is the live site, the other is the staging site. SiteSync supports this without any module-side change – each install has its own repo_url, active_branch, webhook secret, and database.
This is the only deployment shape that gives a true preview, because /site/ holds a single tree at any given time and RockMigrations schema changes commit to the install's DB the moment the deploy refreshes modules. Real isolation of "what's deployed" requires real isolation of both filesystem and DB – which means a second PW install.
What you set up
- Two PW installations with separate databases on separate URLs (e.g.
example.comandstaging.example.com). Either two virtual hosts on one server or two separate hosts. - One GitHub repository with at least two branches that matter to deploys:
- A protected live branch (e.g.
live-stableormain) with GitHub branch protection enabled. - A staging branch where feature work lands first (e.g.
staging).
- A protected live branch (e.g.
- SiteSync installed on both installs, pointed at the same
repo_url. Each install has its ownwebhook_secret. active_branchdiffers per install:- Live install:
active_branch = live-stable. - Staging install:
active_branch = staging.
- Live install:
- Two webhooks in GitHub, one per install's
/sitesync-webhook/endpoint. Each install only acts on pushes that match its own active branch, so a push tostagingtriggers only the staging install, a push tolive-stabletriggers only the live install.
The flow
- Feature branch off
staging. Pushfeature/new-blog, open a PR againststaging. - Merge into
staging. The staging install's webhook fires, SiteSync auto-deploys, RockMigrations applies any schema changes, content seeds run – all against the staging DB. - Browse
staging.example.comto see the rendered result. Anything broken stays on staging; live is untouched. - Promote to live by opening a second PR from
stagingintolive-stable, reviewing the cumulative diff of everything that landed on staging since the last promotion, and merging. The live install's webhook fires; live now mirrors what staging has been showing. - Rollback independently on either side via SiteSync's switch history – staging and live each keep their own.
Caveats
- Media diverges.
site/assets/is excluded from sync by design (uploads, sessions, cache, logs). Editorial work that adds images to pages on live needs the images on staging too if staging templates reference them by path. Easiest workaround: keep editorial work on live and let staging be the structural / code-only preview. - Content seeds run on both. A new
site/content/00-seed-foo.phpruns on staging when it lands there and on live when it lands there. The idempotency guards handle that correctly; both installs end up with the same seeded page – usually the intent. - DB drift. Staging and live DBs diverge once editors start touching either. Periodically resetting staging from a live DB dump keeps the preview meaningful for content-rich sites; for schema-and-code-only sites this is a non-issue.
- Branch protection matters more. An accidental direct push to
live-stablebypasses your staging review entirely. GitHub branch protection on the live branch is not optional in this setup.
Features
- Atomic branch switching with file-level diff - only changed blobs travel.
- Switch preview showing exactly which files would be updated, deleted, or preserved, plus any local modifications at risk.
- Tracked sync, not strict mirror - only files SiteSync has actually written are eligible for deletion. Hand-installed modules, hand-written templates, and third-party additions stay untouched.
- Pre-deploy snapshots as tar.gz of
templates/,modules/,classes/, with configurable retention, one-click restore, and a per-switch override checkbox in the preview to skip or force the snapshot independently of the module-level default. - Switch history with rollback to any prior state via exact-commit SHA pinning.
- Webhook endpoint for auto-deploy on push, with conflict detection that refuses to overwrite local edits silently.
- Hardcoded protection list (PW-bootstrap files, secret-bearing files, SiteSync itself) - never touched, regardless of user config.
- Symlink-safe - symlinks in
/site/and git symlink entries in the repository are skipped at both ends. SiteSync never replaces a symlink with a regular file. - Destructive-deploy guard - refuses a deploy that would wipe more than half of
/site/or delete files without adding any. - Pre-flight permission check - aborts before mutation with the full list of unwritable target directories, their mode/owner/group, and the PHP user.
- Optional GitSync token reuse - when the GitSync sister module is installed, SiteSync can reuse its GitHub token instead of duplicating it.
Requirements
- ProcessWire 3.0+
- PHP 8.0+ with
curlandpharextensions - GitHub Personal Access Token with
Contents: readscope for the configured repository (optional for public repos, required for private repos and for higher API rate limits) /site/writable by the PHP process
Installation
- Copy the
SiteSync/directory into/site/modules/. - In the ProcessWire admin: Modules → Refresh → Install SiteSync.
- The webhook HMAC secret is generated automatically on install.
The module exposes its main page at Setup → SiteSync.
Configuration
- Open Setup → SiteSync and click Settings in the header action bar.
- Fill in:
- GitHub Personal Access Token - create a fine-grained token at GitHub → Settings → Developer settings → Personal access tokens. Grant Contents: read on the deploy repository.
- Repository URL - e.g.
https://github.com/owner/repo.
- Save.
The full configuration form covers GitHub connection, webhook, branch protection, path filters, and hooks. Sensible defaults apply on install.
Reusing a token from GitSync
If GitSync is also installed on the site, the GitHub Connection fieldset shows a Use GitSync token checkbox. Enable it to skip maintaining the same Personal Access Token in two modules. SiteSync's own token (when set) always takes precedence; the fallback only fires when the SiteSync token field is empty.
Usage
Switching to a branch
- Setup → SiteSync lists every branch in the repository with its latest commit (SHA + first line of the commit message), date, status, and a Switch action.
- Click Switch on the target branch. The preview page shows the diff summary in a definition list (target commit, repository subpath, files to update, files to delete, files preserved, local changes at risk) plus a collapsible change list with three sections (Updated / Deleted / Preserved). A Create snapshot before switch checkbox sits next to the change-list toggle, pre-filled from the module's snapshot setting – uncheck to skip the tar.gz for this switch only, check to force one against a disabled default. When tracked files have local modifications, an inline warning panel lists the affected paths above the Confirm button.
- Click Confirm switch to apply the change. SiteSync takes the snapshot (if enabled for this switch), writes the new files atomically via
tmp + rename(preserving each existing file's permissions), deletes ones no longer in the branch (only those it manages), refreshes the module cache, and redirects back to the overview.
Syncing the active branch
When new commits land on the currently active branch, the status badge changes to updates available and the row action becomes Sync now. The flow is identical to a switch: preview, confirm, deploy.
Rolling back
The Recent deploys table at the bottom of the page shows the switch history (the most recent 10 entries; up to 50 are persisted). Every row except the live one has a Rollback action that returns /site/ to the state recorded by that entry, via a deploy pinned to the recorded commit SHA.
Restoring from a snapshot
When the live state still matches the very first deploy (the (initial) row in the history) and its pre-deploy snapshot is still on disk, that row gets a Restore snapshot action. It extracts the tar.gz back into /site/ and rebuilds the managed-files set from the restored contents, so the next preview won't flag every restored path as a local conflict.
Once you've switched to another branch or commit, the (initial) row is no longer the current state, and the UI no longer offers a one-click restore. In that case use the manual shell recovery described under Recovery.
The bottom of the admin page also shows the remaining GitHub API rate limit and when it resets, so you can tell a token problem from a transient quota problem at a glance.
Webhook setup
The Webhook Credentials modal in the header action bar shows the endpoint URL and the auto-generated HMAC secret. In your GitHub repository:
- Settings → Webhooks → Add webhook.
- Payload URL: as shown in the modal (
https://yoursite/sitesync-webhook/). - Content type:
application/json. - Secret: as shown in the modal.
- Events: Just the push event.
- Activate.
When auto_deploy_on_push is enabled (default), any push to the currently active branch triggers a deploy. Pushes to other branches are logged and ignored. Per-branch rate limit is one deploy every 30 seconds to absorb push storms.
Conflict hold-back
If a webhook delivery would overwrite a tracked file that has been edited directly on the server since the last deploy, the deploy is refused:
- HTTP 409 is returned to GitHub. The failed delivery is visible in the GitHub webhook overview.
- A red warning banner appears on the SiteSync admin page listing the affected files.
- The banner clears automatically after the next successful deploy - typically after the user pushes the local change and GitHub re-delivers, or after a manual Sync now resolves the situation.
This prevents a hotfix applied directly on the server from being silently wiped by a later push.
Path filters
Two glob-pattern lists in the module config determine which files are in scope:
- Allowlist - a file is eligible if it matches at least one pattern.
- Denylist - a file matching here is always excluded, regardless of the allowlist.
Default allowlist:
templates/**
modules/**
classes/**
content/**
RockMigrations/**
ready.php
init.php
finished.php
*.phpcontent/** covers the editorial-content seed convention; RockMigrations/** covers the schema migration files. Both directories are scaffolded by SiteSync on install and stay empty until the operator (or a coding agent) populates them. Operators upgrading from an older version need to add these two lines manually in Settings → Path Filters — saved configurations are not overwritten by an updated default.
Default denylist:
assets/**
config.php
config-*.php
install.php
.htaccess
sessions/**
logs/**
cache/**
backups/**
templates/admin.phpAlways protected (hardcoded, cannot be disabled):
- PW bootstrap:
templates/admin.php,config.php,config-dev.php,.htaccess,install.php - Secrets:
.env,.htpasswd, plus any file whose basename starts with.env.(covers.env.local,.env.production, etc.) - SiteSync itself: anything under
modules/SiteSync/
Symlinks in /site/ and entries with the git symlink mode (120000) in the repository tree are skipped on both sides. SiteSync neither hashes the link target nor schedules the link itself for deletion, and refuses to overwrite a local symlink with a regular file from the branch.
Tracked vs. mirror sync
SiteSync is not a strict mirror. It persists a per-installation tracking set at site/assets/cache/SiteSync/managed-files.json that records every path SiteSync has actually written, plus the blob SHA from the most recent deploy. Deletion rules:
- A local file is only scheduled for deletion if it is in the managed set AND no longer in the branch.
- Files SiteSync has never written - manually installed PW modules, files created by third-party modules, hand-written templates that never went through a deploy - stay unmanaged indefinitely and are never deleted.
The Files preserved column in the switch preview makes this explicit: every file listed there is safe through the deploy.
Local-modification detection
For every tracked file scheduled for update, SiteSync compares the local blob SHA, the SHA it wrote last time, and the SHA in the branch. If local ≠ last-written, somebody edited the file directly on the server. The preview flags this as a Local change at risk and the inline warning panel lists the affected paths so the user can decide whether to push the change first.
Pre/Post-deploy hooks
In Settings → Hooks & Snapshots:
- Pre-deploy hook - PHP file (path relative to
/site/) executed before any file mutation. A failing pre-hook aborts the deploy before any file is touched. Use for DB backups, activating maintenance mode, etc. - Post-deploy hook - executed after the deploy. A failing post-hook is logged but does not fail the deploy itself (all file mutations have already succeeded). Typical use: trigger RockMigrations, clear external caches (Cloudflare, Redis), restart queue workers.
The hook is included from inside a Wire-context closure. ProcessWire's bare API variables ($modules, $pages, etc.) are not in scope; use the functions API (wire('modules'), pages(), …) or \ProcessWire\wire(…).
Between the file mutations and the post-deploy hook SiteSync calls $modules->refresh() so PW picks up any new module class files. Exceptions raised by third-party hooks attached to Modules::refresh (notably RockMigrations' triggerMigrations when a watched migration file was just deleted by the switch) are logged but do not fail the deploy – the file state on disk is already final at that point.
Combining with RockMigrations
For DB schema changes (fields, templates, roles), the typical pattern is RockMigrations with its config-migration files under site/RockMigrations/{fields,templates,...}/. Push the files, SiteSync syncs them, RockMigrations applies them on the next module refresh - no custom post-deploy hook needed.
Notes:
- The
site/RockMigrations/andsite/content/directories are scaffolded automatically by SiteSync on install, and the default allowlist covers both (RockMigrations/**,content/**). On installations upgraded from an older SiteSync version, the saved path-allowlist may still need the two patterns added manually in Settings → Path Filters. - See the RockMigrations repository for the schema-file format. Imperative migrations in
site/migrate.phpare also supported; SiteSync scaffolds that file too with a thin dispatcher that requires every PHP file undersite/content/. - For sites where AI coding agents (Claude Code, Cursor, etc.) push to feature branches, the
agent-templates/directory in this repository shipsAGENTS.mdandCLAUDE.mdtemplates documenting the branch-discipline, RockMigrations schema convention, and content-seed pattern. On install, SiteSync copies both to the ProcessWire installation root (skipping any file that already exists there), so coding agents picking up the site repository find them automatically. Commit the resulting files with the rest of the site.
Custom post-deploy hooks remain available for cases that don't fit this pattern (clearing external caches, pinging deployment monitors, etc.).
Server permissions
The most common deploy failure is a directory in /site/ that the PHP process cannot write to. Before the first byte is written, SiteSync walks every target directory and refuses the deploy with a combined error listing each unwritable path, its mode and owner/group, and the PHP user the deploy ran as - so the operator can fix the cause in one round-trip.
Typical fix on the server:
find site -type d -exec chmod 0770 {} +
find site -type f -exec chmod 0660 {} +To make these defaults stick when ProcessWire creates new files itself, add to site/config.php:
$config->chmodDir = '0770';
$config->chmodFile = '0660';SiteSync preserves the existing permissions of any file it overwrites (and falls back to $config->chmodFile for brand-new files). It never changes ownership - correcting owner mismatches (often the result of FTP uploads as a different user) requires shell access on the server or a support ticket with the hosting provider.
Branch discipline (recommended)
The module assumes a workflow where the actively deployed branch (typically live-stable or main) is treated as protected, and changes flow in via pull requests from feature branches (e.g. claude/<task>, feature/<name>). Branch protection rules on the GitHub side are a strong prerequisite - SiteSync trusts them. For setups that want a previewable deploy step between feature branch and live, see Staging environments.
Recovery
If a switch leaves /site/ in a broken state:
Rollback to a previous entry in the switch history. The per-row action restores the exact commit recorded for that entry.
Restore snapshot of the pre-deploy state. The UI exposes this as a one-click action only while the live state still matches the very first deploy (see Restoring from a snapshot). In all other cases, restore from the snapshot directory on the shell:
cd site/assets/cache/SiteSync/snapshots/ tar -xzf <newest>.tar.gz -C ../../../../
If the broken state was caused by SiteSync itself being overwritten (and tracked-sync protection wasn't yet in place at that time), restore
site/modules/SiteSync/from the snapshot first so the admin works again.
Known limitations
- DB content (field definitions, template definitions, page tree) lives in the database, not in the filesystem. SiteSync only syncs files. Combine with RockMigrations (or similar) and sync the migration files with the rest of
/site/- see Combining with RockMigrations. site/assets/(uploads, cache, sessions, logs) is excluded by default and not synced.site/config.phpis excluded so live DB credentials stay untouched.- No
gitbinary is used. SiteSync talks to the GitHub REST API only - no SSH keys, no working copy on the server. - GitHub Tree API caps recursive listings at 100 k files. Tree-truncated responses abort the deploy instead of producing a partial sync.
- Line-ending drift between server and repository (LF vs CRLF) changes the blob SHA and shows up as a false-positive local modification in the switch preview, blocking webhook auto-deploy. The shipped
.gitattributesnormalises text files to LF on commit; keep editors configured to LF for files under/site/to avoid this. Single-file overrides via.gitattributesare the right fix when a binary or generated file must stay verbatim.
License
MIT - see LICENSE.
More modules by Mikel
ProcessDataTables
Displays customizable backend tables for any ProcessWire template with flexible column selection, per-field output templates, and global formatting options.27Added 1 year ago by Mikel- Added 8 months ago by Mikel
- Added 1 year ago by Mikel
Textformatter Smart Quotes
Replaces straight quotes "..." with typographic quotes („...“, “...”, or «...»), in visible text only.20Added 9 months ago by Mikel- Added 4 months ago by Mikel
- Added 8 months ago by Mikel
Stripe Payment Links Admin
View customer, purchases & products with configurable metadata columns & filters. Export reports to CSV.13Added 6 months ago by MikelStripePaymentLinks Customer Portal
Adds a ready-to-use /account/ page with login flow, product grid, purchase history table, and direct access to the Stripe Customer Billing Portal11Added 7 months ago by Mikel
Install and use modules at your own risk. Always have a site and database backup before installing new modules.