Skip to content

Changemaker Control Panel (CCP)

Need help getting set up?

Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | admin@bnkops.ca

The Changemaker Control Panel is a multi-tenant management layer for operators who run multiple Changemaker Lite instances from a single server. It provides a web UI to provision, monitor, and maintain a fleet of instances without manual configuration.

Single instance?

If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with First Steps.


When to Use CCP

CCP is designed for:

  • Campaign organizations managing instances for multiple chapters or regions
  • Hosting providers offering Changemaker Lite as a managed service
  • Development teams spinning up isolated test instances

CCP handles the entire instance lifecycle: provisioning, configuration, health monitoring, backups, and upgrades — all from a single dashboard.


Architecture

CCP runs as 4 Docker containers alongside (but independent from) your CML instances:

┌──────────────────────────┐
│   CCP Admin GUI (5100)   │  React + Vite + Ant Design
│   Dark theme, SPA        │  Zustand auth store
└────────────┬─────────────┘
┌────────────▼─────────────┐
│   CCP API (5000)         │  Express + TypeScript
│   JWT auth, RBAC         │  Prisma ORM → PostgreSQL
│   Docker socket access   │  Winston logger
└────────────┬─────────────┘
    ┌────────┼────────┐
    ▼        ▼        ▼
ccp-postgres ccp-redis  Docker Socket
(port 5480)  (port 6399)
Service Container Port Description
CCP API ccp-api 5000 Express API with Docker CLI access
CCP Admin ccp-admin 5100 React admin GUI
CCP PostgreSQL ccp-postgres 5480 CCP metadata database
CCP Redis ccp-redis 6399 Rate limiting, caching

Each managed CML instance gets its own isolated set of containers and PostgreSQL database, with ports allocated from non-overlapping ranges.


Setup

1. Run the Setup Script

cd changemaker-control-panel
chmod +x setup.sh
./setup.sh

The setup script:

  • Detects the installation directory and resolves absolute paths
  • Creates instances/ and backups/ directories
  • Copies .env.example to .env if not present
  • Sets INSTANCES_BASE_PATH, BACKUP_STORAGE_PATH, and CML_SOURCE_PATH
  • Generates random secrets for any placeholder values

2. Review Environment

Edit .env and verify the key settings:

Variable Default Description
JWT_ACCESS_SECRET Auto-generated JWT signing key
JWT_REFRESH_SECRET Auto-generated Refresh token signing key
ENCRYPTION_KEY Auto-generated AES-256 key for instance secrets at rest
INITIAL_ADMIN_EMAIL admin@example.com Bootstrap admin email
INITIAL_ADMIN_PASSWORD ChangeMe2025!! Bootstrap admin password
INSTANCES_BASE_PATH ./instances Where instance directories are created
CML_SOURCE_PATH Auto-detected Path to CML source repo for provisioning
BACKUP_STORAGE_PATH ./backups Backup archive storage
PANGOLIN_API_URL Pangolin API for tunnel management
PANGOLIN_API_KEY Pangolin authentication
PANGOLIN_ORG_ID Pangolin organization

3. Start CCP

docker compose up -d

# Run database migrations and seed the admin user
docker compose exec ccp-api npx prisma migrate deploy
docker compose exec ccp-api npx prisma db seed

4. Log In

Open http://localhost:5100 and sign in with the admin credentials from .env.


Creating an Instance

The Create Instance wizard walks through 5 steps:

Step 1: Basic Information

  • Instance name — human-readable label (e.g., "Edmonton Chapter")
  • Slug — URL-safe identifier (e.g., edmonton), used for directory names and compose project
  • Domain — the domain this instance will serve (e.g., edmonton.example.org)

Step 2: Features

Toggle which platform features to enable for this instance:

  • Media Manager
  • Listmonk newsletter sync
  • Payments
  • Rocket.Chat
  • Gancio events
  • Jitsi Meet
  • SMS Campaigns

Step 3: Email

Configure SMTP for the instance, or use MailHog for testing.

Step 4: Tunnel

Optionally configure Pangolin tunnel credentials for public access.

Step 5: Review

Review all settings, then click Create to start provisioning.


Provisioning Flow

When you create an instance, CCP runs a 13-step async provisioning process:

Step What Happens
1 Validate uniqueness (slug + domain)
2 Allocate 4 ports from ranges
3 Generate 14 secrets (passwords, JWT keys, encryption keys)
4 Create Instance record (status: PROVISIONING)
5 Create instance directory
6 Copy CML source code (rsync, excluding node_modules/.git/.env)
7 Decrypt secrets and build template context
8 Render 7 config files from Handlebars templates (docker-compose.yml, .env, nginx configs, Pangolin, Prometheus)
9 Copy static files (nginx.conf)
10 docker compose pull (non-fatal if images are cached)
11 docker compose build
12 Start infrastructure (PostgreSQL + Redis), wait for healthy
13 Start API (runs migrations + seed), then start all remaining services

The admin GUI polls every 3 seconds during provisioning to show progress. When complete, the instance status changes to RUNNING.


Port Allocation

CCP allocates ports from 4 non-overlapping ranges to prevent conflicts between instances:

Range Start End Purpose
API 14000 14999 Express API server
Admin 13000 13999 React admin GUI
PostgreSQL 15400 15499 Database
Nginx 10000 10999 Reverse proxy

Each new instance receives one port from each range. Ports are tracked in the database and released when instances are deleted.


Pages Overview

Dashboard

At-a-glance fleet status:

  • Total instances, running, healthy, degraded, stopped, error counts
  • Instance cards with status indicators and quick actions

Instance List

Searchable, filterable table of all instances with status, domain, health, and creation date.

Instance Detail

5-tab view for each instance:

Tab Content
Overview Status, domain, ports, features, health summary
Services Per-container status grid with restart and log-view actions
Logs Real-time log viewer with service filter, tail count, and time range
Backups Backup list with create, download, and delete actions
Tunnel Pangolin tunnel status and configuration

Backups

Cross-instance backup management:

  • All backups in one table with instance filter
  • Stats: total count, total size, last backup time
  • "Backup All Running" bulk action
  • Download and delete individual archives

Audit Log

Filterable activity trail with 18 action types:

  • Instance lifecycle: CREATE, UPDATE, DELETE, START, STOP, RESTART, UPGRADE
  • Backups: CREATE, DELETE
  • Tunnel: PANGOLIN_SETUP, PANGOLIN_SYNC
  • Users: LOGIN, CREATE, UPDATE, DELETE
  • Settings: UPDATE

Each entry includes timestamp, user, action, instance, IP address, and details (expandable JSON).

Settings

CCP-level configuration:

  • Port ranges
  • Pangolin credentials
  • Default feature flags for new instances
  • Health check interval
  • Backup retention period

Roles

Role Capabilities
SUPER_ADMIN Full access: create/delete instances, manage users, view secrets, delete backups
OPERATOR Manage instances: create, start/stop/restart, backups, health checks
VIEWER Read-only: view instances, logs, health, backups, audit log

Security

  • JWT authentication with 15-minute access tokens and 7-day refresh tokens (atomic rotation)
  • AES-256-GCM encryption for instance secrets stored in the database
  • Audit logging on all operations with IP address capture
  • Role-based access control on all API endpoints
  • Docker socket access restricted to the CCP API container only

Registering an Existing Install (Phone-Home)

Instead of provisioning from scratch, CCP can adopt a Changemaker Lite install that already runs elsewhere. The target host's ccp-agent container phones home with an invite code, you approve the registration in CCP, and CCP issues an mTLS cert bundle for ongoing management.

1. Generate an invite code in CCP

curl -s -X POST $CCP_URL/api/invite-codes \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expiryHours":48}'

The response contains a code like KNZH-B6WW. Invite codes are single-use and expire.

2. Install the target with --ccp-* flags

On the host that will be registered:

bash config.sh -y --enable-all \
  --domain yourdomain.org \
  --admin-password 'StrongPassword1' \
  --ccp-url https://ccp.example.com \
  --ccp-invite-code KNZH-B6WW \
  --ccp-agent-url https://100.90.78.47:7443 \
  # ... tunnel, SMTP, and other flags

All three CCP flags are required together. The wizard:

  • Sets ENABLE_CCP_AGENT=true in .env
  • Appends ccp-agent to COMPOSE_PROFILES (without clobbering monitoring)
  • On docker compose up -d, the ccp-agent container starts and phones home

3. Approve in CCP

# List pending registrations
curl -s $CCP_URL/api/agents/registrations -H "Authorization: Bearer $ADMIN_TOKEN"

# Approve one
curl -s -X POST $CCP_URL/api/agents/registrations/$REG_ID/approve \
  -H "Authorization: Bearer $ADMIN_TOKEN" -d '{}'

On approval, CCP issues a mTLS cert bundle. The agent picks it up on its next poll, writes certs to disk, and restarts into mTLS mode. The instance then shows as RUNNING in CCP with live container status via docker compose ps proxied through the agent.

Approval SLA and rate limits

The agent polls /api/agents/poll every 30s while waiting. The endpoint accepts up to 180 polls per 15 minutes (one every ~5s upper bound), so the agent comfortably handles human-paced approval times. If the agent does hit HTTP 429, it backs off exponentially (30s → 60s → 120s → 300s cap) and resets on the next successful poll — no manual restart required.

4. Deregister on teardown

When you wipe the target host (docker compose down -v + sudo rm -rf), also deregister the instance from CCP. Otherwise the stale Instance row blocks re-registration of the same slug, and CCP will return HTTP 409 SLUG_CONFLICT on the next approval.

# From the target host (or anywhere with network access to CCP):
bash scripts/ccp-deregister.sh \
  --ccp-url https://ccp.example.com \
  --token $ADMIN_TOKEN \
  --yes

Defaults: matches by CCP_AGENT_URL from .env. Use --slug or --instance-id for explicit targeting. Dry-run by default.

The teardown sequence for a registered instance is therefore:

cd ~/changemaker.lite
bash scripts/ccp-deregister.sh --token $ADMIN_TOKEN --yes     # CCP-side
bash scripts/pangolin-teardown.sh --yes                        # Pangolin-side
docker compose --profile monitoring down -v --remove-orphans  # Docker
sudo rm -rf ~/changemaker.lite                                 # Filesystem

Next Steps