Zero-dependency webhook server that auto-deploys your repos on git push.
No CI provider needed — just Node.js, a JSON config, and a server.
We build apps in Lovable, which syncs every change to a GitHub repo. GitHub Watcher bridges the gap between Lovable's cloud development and our self-hosted infrastructure: every time Lovable pushes to main, this server pulls the code, patches it for our subpath deployment (e.g. /hrms/, /pipeline/), builds it, and copies the output to the web server — all without touching the Lovable project files.
No GitHub Actions YAML, no build minutes to burn, no vendor lock-in. Just a single Node.js process, a JSON config, and a GitHub webhook.
| Scenario | GitHub Actions | GitHub Watcher |
|---|---|---|
| Build minutes | Limited free tier, then paid | Unlimited — your own CPU |
| Self-hosted deploy | Needs SSH keys, runners, or third-party actions | Built-in — deploys locally |
| Subpath SPA patching | Custom scripts in YAML | First-class preBuild config |
| CloudFront invalidation | Extra action + AWS credentials in secrets | Built-in, one config key |
| Cloudflare cache purge | Extra action + API token in secrets | Built-in, one config key |
| Webhook secret rotation | Update repo settings + re-deploy secrets | Edit .secrets, restart PM2 |
| Debugging deploys | Scroll through action logs in browser | pm2 logs github-watcher or ./logs/ |
- Zero dependencies — runs on Node.js standard library only
- Multi-repo — deploy any number of repositories from one instance
- Branch filtering — only deploy pushes to the branch you care about
- Signature verification — validates
X-Hub-Signature-256using HMAC-SHA256 - Pre-build patching — apply find/replace patches before build, auto-reverted after
- Post-deploy hooks — run arbitrary commands after deployment (restart services, notify, etc.)
- CloudFront invalidation — optional CDN cache busting via AWS CLI
- Cloudflare cache purge — optional edge + Worker Cache API purge via Cloudflare API
- Deploy queue — concurrent pushes to the same repo are queued, not dropped
- Deploy stamping — injects commit hash + timestamp into
index.htmlfor traceability - Health check —
GET /healthendpoint for uptime monitoring - PM2 ready — ships with an
ecosystem.config.jsfor production process management
sequenceDiagram
participant L as Lovable
participant GH as GitHub
participant WH as webhook-server.js
participant DS as deploy.sh
participant CF as CloudFront
participant CFL as Cloudflare
L->>GH: git push
GH->>WH: webhook POST
WH->>WH: verify HMAC-SHA256
WH->>DS: spawn
DS->>DS: git pull
DS->>DS: pre-build patches
DS->>DS: build
DS->>DS: copy to deploy path
DS->>DS: stamp index.html
DS->>DS: post-deploy hooks
DS->>DS: revert patches
DS->>CF: invalidation request
CF-->>DS: invalidation created
DS->>CFL: purge cache API
CFL-->>DS: cache purged
DS-->>WH: exit 0
git clone https://github.com/sharpsir-group/github-watcher.git
cd github-watchercp config.example.json config.jsonEdit config.json with your repositories:
{
"repos": {
"your-org/your-repo": {
"name": "My App",
"localPath": "/home/deploy/your-repo",
"deployPath": "/var/www/my-app",
"branch": "main",
"preBuild": [],
"buildCmd": "npm install --include=dev && npm run build",
"distFolder": "dist",
"postDeploy": [],
"cloudfront": {},
"secret": "WEBHOOK_SECRET_MY_APP"
}
}
}Note: Use
npm install --include=devinstead ofnpm ciinbuildCmd. PM2 setsNODE_ENV=production, which causesnpm install/npm cito skip devDependencies (including build tools like Vite). The--include=devflag ensures they are always installed.
cp .env.example .env
chmod 600 .envGenerate a webhook secret and add it:
openssl rand -hex 32
# Paste the output as WEBHOOK_SECRET_MY_APP= in .envAWS and Cloudflare credentials go in the same file (see Configuration Reference below).
git clone git@github.com:your-org/your-repo.git /home/deploy/your-repo
mkdir -p /var/www/my-app# Direct
node webhook-server.js
# Or with PM2 (recommended)
pm2 start ecosystem.config.js
pm2 saveGo to your repository Settings > Webhooks > Add webhook:
| Field | Value |
|---|---|
| Payload URL | http://your-server:9001/ |
| Content type | application/json |
| Secret | The WEBHOOK_SECRET_* value from your .env file |
| Events | Just the push event |
Or use the GitHub CLI:
gh api repos/your-org/your-repo/hooks --method POST \
-f 'name=web' \
-f 'config[url]=https://your-server/webhook/github-watcher' \
-f 'config[content_type]=json' \
-f 'config[secret]=YOUR_SECRET_VALUE' \
-f 'config[insecure_ssl]=0' \
-f 'events[]=push' \
-F 'active=true'| Field | Type | Description |
|---|---|---|
name |
string | Display name used in logs |
localPath |
string | Absolute path to the cloned repository |
deployPath |
string | Where built files are copied to |
branch |
string | Only deploy pushes to this branch |
preBuild |
array | Find/replace patches applied before build (auto-reverted) |
buildCmd |
string | Shell command to build the project |
distFolder |
string | Build output directory (relative to repo root) |
postDeploy |
array | Shell commands to run after deployment |
cloudfront |
object | Optional CloudFront CDN invalidation config |
cloudflare |
object | Optional Cloudflare cache purge config |
secret |
string | Key name in .env for webhook signature verification |
Patches let you modify source files before build without polluting your git history. They are automatically reverted after the build completes (or fails).
{
"preBuild": [
{
"file": "vite.config.ts",
"find": "export default defineConfig({",
"replace": "export default defineConfig({\n base: \"/app/\","
},
{
"file": "src/lib/matrix-sso.ts",
"find": "const BASE_PATH = '/matrix-apps-template'",
"replace": "const BASE_PATH = '/app'"
}
]
}When deploying a Vite + React Router app to a subpath (e.g. /app/), three patches are typically needed:
- Vite
base— so asset URLs (JS, CSS, images) resolve correctly - React Router
basename— so the client-side router matches routes under the subpath - SSO
BASE_PATH— so OAuth redirect URIs point to the correct callback URL (Matrix SSO apps only)
{
"preBuild": [
{
"file": "vite.config.ts",
"find": "export default defineConfig(({ mode }) => ({",
"replace": "export default defineConfig(({ mode }) => ({\n base: \"/app/\","
},
{
"file": "src/App.tsx",
"find": "<BrowserRouter>",
"replace": "<BrowserRouter basename=\"/app\">"
},
{
"file": "src/lib/matrix-sso.ts",
"find": "const BASE_PATH = '/matrix-apps-template'",
"replace": "const BASE_PATH = '/app'"
}
]
}Without the basename patch, the app will load but the router will show a 404 because it doesn't know its routes are prefixed.
Without the BASE_PATH patch, the app will redirect to SSO login with the wrong redirect_uri (e.g. /matrix-apps-template/auth/callback instead of /app/auth/callback), causing an "Invalid redirect_uri" error after authentication.
If your deploy path is behind a CloudFront distribution, configure automatic cache invalidation:
{
"cloudfront": {
"distributionId": "E1XXXXXXXXXX",
"invalidationPaths": ["/*"]
}
}Requires AWS CLI installed and credentials in .env:
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=us-east-1If your site uses a Cloudflare Worker for prerendering or caching, configure automatic cache purge after deploy:
{
"cloudflare": {
"zoneId": "your-zone-id",
"purgeEverything": true,
"apiTokenKey": "CF_API_TOKEN"
}
}| Field | Type | Description |
|---|---|---|
zoneId |
string | Cloudflare zone ID for the domain |
purgeEverything |
boolean | When true, purges all cached content for the zone |
apiTokenKey |
string | Key name in .env whose value is the Cloudflare API token |
The API token needs Zone > Cache Purge > Purge permission. Add it to .env:
CF_API_TOKEN=your-cloudflare-api-tokenRepos without a cloudflare block are unaffected — the purge step is silently skipped.
| Method | Path | Description |
|---|---|---|
GET |
/ or /health |
Health check — returns {"status":"ok"} |
POST |
/ |
Webhook receiver — accepts GitHub push events |
When a valid push event is received, the deploy script runs these steps in order:
- Git pull —
fetch+reset --hardto the configured branch - Pre-build patches — apply configured find/replace transformations
- Build — run the configured build command
- Deploy — copy build output to the deploy path
- Stamp — inject deploy timestamp and commit hash into
index.html - Post-deploy hooks — run any configured post-deploy commands
- Revert patches — restore patched files to their original state
- CloudFront invalidation — create CloudFront invalidation if configured
- Cloudflare cache purge — purge Cloudflare edge + Worker Cache API if configured
If the build fails at any step, patches are reverted and the deploy is aborted.
In production, place the webhook server behind a reverse proxy (Apache, Nginx) with TLS.
ProxyPass /webhook/github-watcher http://127.0.0.1:9001/
ProxyPassReverse /webhook/github-watcher http://127.0.0.1:9001/GitHub webhook Payload URL: https://your-domain/webhook/github-watcher
Each deploy path serving a single-page app needs an .htaccess for client-side routing:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /app/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /app/index.html [L]
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "^index\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>
<FilesMatch "\.(js|css|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>Note:
deploy.shrunsrm -rf "$DEPLOY_PATH"/*before copying, but the*glob does not match dotfiles, so.htaccesssurvives redeploys.
The included ecosystem.config.js is ready for production use:
pm2 start ecosystem.config.js
pm2 save
pm2 startup
pm2 monit
pm2 logs github-watcherTrigger a deploy without a webhook:
./deploy.sh "your-org/your-repo"github-watcher/
├── webhook-server.js # HTTP server — receives and validates webhooks
├── deploy.sh # Build and deploy pipeline
├── config.json # Repository configurations (git-ignored)
├── config.example.json # Example configuration
├── ecosystem.config.js # PM2 process manager config
├── package.json # npm metadata and keywords
├── .env # All secrets and credentials (git-ignored, chmod 600)
├── .env.example # Template for .env
├── logs/ # Deployment logs (git-ignored)
└── README.md
- Webhook signatures are verified using HMAC-SHA256 (
X-Hub-Signature-256) - All secrets and credentials live in a single
.envfile with600permissions - Sensitive files (
.env,config.json,logs/) are git-ignored - Request body size is capped at 10 MB
- The server binds to
0.0.0.0— use a firewall or reverse proxy to restrict access
- Lovable developers deploying to self-hosted infrastructure
- Indie hackers who want CI/CD without GitHub Actions limits
- Self-hosters who prefer control over third-party services
- Teams deploying multiple Vite/React SPAs from one server
- Node.js >= 14
- Git (on the server)
- PM2 (optional, recommended for production)
- AWS CLI (optional, only for CloudFront invalidation)
See CONTRIBUTING.md for development setup and guidelines.
Part of the Sharp Matrix platform · sharpsir.group