---
title: Control Panel (CCP)
description: Multi-tenant management for provisioning and operating multiple Changemaker Lite instances.
icon: material/console
status: new
tags:
  - guide
  - operator
  - multi-tenant
---

# Changemaker Control Panel (CCP)

!!! tip "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](https://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.

!!! info "Single instance?"
    If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with [First Steps](first-steps.md).

---

## 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

```bash
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

```bash
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

```bash
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
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

```bash
# 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.

!!! info "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.

```bash
# 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:

```bash
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](services.md) — learn about the services CCP provisions for each instance
- [Updates & Upgrades](upgrades.md) — upgrading CML instances
- [Deployment](../deployment/index.md) — production setup with tunneling and SSL
