---
title: Environment Variables
description: Complete reference for every .env variable in Changemaker Lite.
icon: material/file-cog
tags:
  - reference
  - getting-started
  - operator
  - configuration
---

# Environment Variables

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

Changemaker Lite uses a single `.env` file at the project root to configure all services. Copy the example file to get started:

```bash
cp .env.example .env
```

!!! danger "Security Essentials"
    - **Change every** `REQUIRED_STRONG_PASSWORD_CHANGE_THIS` value before starting services
    - **Generate secrets** with `openssl rand -hex 32` (or `-hex 16` where noted)
    - **Never** commit `.env` to version control
    - Use unique values for each secret &mdash; do not reuse JWT secrets as encryption keys

---

## Quick Reference

Variables are grouped by service. Each table marks whether a variable is **required** for a basic deployment or **optional** (has a sensible default or only needed for specific features).

| Symbol | Meaning |
|--------|---------|
| :material-alert-circle:{ .text-red } | Must be set before first run |
| :material-tune-variant: | Has a working default; change for production |
| :material-flask: | Feature flag &mdash; opt-in |

---

## General

| Variable | Default | Description |
|----------|---------|-------------|
| `NODE_ENV` | `development` | Set to `production` for production deployments. Controls logging, error detail, and security checks. |
| `DOMAIN` | `cmlite.org` | Root domain. Used for nginx subdomain routing (`app.DOMAIN`, `api.DOMAIN`, etc.). The root domain serves the MkDocs documentation site; all application routes live under `app.DOMAIN`. |
| `USER_ID` | `1000` | UID for container file ownership. Match your host user's UID (`id -u`). |
| `GROUP_ID` | `1000` | GID for container file ownership. Match your host user's GID (`id -g`). |
| `DOCKER_GROUP_ID` | `984` | GID of the `docker` group on the host. Needed for containers that access the Docker socket. Find with `getent group docker`. |

---

## PostgreSQL (Main Database) :material-alert-circle:{ .text-red }

The primary database for both the Express API and the Fastify Media API (shared).

| Variable | Default | Description |
|----------|---------|-------------|
| `V2_POSTGRES_USER` | `changemaker` | Database username. |
| `V2_POSTGRES_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } **Must change.** Database password. |
| `V2_POSTGRES_DB` | `changemaker_v2` | Database name. |
| `V2_POSTGRES_PORT` | `5433` | Host port mapping. The container listens on `5432` internally. |

!!! tip "Connection string"
    The `DATABASE_URL` is constructed automatically inside Docker. If running locally, set:
    ```
    DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2
    ```

---

## JWT Authentication :material-alert-circle:{ .text-red }

| Variable | Default | Description |
|----------|---------|-------------|
| `JWT_ACCESS_SECRET` | &mdash; | :material-alert-circle:{ .text-red } Secret for signing access tokens. Generate with `openssl rand -hex 32`. |
| `JWT_REFRESH_SECRET` | &mdash; | :material-alert-circle:{ .text-red } Secret for signing refresh tokens. **Must differ** from the access secret. |
| `JWT_INVITE_SECRET` | &mdash; | :material-alert-circle:{ .text-red } Secret for signing volunteer invite tokens. **Must differ** from access and refresh secrets. Generate with `openssl rand -hex 32`. |
| `JWT_ACCESS_EXPIRY` | `15m` | Access token lifetime. Short-lived by design. |
| `JWT_REFRESH_EXPIRY` | `7d` | Refresh token lifetime. Tokens are rotated atomically on each refresh. |

---

## Encryption Key :material-alert-circle:{ .text-red }

| Variable | Default | Description |
|----------|---------|-------------|
| `ENCRYPTION_KEY` | &mdash; | :material-alert-circle:{ .text-red } AES key for encrypting secrets stored in the database (SMTP passwords, API keys, etc.). Generate with `openssl rand -hex 32`. **Must not** reuse a JWT secret. **Required in all environments** (no longer falls back to JWT secret in development). |

---

## Security Extras :material-tune-variant:

Additional secrets for key separation. These fall back to `JWT_ACCESS_SECRET` if empty, but setting unique values is **strongly recommended** for production.

| Variable | Default | Description |
|----------|---------|-------------|
| `GITEA_SSO_SECRET` | *(empty)* | Cookie signing secret for Gitea SSO integration. Falls back to `JWT_ACCESS_SECRET` if empty. Generate with `openssl rand -hex 32`. |
| `SERVICE_PASSWORD_SALT` | *(empty)* | Salt for deriving deterministic service passwords (Gitea, Rocket.Chat user provisioning). Falls back to `JWT_ACCESS_SECRET` if empty &mdash; rotating JWT_ACCESS_SECRET would then invalidate all provisioned service passwords. Generate with `openssl rand -hex 32`. |
| `CSP_ENABLED` | `false` | Enable Content Security Policy headers in API responses. |

!!! warning "Key separation"
    If `GITEA_SSO_SECRET` and `SERVICE_PASSWORD_SALT` are left empty, the API logs security warnings on every startup. Set unique values to isolate secret rotation and prevent one compromised key from affecting other subsystems.

---

## Initial Admin Account :material-alert-circle:{ .text-red }

These credentials create the first super-admin user during database seeding (`npx prisma db seed`).

| Variable | Default | Description |
|----------|---------|-------------|
| `INITIAL_ADMIN_EMAIL` | `admin@cmlite.org` | Email address for the initial admin. |
| `INITIAL_ADMIN_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } **Must change.** Must be 12+ characters with uppercase, lowercase, and a digit. Change this password after first login. |

---

## API Server

| Variable | Default | Description |
|----------|---------|-------------|
| `API_PORT` | `4000` | Host port for the Express API. |
| `API_URL` | `http://localhost:4000` | Public URL of the API. Used for generating links in emails and QR codes. |
| `CORS_ORIGINS` | `http://localhost:3000,http://localhost` | Comma-separated list of allowed CORS origins. **Add your production domain** (e.g., `https://app.yourdomain.org`) for production. |

!!! warning "Production CORS"
    If you deploy behind a tunnel (Pangolin, Cloudflare) and API requests fail with CORS errors, add your production `app.` subdomain here:
    ```
    CORS_ORIGINS=https://app.betteredmonton.org,http://localhost:3000,http://localhost
    ```
    Then restart the API: `docker compose restart api`

---

## Admin GUI

| Variable | Default | Description |
|----------|---------|-------------|
| `ADMIN_PORT` | `3000` | Host port for the React admin dashboard. |
| `ADMIN_URL` | `http://localhost:3000` | Public URL of the admin GUI. |

---

## Rate Limiting :material-tune-variant:

| Variable | Default | Description |
|----------|---------|-------------|
| `RATE_LIMIT_WINDOW_MS` | `900000` | Rate limit window in milliseconds (default: 15 minutes). |
| `RATE_LIMIT_MAX` | `500` | Maximum requests per window per IP. Auth endpoints have a stricter limit (10/min). |

---

## Nginx Reverse Proxy

| Variable | Default | Description |
|----------|---------|-------------|
| `NGINX_HTTP_PORT` | `80` | HTTP port. All subdomains route through nginx. |
| `NGINX_HTTPS_PORT` | `443` | HTTPS port. SSL is typically handled by the tunnel provider (Pangolin/Cloudflare). |

---

## Redis :material-alert-circle:{ .text-red }

Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.

| Variable | Default | Description |
|----------|---------|-------------|
| `REDIS_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } **Must change.** Redis requires authentication. |
| `REDIS_URL` | `redis://:${REDIS_PASSWORD}@redis-changemaker:6379` | Full connection URL. Uses the password variable automatically. |

---

## Payments (Stripe) :material-flask:

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_PAYMENTS` | `false` | :material-flask: Set to `true` to enable the payments feature (memberships, products, donations). Stripe API keys are stored encrypted in the database via the admin settings page. |

---

## Email / SMTP :material-tune-variant:

| Variable | Default | Description |
|----------|---------|-------------|
| `SMTP_HOST` | `mailhog-changemaker` | SMTP server. Default points to the MailHog dev container. |
| `SMTP_PORT` | `1025` | SMTP port. `1025` for MailHog, `587` for most production SMTP. |
| `SMTP_USER` | *(empty)* | SMTP username. Not needed for MailHog. |
| `SMTP_PASS` | *(empty)* | SMTP password. |
| `SMTP_FROM` | `noreply@cmlite.org` | "From" address on outgoing emails. |
| `SMTP_FROM_NAME` | `Changemaker Lite` | Display name for the "From" header. |
| `EMAIL_TEST_MODE` | `true` | When `true`, all emails go to MailHog instead of real SMTP. **Set to `false` in production.** |
| `TEST_EMAIL_RECIPIENT` | `admin@cmlite.org` | Catch-all recipient when test mode is on. |

!!! info "Development email"
    With `EMAIL_TEST_MODE=true`, all outgoing email is captured in MailHog at `http://localhost:8025`. No real emails are sent.

---

## Listmonk (Newsletters) :material-flask:

Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.

| Variable | Default | Description |
|----------|---------|-------------|
| `LISTMONK_PORT` | `9001` | Listmonk web UI port. |
| `LISTMONK_DB_PORT` | `5434` | Listmonk's own PostgreSQL port (separate from the main DB). Uses 5434 to avoid conflict with the main PostgreSQL (5432 internal / 5433 host). |
| `LISTMONK_DB_USER` | `listmonk` | Listmonk database user. |
| `LISTMONK_DB_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } Listmonk database password. |
| `LISTMONK_DB_NAME` | `listmonk` | Listmonk database name. |
| `LISTMONK_WEB_ADMIN_USER` | `admin` | Login for the Listmonk web dashboard. |
| `LISTMONK_WEB_ADMIN_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } Password for the Listmonk web dashboard. |
| `LISTMONK_API_USER` | `v2-api` | API user for programmatic access (auto-created by init container). |
| `LISTMONK_API_TOKEN` | &mdash; | Token for API user. Generate with `openssl rand -hex 16`. |
| `LISTMONK_ADMIN_USER` | `v2-api` | Same as `LISTMONK_API_USER` (used by the sync service). |
| `LISTMONK_ADMIN_PASSWORD` | &mdash; | Same as `LISTMONK_API_TOKEN`. |
| `LISTMONK_SYNC_ENABLED` | `false` | :material-flask: Set to `true` to sync participants/locations/users to Listmonk lists. |
| `LISTMONK_WEBHOOK_SECRET` | *(empty)* | Shared secret for Listmonk webhook callbacks. |
| `LISTMONK_PROXY_PORT` | `9002` | Nginx proxy port for Listmonk. |

??? example "Listmonk SMTP settings"
    Listmonk has its own SMTP configuration, separate from the main platform's:

    | Variable | Default | Description |
    |----------|---------|-------------|
    | `LISTMONK_SMTP_HOST` | `mailhog-changemaker` | SMTP host for Listmonk. |
    | `LISTMONK_SMTP_PORT` | `1025` | SMTP port. |
    | `LISTMONK_SMTP_USER` | *(empty)* | SMTP username. |
    | `LISTMONK_SMTP_PASSWORD` | *(empty)* | SMTP password. |
    | `LISTMONK_SMTP_TLS_TYPE` | `none` | TLS mode: `none`, `STARTTLS`, or `TLS`. |
    | `LISTMONK_SMTP_FROM` | `Changemaker Lite <noreply@cmlite.org>` | From address for newsletters. |

---

## Represent API (Canadian Electoral Data)

| Variable | Default | Description |
|----------|---------|-------------|
| `REPRESENT_API_URL` | `https://represent.opennorth.ca` | OpenNorth Represent API endpoint. Used for postal code &rarr; representative lookups. No API key required. |

---

## NocoDB (Data Browser) :material-tune-variant:

Read-only database browser. Useful for inspecting data without SQL.

| Variable | Default | Description |
|----------|---------|-------------|
| `NOCODB_V2_PORT` / `NOCODB_PORT` | `8091` | Host port for the NocoDB web UI. |
| `NOCODB_URL` | `http://changemaker-v2-nocodb:8080` | Internal Docker URL. |
| `NC_ADMIN_EMAIL` | `admin@cmlite.org` | NocoDB admin email. |
| `NC_ADMIN_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } NocoDB admin password. |

---

## Media Manager :material-flask:

Video library with upload, analytics, scheduling, and a public gallery.

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_MEDIA_FEATURES` | `false` | :material-flask: Set to `true` to enable the media system. |
| `MEDIA_API_PORT` | `4100` | Fastify media API port. |
| `MEDIA_API_PUBLIC_URL` | `http://media-api:4100` | Internal URL for the media API container. |
| `MEDIA_ROOT` | `/media/library` | Path to the video library inside the container. |
| `MEDIA_UPLOADS` | `/media/uploads` | Path for upload processing. |
| `MAX_UPLOAD_SIZE_GB` | `10` | Maximum single-file upload size in gigabytes. |
| `PUBLIC_MEDIA_PORT` | `3100` | Public media gallery server port. |
| `VIDEO_PLAYER_DEBUG` | `false` | Enable verbose video player logging. |

??? example "Analytics & scheduling settings"

    | Variable | Default | Description |
    |----------|---------|-------------|
    | `VIDEO_ANALYTICS_RETENTION_DAYS` | `90` | Days to retain analytics data. GDPR-compliant with IP hashing. |
    | `VIDEO_ANALYTICS_IP_HASHING_ENABLED` | `true` | Hash viewer IPs for privacy. |
    | `VIDEO_SCHEDULE_DEFAULT_TIMEZONE` | `UTC` | Default timezone for scheduled publishing. |
    | `VIDEO_SCHEDULE_NOTIFICATION_ENABLED` | `true` | Notify on scheduled publish/unpublish. |
    | `VIDEO_PREVIEW_LINK_EXPIRY_HOURS` | `24` | Preview link JWT expiry (hours). |

---

## Gitea (Git Hosting) :material-tune-variant:

Self-hosted Git repository. Optional service.

| Variable | Default | Description |
|----------|---------|-------------|
| `GITEA_URL` | `http://gitea-changemaker:3000` | Internal container URL for Gitea. |
| `GITEA_PORT` / `GITEA_WEB_PORT` | `3030` | Gitea web UI port. |
| `GITEA_SSH_PORT` | `2222` | Gitea SSH port for git operations. |
| `GITEA_DB_TYPE` | `mysql` | Database type (Gitea uses its own MySQL). |
| `GITEA_DB_HOST` | `gitea-db:3306` | Internal database host. |
| `GITEA_DB_NAME` | `gitea` | Database name. |
| `GITEA_DB_USER` | `gitea` | Database user. |
| `GITEA_DB_PASSWD` | &mdash; | :material-alert-circle:{ .text-red } Gitea database password. |
| `GITEA_DB_ROOT_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } MySQL root password for Gitea. |
| `GITEA_ROOT_URL` | `https://git.cmlite.org` | Public-facing URL for Gitea. |
| `GITEA_DOMAIN` | `git.cmlite.org` | Domain used in git clone URLs. |

??? example "Gitea Docs Comments"
    Enable comments on MkDocs documentation pages, backed by Gitea Issues.

    | Variable | Default | Description |
    |----------|---------|-------------|
    | `GITEA_COMMENTS_ENABLED` | `false` | :material-flask: Enable comments on MkDocs pages. |
    | `GITEA_API_TOKEN` | *(empty)* | Personal access token with repo write scope. Create in Gitea &rarr; Settings &rarr; Applications. |
    | `GITEA_COMMENTS_REPO_OWNER` | *(empty)* | Gitea username that owns the docs-comments repo. |
    | `GITEA_COMMENTS_REPO_NAME` | `docs-comments` | Repository name (auto-created via admin setup). |
    | `GITEA_OAUTH_CLIENT_ID` | *(empty)* | OAuth2 application client ID (create in Gitea &rarr; Settings &rarr; Applications &rarr; OAuth2). |
    | `GITEA_OAUTH_CLIENT_SECRET` | *(empty)* | OAuth2 application client secret. |

---

## n8n (Workflow Automation) :material-tune-variant:

| Variable | Default | Description |
|----------|---------|-------------|
| `N8N_PORT` | `5678` | n8n web UI port. |
| `N8N_HOST` | `n8n.cmlite.org` | Public hostname for n8n. |
| `N8N_ENCRYPTION_KEY` | &mdash; | :material-alert-circle:{ .text-red } Encryption key for n8n credentials storage. |
| `N8N_USER_EMAIL` | `admin@example.com` | Initial n8n admin email. |
| `N8N_USER_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } Initial n8n admin password. |
| `GENERIC_TIMEZONE` | `UTC` | Timezone for n8n cron triggers. |

---

## MkDocs (Documentation)

| Variable | Default | Description |
|----------|---------|-------------|
| `MKDOCS_PORT` | `4003` | MkDocs dev server port (live preview). |
| `MKDOCS_SITE_SERVER_PORT` | `4004` | MkDocs static site server port. |
| `BASE_DOMAIN` | `https://cmlite.org` | Base URL for generated documentation links. |
| `MKDOCS_PREVIEW_URL` | `http://mkdocs:8000` | Internal container URL. |
| `MKDOCS_DOCS_PATH` | `/mkdocs/docs` | Documentation source directory inside the container. |

---

## Code Server (Web IDE)

| Variable | Default | Description |
|----------|---------|-------------|
| `CODE_SERVER_PORT` | `8888` | Code Server web UI port. |
| `CODE_SERVER_URL` | `http://code-server-changemaker:8443` | Internal container URL. |

---

## Homepage (Service Dashboard)

| Variable | Default | Description |
|----------|---------|-------------|
| `HOMEPAGE_PORT` | `3010` | Homepage web UI port. |
| `HOMEPAGE_EMBED_PORT` | `8887` | Port for iframe embedding in admin. |
| `HOMEPAGE_VAR_BASE_URL` | `http://localhost` | Base URL used in Homepage service links. |

---

## Mini QR (QR Code Generator)

| Variable | Default | Description |
|----------|---------|-------------|
| `MINI_QR_PORT` | `8089` | Mini QR direct access port. |
| `MINI_QR_URL` | `http://mini-qr:8080` | Internal container URL. |
| `MINI_QR_EMBED_PORT` | `8885` | Port for iframe embedding (walk sheets, cut exports). |

---

## Excalidraw (Whiteboard)

| Variable | Default | Description |
|----------|---------|-------------|
| `EXCALIDRAW_PORT` | `8090` | Excalidraw web UI port. |
| `EXCALIDRAW_URL` | `http://excalidraw-changemaker:80` | Internal container URL. |
| `EXCALIDRAW_EMBED_PORT` | `8886` | Port for iframe embedding. |
| `EXCALIDRAW_WS_URL` | `wss://draw.cmlite.org` | WebSocket URL for real-time collaboration. |

---

## Vaultwarden (Password Manager) :material-tune-variant:

Self-hosted Bitwarden-compatible password manager. Optional service.

| Variable | Default | Description |
|----------|---------|-------------|
| `VAULTWARDEN_PORT` | `8445` | Vaultwarden web UI port. |
| `VAULTWARDEN_URL` | `http://vaultwarden-changemaker:80` | Internal container URL. |
| `VAULTWARDEN_EMBED_PORT` | `8890` | Port for iframe embedding in admin. |
| `VAULTWARDEN_ADMIN_TOKEN` | *(empty)* | Admin panel token (access at `/admin`). Generate with `openssl rand -hex 32`. |
| `VAULTWARDEN_DOMAIN` | `https://vault.cmlite.org` | Public-facing URL. **Must use HTTPS** &mdash; Bitwarden web vault enforces HTTPS for account creation. Set to your Pangolin tunnel URL. |
| `VAULTWARDEN_SIGNUPS_ALLOWED` | `false` | Allow new user self-registration. Keep `false` and use admin panel invites. |
| `VAULTWARDEN_WEBSOCKET_ENABLED` | `true` | Enable WebSocket notifications for real-time sync. |
| `VAULTWARDEN_SMTP_SECURITY` | `off` | SMTP security mode: `off` for MailHog, `starttls` or `force_tls` for production. Uses the main `SMTP_*` variables for host/credentials. |

!!! info "Initial setup"
    The `vaultwarden-init` container automatically invites the `INITIAL_ADMIN_EMAIL` user when starting. Check MailHog (or your SMTP) for the invitation email.

---

## Rocket.Chat (Team Chat) :material-flask:

Self-hosted team chat for volunteer coordination. Requires MongoDB (auto-configured).

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_CHAT` | `false` | :material-flask: Set to `true` to enable the Rocket.Chat integration. The initial default; once saved in admin Settings, the DB value is authoritative. |
| `ROCKETCHAT_ADMIN_USER` | `rcadmin` | Rocket.Chat admin username. |
| `ROCKETCHAT_ADMIN_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } Rocket.Chat admin password. |
| `ROCKETCHAT_URL` | `http://rocketchat-changemaker:3000` | Internal container URL. |
| `ROCKETCHAT_EMBED_PORT` | `8891` | Port for iframe embedding in admin. |
| `MONGO_ROOT_USER` | `rocketchat` | MongoDB admin username. |
| `MONGO_ROOT_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } MongoDB admin password. MongoDB runs with `--auth` enabled. |

---

## Gancio (Event Management) :material-flask:

Self-hosted event management platform. Uses the shared PostgreSQL database (auto-created by `init-gancio-db.sh`).

| Variable | Default | Description |
|----------|---------|-------------|
| `GANCIO_PORT` | `8092` | Gancio web UI port. |
| `GANCIO_URL` | `http://gancio-changemaker:13120` | Internal container URL. |
| `GANCIO_EMBED_PORT` | `8892` | Port for iframe embedding in admin. |
| `GANCIO_BASE_URL` | `https://events.cmlite.org` | Public-facing URL for Gancio. Used in event links. |
| `GANCIO_ADMIN_USER` | `admin` | Gancio admin username for shift-to-event sync (OAuth login). |
| `GANCIO_ADMIN_PASSWORD` | &mdash; | :material-alert-circle:{ .text-red } Gancio admin password. |
| `GANCIO_SYNC_ENABLED` | `false` | :material-flask: Set to `true` to enable automatic shift &rarr; Gancio event synchronization. |

---

## Jitsi Meet (Video Conferencing) :material-flask:

Self-hosted video conferencing with JWT authentication. Integrates with Rocket.Chat for in-channel video calls.

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_MEET` | `false` | :material-flask: Set to `true` to enable the Jitsi Meet integration. The initial default; once saved in admin Settings, the DB value is authoritative. |
| `JITSI_APP_ID` | `changemaker` | JWT application ID. Must match across Jitsi Prosody, Rocket.Chat app settings, and `JWT_ACCEPTED_ISSUERS`/`JWT_ACCEPTED_AUDIENCES`. |
| `JITSI_APP_SECRET` | &mdash; | :material-alert-circle:{ .text-red } JWT secret for signing Jitsi tokens. Generate with `openssl rand -hex 32`. Shared between Jitsi Prosody, Rocket.Chat, and the API. |
| `JITSI_JICOFO_AUTH_PASSWORD` | &mdash; | Internal XMPP password for Jicofo (conference focus). Generate with `openssl rand -hex 16`. |
| `JITSI_JVB_AUTH_PASSWORD` | &mdash; | Internal XMPP password for JVB (video bridge). Generate with `openssl rand -hex 16`. |
| `JITSI_EMBED_PORT` | `8893` | Port for iframe embedding in admin. |
| `JITSI_URL` | `http://jitsi-web-changemaker:80` | Internal container URL. |
| `JVB_ADVERTISE_IP` | *(empty)* | Server's public IP address. **Required in production** for NAT traversal so remote participants can connect. |
| `JVB_PORT` | `10000` | UDP port for media traffic. Must be open in your firewall. |

!!! warning "Production requirements"
    - `JVB_ADVERTISE_IP` must be set to your server's public IP for calls to work outside the local network.
    - Port `10000/udp` must be open in your firewall for media traffic.
    - Calls must go through the production domain (not localhost) for SSL/JWT to work.

---

## SMS Campaigns (Termux Android Bridge) :material-flask:

Send SMS messages via an Android phone running the Termux API server. The phone acts as an SMS gateway.

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_SMS` | `false` | :material-flask: Set to `true` to enable SMS campaigns. The initial default; once saved in admin Settings, the DB value is authoritative. |
| `TERMUX_API_URL` | `http://10.0.0.193:5001` | URL of the Termux API server running on the Android phone. |
| `TERMUX_API_KEY` | *(empty)* | API key for authenticating with the Termux server (HMAC auth via `X-API-Key` header). |
| `SMS_DELAY_BETWEEN_MS` | `3000` | Delay between sending individual SMS messages (ms). Prevents carrier throttling. |
| `SMS_MAX_RETRIES` | `3` | Maximum retry attempts for failed SMS sends. |
| `SMS_RESPONSE_SYNC_INTERVAL_MS` | `30000` | How often to poll the phone's inbox for responses (ms). |
| `SMS_DEVICE_MONITOR_INTERVAL_MS` | `30000` | How often to check device health &mdash; battery, connectivity (ms). |

!!! tip "GUI configuration"
    The Termux API URL and API key can also be configured from **Admin &rarr; Settings &rarr; SMS**. Database values override these env vars when set.

---

## MailHog (Development Email)

| Variable | Default | Description |
|----------|---------|-------------|
| `MAILHOG_SMTP_PORT` | `1025` | SMTP port for capturing emails. |
| `MAILHOG_WEB_PORT` | `8025` | Web UI to view captured emails. |

---

## NAR (National Address Register) :material-tune-variant:

Canadian address data import for geographic canvassing.

| Variable | Default | Description |
|----------|---------|-------------|
| `NAR_DATA_DIR` | `/data` | Path to extracted NAR data inside the container. Expects `YYYYMM/Addresses/` and `YYYYMM/Locations/` subdirectories. Mount via `./data:/data:ro` in Docker Compose. |

Download NAR data from [Statistics Canada](https://www150.statcan.gc.ca/n1/pub/46-26-0002/462600022022001-eng.htm).

---

## Geocoding :material-tune-variant:

Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.

| Variable | Default | Description |
|----------|---------|-------------|
| `MAPBOX_API_KEY` | *(empty)* | Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. [Sign up](https://www.mapbox.com/pricing). |
| `GEOCODING_RATE_LIMIT_MS` | `1100` | Delay between requests to free providers (ms). Respects rate limits. |
| `GEOCODING_CACHE_ENABLED` | `true` | Enable Redis-backed geocoding cache. |
| `GEOCODING_CACHE_TTL_HOURS` | `24` | Cache lifetime in hours. |
| `GOOGLE_MAPS_API_KEY` | *(empty)* | Google Maps API key. Most accurate but $0.005/request after free tier. |
| `GOOGLE_MAPS_ENABLED` | `false` | Enable Google Maps as a geocoding provider. |
| `GEOCODING_PARALLEL_ENABLED` | `true` | Enable parallel geocoding for bulk imports (~10x speedup). |
| `GEOCODING_BATCH_SIZE` | `10` | Number of concurrent geocoding requests during bulk operations. |
| `BULK_GEOCODE_ENABLED` | `true` | Enable bulk re-geocoding from the admin UI. |
| `BULK_GEOCODE_MAX_BATCH` | `5000` | Maximum locations per bulk geocoding run. |

---

## Overpass / Area Import :material-tune-variant:

OpenStreetMap data import for map enrichment.

| Variable | Default | Description |
|----------|---------|-------------|
| `OVERPASS_API_URL` | `https://overpass-api.de/api/interpreter` | Overpass API endpoint. Use a private instance for heavy usage. |
| `OVERPASS_MIN_DELAY_MS` | `30000` | Minimum delay between requests (ms). The public API requires 30 seconds. |
| `AREA_IMPORT_MAX_GRID_POINTS` | `500` | Maximum reverse-geocode grid points per area import. |

---

## Pangolin Tunnel :material-tune-variant:

Expose services to the internet without port forwarding, using a self-hosted [Pangolin](https://github.com/fosrl/pangolin) instance.

| Variable | Default | Description |
|----------|---------|-------------|
| `PANGOLIN_API_URL` | `https://api.bnkserve.org/v1` | Pangolin server API endpoint. |
| `PANGOLIN_API_KEY` | *(empty)* | API key for Pangolin management. |
| `PANGOLIN_ORG_ID` | *(empty)* | Organization ID in Pangolin. |
| `PANGOLIN_SITE_ID` | *(empty)* | Site ID (populated after setup via admin GUI). |
| `PANGOLIN_ENDPOINT` | `https://pangolin.bnkserve.org` | Pangolin tunnel endpoint. |
| `PANGOLIN_NEWT_ID` | *(empty)* | Newt client ID (populated after setup). |
| `PANGOLIN_NEWT_SECRET` | *(empty)* | Newt client secret (populated after setup). |

!!! tip "Setup flow"
    Configure the tunnel from **Admin &rarr; Settings &rarr; Pangolin**. The setup wizard walks you through creating a site, copying credentials, and connecting the Newt container. See [Deployment](../deployment/index.md) for the full guide.

---

## Monitoring :material-flask:

These services are behind the `monitoring` Docker Compose profile. Start them with:

```bash
docker compose --profile monitoring up -d
```

| Variable | Default | Description |
|----------|---------|-------------|
| `PROMETHEUS_PORT` | `9090` | Prometheus web UI / query port. |
| `GRAFANA_PORT` | `3005` | Grafana dashboard port. |
| `GRAFANA_ADMIN_PASSWORD` | `admin` | :material-tune-variant: Change in production. |
| `GRAFANA_ROOT_URL` | `http://localhost:3005` | Public URL for Grafana (used in links). |
| `CADVISOR_PORT` | `8086` | cAdvisor container metrics port. |
| `NODE_EXPORTER_PORT` | `9100` | Prometheus node exporter port. |
| `REDIS_EXPORTER_PORT` | `9121` | Redis metrics exporter port. |
| `ALERTMANAGER_PORT` | `9093` | Alertmanager web UI port. |
| `GOTIFY_PORT` | `8889` | Gotify push notification port. |
| `GOTIFY_ADMIN_USER` | `admin` | Gotify admin username. |
| `GOTIFY_ADMIN_PASSWORD` | `admin` | :material-tune-variant: Change in production. |
| `GRAFANA_EMBED_PORT` | `8894` | Port for iframe embedding Grafana in admin. |
| `ALERTMANAGER_EMBED_PORT` | `8895` | Port for iframe embedding Alertmanager in admin. |

---

## Bunker Ops (Fleet Management) :material-flask:

Remote metrics push for managing multiple Changemaker Lite instances from a central monitoring server.

| Variable | Default | Description |
|----------|---------|-------------|
| `INSTANCE_LABEL` | *(empty)* | Unique label for this instance (used as a Prometheus metric label). Falls back to `DOMAIN` if empty. |
| `BUNKER_OPS_ENABLED` | `false` | :material-flask: Enable remote metrics push to a central VictoriaMetrics server. |
| `BUNKER_OPS_REMOTE_WRITE_URL` | *(empty)* | VictoriaMetrics `remote_write` endpoint (e.g., `https://ops.example.com/api/v1/write`). |

---

## Social, People & Analytics :material-flask:

Feature flags for the social graph, CRM people module, and analytics dashboard.

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_SOCIAL` | `false` | :material-flask: Enable the social module (friendships, challenges, spotlights, referrals). The initial default; once saved in admin Settings, the DB value is authoritative. |
| `ENABLE_PEOPLE` | `false` | :material-flask: Enable the CRM people module. The initial default; once saved in admin Settings, the DB value is authoritative. |
| `ENABLE_ANALYTICS` | `false` | :material-flask: Enable the analytics dashboard with visitor tracking and geographic insights. The initial default; once saved in admin Settings, the DB value is authoritative. |

---

## GeoIP (MaxMind GeoLite2) :material-flask:

Geographic IP lookup for analytics visitor location tracking. Requires a free MaxMind account.

| Variable | Default | Description |
|----------|---------|-------------|
| `MAXMIND_ACCOUNT_ID` | *(empty)* | MaxMind account ID. [Sign up free](https://www.maxmind.com/en/geolite2/signup). |
| `MAXMIND_LICENSE_KEY` | *(empty)* | MaxMind license key. When set, the GeoLite2-City database auto-downloads at startup. |
| `GEOIP_DB_PATH` | `/data/geoip/GeoLite2-City.mmdb` | Path to the GeoLite2 database file inside the container. |

---

## Control Panel Agent (CCP) :material-flask:

Remote management agent for the Changemaker Control Panel &mdash; enables centralized multi-instance management.

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_CCP_AGENT` | `false` | :material-flask: Enable the CCP remote management agent. |
| `CCP_URL` | *(empty)* | URL of the Changemaker Control Panel server. |
| `CCP_INVITE_CODE` | *(empty)* | One-time invite code for agent registration with the control panel. |
| `CCP_AGENT_URL` | *(empty)* | How the CCP can reach this agent (must be externally accessible). |
| `CCP_AGENT_PORT` | `7443` | Agent listener port. |

---

## Container Registry :material-tune-variant:

Settings for pulling pre-built production images from the Gitea container registry.

| Variable | Default | Description |
|----------|---------|-------------|
| `GITEA_REGISTRY` | `gitea.bnkops.com/admin` | Registry hostname and namespace for pulling images. |
| `IMAGE_TAG` | *(empty)* | Image tag to pull. Set to a commit SHA or `latest` for pre-built images. Leave empty (defaults to `local`) to build from source. |
| `COMPOSE_PROFILES` | *(empty)* | Docker Compose profiles to activate. Set to `monitoring` to include Prometheus/Grafana/Alertmanager in every `docker compose up -d`. |
| `GITEA_REGISTRY_USER` | `admin` | Registry username for `docker login` and the registry status API endpoint. |
| `GITEA_REGISTRY_PASS` | *(empty)* | Registry password for the status API endpoint. For `docker push/pull`, use `docker login gitea.bnkops.com`. |
| `GITEA_REGISTRY_API_TOKEN` | *(empty)* | API token for the **remote** registry (gitea.bnkops.com). Used by `build-release.sh --upload` to publish release tarballs. Create at Gitea &rarr; User Settings &rarr; Applications. **Not** the same as `GITEA_API_TOKEN`. |

---

## Docker / Container Management :material-tune-variant:

Internal settings for the admin dashboard's service status panel and container management.

| Variable | Default | Description |
|----------|---------|-------------|
| `DOCKER_NETWORK_NAME` | `changemaker-lite` | Docker bridge network name. Used by the dashboard to auto-discover containers. |
| `DOCKER_PROXY_URL` | `http://docker-socket-proxy:2375` | Read-only Docker socket proxy URL for container inspection. |
| `NEWT_CONTAINER_NAME` | `newt-changemaker` | Newt tunnel container name (for restart/status checks). |
| `NEWT_COMPOSE_SERVICE` | `newt` | Docker Compose service name for the Newt container. |

---

## Embed Proxy Ports :material-tune-variant:

Dedicated nginx ports for iframe embedding services in the admin dashboard without requiring DNS/subdomains. Change these to avoid port conflicts when running multiple instances on one host.

| Variable | Default | Description |
|----------|---------|-------------|
| `NOCODB_EMBED_PORT` | `8881` | NocoDB iframe port. |
| `N8N_EMBED_PORT` | `8882` | n8n iframe port. |
| `GITEA_EMBED_PORT` | `8883` | Gitea iframe port. |
| `MAILHOG_EMBED_PORT` | `8884` | MailHog iframe port. |
| `MINI_QR_EMBED_PORT` | `8885` | Mini QR iframe port. |
| `EXCALIDRAW_EMBED_PORT` | `8886` | Excalidraw iframe port. |
| `HOMEPAGE_EMBED_PORT` | `8887` | Homepage iframe port. |
| `VAULTWARDEN_EMBED_PORT` | `8890` | Vaultwarden iframe port. |
| `ROCKETCHAT_EMBED_PORT` | `8891` | Rocket.Chat iframe port. |
| `GANCIO_EMBED_PORT` | `8892` | Gancio iframe port. |
| `JITSI_EMBED_PORT` | `8893` | Jitsi iframe port. |
| `GRAFANA_EMBED_PORT` | `8894` | Grafana iframe port. |
| `ALERTMANAGER_EMBED_PORT` | `8895` | Alertmanager iframe port. |

---

## Gitea Docs Version History :material-tune-variant:

Settings for the documentation version history feature (backed by Gitea repository commits).

| Variable | Default | Description |
|----------|---------|-------------|
| `GITEA_DOCS_REPO` | `admin/changemaker.lite` | Gitea repository path for docs version history. |
| `GITEA_DOCS_PREFIX` | `mkdocs/docs` | Path prefix within the repository where documentation files live. |
| `GITEA_DOCS_BRANCH` | `v2` | Git branch to query for version history. |
| `GITEA_ADMIN_PASSWORD` | *(empty)* | Gitea admin password. Used **once** during initial setup to create an API token, then can be cleared. |

---

## Prisma CLI (Host-Side) :material-tune-variant:

| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `postgresql://changemaker:YOUR_POSTGRES_PASSWORD@localhost:5433/changemaker_v2` | Full PostgreSQL connection string. **Only used when running Prisma CLI on the host** (`npx prisma migrate dev`). Docker containers resolve the database hostname internally via Docker Compose environment variables. |

---

## Generating Secrets

Use these commands to generate all required secrets at once:

```bash
# JWT secrets (three separate values)
echo "JWT_ACCESS_SECRET=$(openssl rand -hex 32)"
echo "JWT_REFRESH_SECRET=$(openssl rand -hex 32)"
echo "JWT_INVITE_SECRET=$(openssl rand -hex 32)"

# Encryption key (must differ from JWT secrets)
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)"

# Security extras (key separation)
echo "GITEA_SSO_SECRET=$(openssl rand -hex 32)"
echo "SERVICE_PASSWORD_SALT=$(openssl rand -hex 32)"

# Database and Redis passwords
echo "V2_POSTGRES_PASSWORD=$(openssl rand -hex 24)"
echo "REDIS_PASSWORD=$(openssl rand -hex 24)"

# Listmonk
echo "LISTMONK_DB_PASSWORD=$(openssl rand -hex 24)"
echo "LISTMONK_WEB_ADMIN_PASSWORD=$(openssl rand -hex 16)"
LISTMONK_TOKEN=$(openssl rand -hex 16)
echo "LISTMONK_API_TOKEN=$LISTMONK_TOKEN"
echo "LISTMONK_ADMIN_PASSWORD=$LISTMONK_TOKEN"

# Supporting services
echo "GITEA_DB_PASSWD=$(openssl rand -hex 24)"
echo "GITEA_DB_ROOT_PASSWORD=$(openssl rand -hex 24)"
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "N8N_USER_PASSWORD=$(openssl rand -hex 16)"
echo "NC_ADMIN_PASSWORD=$(openssl rand -hex 16)"
echo "INITIAL_ADMIN_PASSWORD=$(openssl rand -base64 18)"

# Vaultwarden
echo "VAULTWARDEN_ADMIN_TOKEN=$(openssl rand -hex 32)"

# Rocket.Chat + MongoDB
echo "ROCKETCHAT_ADMIN_PASSWORD=$(openssl rand -hex 16)"
echo "MONGO_ROOT_PASSWORD=$(openssl rand -hex 24)"

# Gancio
echo "GANCIO_ADMIN_PASSWORD=$(openssl rand -hex 16)"

# Jitsi Meet
echo "JITSI_APP_SECRET=$(openssl rand -hex 32)"
echo "JITSI_JICOFO_AUTH_PASSWORD=$(openssl rand -hex 16)"
echo "JITSI_JVB_AUTH_PASSWORD=$(openssl rand -hex 16)"
```

!!! tip
    Copy the output and paste the values into your `.env` file. The `INITIAL_ADMIN_PASSWORD` uses base64 encoding to ensure it contains uppercase, lowercase, and digits (meeting the password policy).

---

## Minimal vs Full Deployment

=== "Minimal (Core Only)"

    For a basic deployment with campaigns, map, and admin:

    ```bash title="Required variables"
    V2_POSTGRES_PASSWORD=...
    REDIS_PASSWORD=...
    JWT_ACCESS_SECRET=...
    JWT_REFRESH_SECRET=...
    JWT_INVITE_SECRET=...
    ENCRYPTION_KEY=...
    INITIAL_ADMIN_PASSWORD=...
    ```

    ```bash title="Start services"
    docker compose up -d v2-postgres redis api admin
    ```

=== "Full Stack"

    For the complete platform including media, newsletters, monitoring, and all services:

    ```bash title="Additional variables needed"
    # Everything above, plus:
    ENABLE_MEDIA_FEATURES=true
    ENABLE_PAYMENTS=true
    ENABLE_CHAT=true
    ENABLE_MEET=true
    ENABLE_SMS=true
    LISTMONK_SYNC_ENABLED=true
    GANCIO_SYNC_ENABLED=true
    LISTMONK_DB_PASSWORD=...
    LISTMONK_WEB_ADMIN_PASSWORD=...
    LISTMONK_API_TOKEN=...
    NC_ADMIN_PASSWORD=...
    GITEA_DB_PASSWD=...
    GITEA_DB_ROOT_PASSWORD=...
    N8N_ENCRYPTION_KEY=...
    N8N_USER_PASSWORD=...
    VAULTWARDEN_ADMIN_TOKEN=...
    ROCKETCHAT_ADMIN_PASSWORD=...
    MONGO_ROOT_PASSWORD=...
    GANCIO_ADMIN_PASSWORD=...
    JITSI_APP_SECRET=...
    JITSI_JICOFO_AUTH_PASSWORD=...
    JITSI_JVB_AUTH_PASSWORD=...
    JVB_ADVERTISE_IP=your.public.ip.here
    EMAIL_TEST_MODE=false
    SMTP_HOST=smtp.your-provider.com
    SMTP_PORT=587
    SMTP_USER=you@example.com
    SMTP_PASS=your-smtp-password
    ```

    ```bash title="Start services"
    docker compose up -d
    docker compose --profile monitoring up -d
    ```
