Deploy
GitPigeon is single-tenant: every blog deploys its own instance. All platforms share the same core; only the entry point changes.
Prerequisites
- A GitHub repository for comment data (can be separate from your blog repo)
- A Fine-grained PAT (see GitHub setup)
- Cloudflare Turnstile site key and secret (optional; setting both enables the captcha)
- A mail provider: Resend or MailPort (see Mail setup)
Vercel
- Import this repository
- Add every variable from Environment
- Set Framework Preset to Other, leave build and output settings empty
- After deploy, open
/api/health
Netlify
- Import this repository
netlify.tomlalready declarespublish = "public"and the functions directory- Add variables under Site settings → Environment variables
- Open
/api/health
Cloudflare Workers
- Run
npm run buildto producepublic/embed.jsandpublic/review.js - Deploy with
wrangler deploy;wrangler.tomlalready points at the assets directory - Use
wrangler secret putfor sensitive values - Open
https://<worker>.workers.dev/api/health
Automatic deploys with GitHub Actions
The repository ships .github/workflows/deploy-worker.yml, which builds and deploys to Workers on every push to main, and syncs the application variables you set on GitHub into the Worker runtime.
Configure these under Settings → Secrets and variables → Actions. The two authentication values may live in either Secrets or Variables:
| Name | Notes |
|---|---|
CLOUDFLARE_API_TOKEN | API token with the Workers Scripts: Edit permission |
CLOUDFLARE_ACCOUNT_ID | Cloudflare account ID (shown in the dashboard sidebar) |
The workflow reads Secrets first and falls back to Variables. If neither is configured the workflow does not fail: the check job emits a notice and the deploy job is skipped. Once set, future pushes deploy automatically, and you can also trigger it manually from the Actions tab (workflow_dispatch).
Runtime variable sync
Both groups below may be placed in Secrets or Variables; the workflow scans both. When the same name exists in both, precedence depends on the group:
- Sensitive group (secret list): Secrets win; Variables are only used when the Secret is empty
- Plain group (variable list): Variables win; Secrets are only used when the Variable is empty
| Sensitive (shipped as a Worker secret) | Plain (shipped as a Worker var) |
|---|---|
COMMENTS_GITHUB_TOKEN → GITHUB_TOKEN | COMMENTS_REPO |
COMMENTS_SECRET | COMMENTS_BRANCH |
TURNSTILE_SECRET | COMMENTS_PATH |
MODERATOR_EMAIL | SELF_ORIGIN |
RESEND_API_KEY | SELF_REPO |
MAILPORT_API_KEY | ALLOWED_ORIGINS |
TURNSTILE_SITE_KEY | |
MODERATION_REQUIRED | |
MAIL_PROVIDER | |
RESEND_FROM | |
MAILPORT_BASE_URL | |
MAILPORT_PID |
Notes:
- The repository PAT must be named
COMMENTS_GITHUB_TOKEN. GitHub forbids names starting withGITHUB_, so the workflow maps it to the Worker'sGITHUB_TOKEN CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_IDare only used for deployment authentication and are never passed to the Worker- Empty values are not passed, so the Worker keeps its existing value
- Plain values go to
wrangler deployvia--var, with--keep-varsenabled so existing Worker variables are not deleted - Pushes that only touch
comments/**do not trigger a deploy, avoiding pointless redeploys when comments are approved - Confirm the Worker name matches
nameinwrangler.toml - Worker secrets persist. Clearing the GitHub config later does not remove the deployed value; run
wrangler secret deletemanually
Cloudflare Pages
functions/api/[[path]].ts forwards /api/* to the shared router, and public/_routes.json makes only /api/* invoke functions while everything else is served statically.
- In the Cloudflare Dashboard → Workers & Pages → Create → Pages, connect this repository
- Set Build command to
npm run buildand Build output directory topublic - Add every variable from Environment under Settings → Environment variables
- Open
https://<project>.pages.dev/api/health
Notes:
- Pages requires a redeploy after changing environment variables
- If your blog also runs on Pages, make sure its worker routes do not conflict with this system's
/api/* - The Pages free tier has a daily function request limit; usually enough for a comment system
Local deployment (not recommended)
WARNING
Not recommended for production. Local mode is for debugging, LAN use, or a quick integration check. In production it has real drawbacks:
- No TLS: it serves plain
http://. If your blog is onhttps://, browsers block the widget as mixed content (see "HTTPS blogs" below) - No edge cache: every comment read hits the GitHub API live, so latency depends on your network
- The process must stay resident: closing the terminal or rebooting kills the service; you have to run it under a process manager yourself
- Rate limiting is in-memory: a restart clears it and multiple instances do not share it
- No platform Git integration: no deploy-on-push, no automatic rollback
If you just want a reliable comment service for a blog, use one of the platforms above. Continue only if you accept these trade-offs.
1. Prerequisites
| Dependency | Requirement | Check |
|---|---|---|
| Node.js | 20 or newer (the script targets node20) | node --version |
| npm | Ships with Node | npm --version |
| Network | Can reach api.github.com and your mail provider | curl -I https://api.github.com |
Commands below use macOS / Linux. On Windows, use PowerShell or WSL; inline assignments like PORT=9000 become $env:PORT=9000 or set PORT=9000.
2. Clone and install
git clone https://github.com/wool-hmq/GitPigeon.git
cd GitPigeon
npm install3. Build widget assets
npm run buildThis produces public/embed.js and public/review.js. Both are ignored by .gitignore, so you must run this after every fresh clone; otherwise the page loads but the comment area stays empty.
4. Prepare the comments repository and PAT
- Create a repository dedicated to comments (recommended), e.g.
your-name/blog-comments, and make sure it has amainbranch - Create a Fine-grained PAT as described in GitHub setup, select only that repository, and grant
Contents: Read and write - Note the exact
owner/repospelling; it is case-sensitive on GitHub
5. Configure .env
cp .env.example .envEdit .env. The minimal working local configuration is:
# Comments data repository
GITHUB_TOKEN=github_pat_xxxxxxxx
COMMENTS_REPO=your-name/blog-comments
COMMENTS_BRANCH=main
COMMENTS_PATH=comments
# Approval signing and encryption master secret: at least 32 bytes, random
COMMENTS_SECRET=replace-with-a-random-string-of-at-least-32-bytes
# Public URL of this service (the local port address, no trailing slash)
SELF_ORIGIN=http://localhost:8787
# Blog origins allowed to submit, comma-separated, no trailing slash
ALLOWED_ORIGINS=http://localhost:8787Notes:
COMMENTS_SECRETmust be at least 32 bytes. Generate one with:
openssl rand -hex 32- To test the "write directly" path locally, set
MODERATION_REQUIRED=false; no mail variables are needed then - To test email moderation, keep
MODERATION_REQUIRED=trueand fill inMODERATOR_EMAILplus one completeRESEND_*orMAILPORT_*group per Mail setup SELF_ORIGINis used to build review links; update it if you change the port.envis ignored by.gitignore, so it will not be committed by accident; never commit it anywhere
6. Start the server
npm run serve:localThe script bundles dev-server.mjs and starts a Node HTTP server. A successful start prints:
preview server on http://localhost:8787The server reads .env from the project root. Real environment variables with the same name take precedence, which is handy for temporary overrides.
7. Verify
First check the configuration:
curl http://localhost:8787/api/healthExpected:
statusisokgit.readableistruemail.configuredistruewhen moderation is onsameRepoWarningisnullwhen you use a separate repository
If it is not ok, the issues array names the offending fields; see Troubleshooting.
Then open http://localhost:8787/. The page ships with a demo comment area (post=hello-world) you can submit against.
8. Wire it into your blog
Point the blog script at the local service:
<div id="comments"></div>
<script src="http://localhost:8787/embed.js"
data-server="http://localhost:8787"
defer></script>Also make sure the blog origin (e.g. http://localhost:4321) is in ALLOWED_ORIGINS; otherwise submissions fail with 403 origin_not_allowed.
9. Change the port
PORT=9000 npm run serve:localAfter changing the port, update SELF_ORIGIN, ALLOWED_ORIGINS, and the blog's data-server together. Mismatches cause broken review links or CORS rejections.
Common issues
Empty comment area
- Did you run
npm run build(doespublic/embed.jsexist)? - Any CORS errors in the console? Confirm
ALLOWED_ORIGINSincludes the blog origin - Does
/api/comments?post=<slug>return 200?
Submit returns 503
Check issues in /api/health: usually a missing COMMENTS_SECRET, GITHUB_TOKEN, or COMMENTS_REPO, or moderation on with incomplete mail configuration.
Review link does not open
Review links are built from SELF_ORIGIN. Starting on 127.0.0.1 while setting SELF_ORIGIN to localhost (or vice versa) sends the link to a different host. Keep host and port consistent.
Widget fails on an HTTPS blog
An https:// page cannot load an http:// script; browsers enforce this as mixed content. Options:
- Put a reverse proxy with a certificate (Caddy / Nginx) in front of the local service so it is served over
https - Or debug the blog locally over
http://
Stopping and cleanup
Press Ctrl+C to stop. Local mode has no database; all state lives in your comments repository. Deleting the local .env or re-cloning does not affect already approved comments.
Post-deploy checklist
statusisokgit.readableistruemail.configuredistruewhen moderation is onsameRepoWarningisnullwhen you use a separate repository