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¶
The setup script:
- Detects the installation directory and resolves absolute paths
- Creates
instances/andbackups/directories - Copies
.env.exampleto.envif not present - Sets
INSTANCES_BASE_PATH,BACKUP_STORAGE_PATH, andCML_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=truein.env - Appends
ccp-agenttoCOMPOSE_PROFILES(without clobberingmonitoring) - On
docker compose up -d, theccp-agentcontainer 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¶
- Services Overview — learn about the services CCP provisions for each instance
- Updates & Upgrades — upgrading CML instances
- Deployment — production setup with tunneling and SSL