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

- Services Overview — learn about the services CCP provisions for each instance
- Updates & Upgrades — upgrading CML instances
- Deployment — production setup with tunneling and SSL