📚 Documentation
NexusCMS Deploy is a self-hosted service that listens to push and tag webhooks from GitHub, GitLab, and Bitbucket, then delivers rich Discord embeds to the right channel automatically — with smart routing, encrypted secrets, and delivery analytics.
This documentation covers everything from first deployment to advanced API usage.
Architecture overview
The application is a Node.js 22 + Express server with a SQLite (default) or PostgreSQL backend. All state is stored in the database — there is no external cache or broker required. An in-memory notification queue with exponential backoff handles Discord delivery retries.
Git provider ─── POST /webhooks/{github|gitlab|bitbucket} ──▶ Signature validation
│
Parse & store deploy event
│
Match Discord routing rules
│
Notification queue (retries)
│
Discord Webhook URL ──▶ 💬
🚀 Quick Start
Get NexusCMS Deploy running in under five minutes using Docker Compose.
-
1
Clone the repository
bashgit clone https://github.com/your-org/nexuscms-deploy.git cd nexuscms-deploy
-
2
Create your environment file
Copy the example and fill in your values:
bashcp .env.example .env
Minimum required variables:
envDISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... JWT_SECRET=your-super-secret-jwt-key-min-32-chars ENCRYPTION_KEY=64-char-hex-key # see Configuration section
-
3
Start the stack
bashdocker compose up -d
The dashboard will be available at
http://localhost:3000. -
4
Register your first user
bashcurl -s -X POST http://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"changeme","name":"Admin"}' | jqSave the returned
token— you'll use it as a Bearer token for all API calls. -
5
Register a webhook in your Git provider
Point the webhook URL to your server and select push events:
texthttps://your-server.com/webhooks/github https://your-server.com/webhooks/gitlab https://your-server.com/webhooks/bitbucket
Push a commit and watch a Discord notification appear within seconds.
⚙️ Configuration
All configuration is done via environment variables. Copy .env.example to .env and adjust as needed.
Environment variables reference
| Variable | Description | Default |
|---|---|---|
| Server | ||
| PORT | HTTP port the server listens on. | 3000 |
| NODE_ENV | development | production | development |
| DASHBOARD_ORIGIN | CORS allowed origin for the dashboard. Use * in dev. | * |
| Authentication | ||
| JWT_SECRET | Secret used to sign JWT tokens. Generate with openssl rand -hex 48. Required. | — |
| ENCRYPTION_KEY | 64-char hex key for AES-256-GCM encryption of webhook secrets. Generate with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" | — (plaintext) |
| Discord | ||
| DISCORD_WEBHOOK_URL | Default Discord webhook URL for unmatched deploys. Required. | — |
| DISCORD_MAX_RETRIES | Number of delivery retries on failure. | 3 |
| DISCORD_RETRY_DELAY_MS | Initial backoff delay in milliseconds (doubles each retry). | 1000 |
| Webhook Signing | ||
| GITHUB_WEBHOOK_SECRET | Shared secret for HMAC-SHA256 validation of GitHub payloads. | — |
| GITLAB_WEBHOOK_TOKEN | Secret token validated against the X-Gitlab-Token header. | — |
| BITBUCKET_WEBHOOK_SECRET | Shared secret for HMAC-SHA256 validation of Bitbucket payloads. | — |
| Database | ||
| DB_CLIENT | sqlite | postgres | sqlite |
| DB_PATH | Path to the SQLite file (SQLite only). | ./data/deploys.db |
| DATABASE_URL | Full PostgreSQL connection string. Example: postgresql://user:pass@host:5432/dbname | — |
| DB_SSL | Set true to enable SSL for PostgreSQL. | false |
| Filtering | ||
| ALLOWED_BRANCHES | Comma-separated list of branches to process. Leave empty to accept all. | — (all) |
Generating secrets
# JWT_SECRET (48-byte hex)
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
# ENCRYPTION_KEY (32-byte hex)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Webhook secret (random 32-byte string)
openssl rand -hex 32
🐳 Docker Deployment
The included docker-compose.yml sets up the application with a PostgreSQL 16 database and a persistent volume.
Starting the stack
# Start all services in the background docker compose up -d # View live logs docker compose logs -f app # Stop the stack docker compose down
Health check
The container exposes a health endpoint that Docker uses internally and you can poll externally:
curl http://localhost:3000/health
# → {"status":"ok","timestamp":"2026-04-25T18:00:00.000Z"}
Upgrading
git pull docker compose build --no-cache docker compose up -d
Always back up your ./data volume before upgrading, especially when using SQLite.
🔗 Webhook Setup
GitHub
- Go to your repository → Settings → Webhooks → Add webhook.
- Set Payload URL to
https://your-server.com/webhooks/github. - Set Content type to
application/json. - Set Secret to the value you used for
GITHUB_WEBHOOK_SECRET. - Select Just the push event (or individual events — only
pushis processed). - Enable the webhook and click Add webhook.
NexusCMS Deploy validates the X-Hub-Signature-256 header. Requests with missing or invalid signatures are rejected with 401.
GitLab
- Go to your project → Settings → Webhooks.
- Set URL to
https://your-server.com/webhooks/gitlab. - Set Secret token to the value you used for
GITLAB_WEBHOOK_TOKEN. - Check Push events and Tag push events.
- Click Add webhook.
GitLab sends the token in the X-Gitlab-Token header. Only Push Hook and Tag Push Hook events are processed; all others return 200 ignored.
Bitbucket Cloud
- Go to your repository → Repository settings → Webhooks → Add webhook.
- Set URL to
https://your-server.com/webhooks/bitbucket. - Set Secret to the value you used for
BITBUCKET_WEBHOOK_SECRET. - Under Triggers, select Repository push.
- Click Save.
Per-repository Signing Secrets
The global environment variables (GITHUB_WEBHOOK_SECRET etc.) apply to all
repositories. For per-repository secrets, use the Secrets tab in the
dashboard or the Webhook Secrets API.
Per-repository secrets take precedence over the global one when present.
🔐 API Reference — Authentication
All protected endpoints require a Bearer JWT token in the
Authorization header. Obtain a token by registering or logging in.
Include the token in every protected request:
Authorization: Bearer <your-token>
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | User email address. | |
| password | string | required | Minimum 8 characters. |
| name | string | optional | Display name. |
Response — 201
{
"user": { "id": 1, "email": "[email protected]", "name": "Admin", "plan": "starter" },
"token": "eyJhbGciOiJIUzI1NiIs..."
}
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | ||
| password | string | required |
Response — 200
{ "user": { ... }, "token": "eyJhbGciOiJIUzI1NiIs..." }
Response — 200
{
"user": { "id": 1, "email": "[email protected]", "name": "Admin", "plan": "pro" },
"plan": { "routes": { "used": 3, "limit": null }, "secrets": { "used": 5, "limit": null } }
}
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | New display name. |
| password | string | optional | New password (min 8 chars). |
Response — 200
{ "api_key": "nx_live_a1b2c3d4..." }
📋 API Reference — Deploys
All deploy endpoints require authentication.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number (default: 1). |
| limit | integer | optional | Items per page (default: 20, max: 100). |
| repository | string | optional | Filter by repository name (partial match). |
| provider | string | optional | github | gitlab | bitbucket |
| branch | string | optional | Filter by branch name (partial match). |
Response — 200
{
"data": [
{
"id": 42,
"repository": "org/frontend",
"branch": "main",
"provider": "github",
"commit_count": 3,
"discord_sent": true,
"created_at": "2026-04-25T17:00:00.000Z"
}
],
"total": 284,
"page": 1,
"limit": 20
}
404 if the deploy is not a release or has no changelog..json file attachment..md file attachment.409 if the notification was already delivered.Response — 200
{ "ok": true, "deployId": 42, "messageId": "1234567890123456789" }
🔀 API Reference — Discord Routes
Routing rules define which Discord channel receives notifications based on repository, branch and event type.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| windowMinutes | integer | Look-back window in minutes (default: 1440 = 24 h). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Human-readable label for the rule. |
| webhook_url | string | required | Discord webhook URL to deliver to. |
| match_repository | string | optional | Glob pattern, e.g. org/*. Empty matches all. |
| match_branch | string | optional | Glob pattern, e.g. release/*. Empty matches all. |
| match_event_type | string | optional | all (default) | push | tag |
| priority | integer | optional | Lower number = higher priority (default: 10). First match wins. |
| enabled | boolean | optional | Whether the rule is active (default: true). |
Response — 200
{ "ok": true }
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| webhook_url | string | required | The Discord webhook URL to test. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| repository | string | required | Repository name, e.g. org/frontend. |
| event_type | string | optional | push (default) | tag |
| branch | string | optional | Required when event_type is push. |
| tag_name | string | optional | Required when event_type is tag. |
Response — 200
{
"matched": { "id": 3, "name": "Production", "webhook_url": "https://discord..." },
"fallbackToDefault": false
}
🔑 API Reference — Webhook Secrets
Per-repository signing secrets take precedence over the global environment variable when validating incoming webhook payloads. Secrets are stored encrypted with AES-256-GCM.
abc***xyz).Query parameters
| Parameter | Type | Description |
|---|---|---|
| provider | string | Filter by provider: github | gitlab | bitbucket |
| repository | string | Filter by repository name. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | required | github | gitlab | bitbucket |
| repository | string | required | Repository name, e.g. org/repo. |
| secret | string | required | The webhook signing secret. |
📊 API Reference — Stats & Queue
{"status":"ok"} when the server is running. Suitable for Docker health checks and load-balancer probes.Response — 200
{ "status": "ok", "timestamp": "2026-04-25T18:00:00.000Z" }
🗺️ Guide — Smart Routing
Route different deploy events to different Discord channels using glob-based matching rules.
How rules are evaluated
- All enabled rules are sorted by priority (ascending — lower number wins).
- Each rule is tested against the incoming deploy's
repository,branch, andevent_type. - The first matching rule wins and its
webhook_urlis used. - If no rule matches, the global
DISCORD_WEBHOOK_URLis used as fallback.
Glob pattern examples
| Pattern | Matches |
|---|---|
| org/* | Any repository in the org namespace. |
| */frontend | Any frontend repository regardless of owner. |
| release/* | Any branch starting with release/. |
| main | Exact match for the main branch. |
| (empty) | Matches everything — acts as a wildcard. |
Example setup
// Rule 1 — priority 1: tag releases → #releases
{
"name": "Tag Releases",
"webhook_url": "https://discord.com/api/webhooks/.../releases",
"match_event_type": "tag",
"priority": 1
}
// Rule 2 — priority 5: production branch → #prod-deploys
{
"name": "Production",
"webhook_url": "https://discord.com/api/webhooks/.../prod",
"match_branch": "main",
"match_event_type": "push",
"priority": 5
}
// Rule 3 — priority 10: everything else → #staging-deploys
{
"name": "Staging catch-all",
"webhook_url": "https://discord.com/api/webhooks/.../staging",
"priority": 10
}
📝 Guide — Release Changelogs
When a tag push is received, NexusCMS Deploy automatically generates a structured changelog grouped by conventional commit type.
How it works
- A tag push event is received (e.g.
v2.3.0). - Commits are parsed for conventional commit prefixes:
feat,fix,chore,docs,refactor,perf,test, etc. - Breaking changes (
!suffix orBREAKING CHANGEfooter) are detected and surfaced. - The version is automatically classified as major, minor, or patch.
- The changelog is stored on the deploy and available via the changelog API endpoints.
Downloading a changelog
# Download as Markdown curl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/deploys/42/changelog.md -o changelog.md # Download as JSON curl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/deploys/42/changelog.json -o changelog.json
🔐 Guide — Secret Encryption
Webhook signing secrets are stored at rest using AES-256-GCM authenticated encryption when ENCRYPTION_KEY is configured.
Generating the key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# e.g. → 4a7f3c2e1b9d8a6f5e4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e
Add this value to your .env file:
ENCRYPTION_KEY=4a7f3c2e1b9d8a6f5e4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e
Back up your ENCRYPTION_KEY securely. If you lose it, all stored
webhook secrets become unrecoverable and must be re-entered.
Key rotation
There is no automatic key rotation at this time. To rotate:
delete all secrets from the dashboard, update ENCRYPTION_KEY,
restart the application, and re-enter all secrets.