{"config":{"lang":["en"],"separator":"[\\s\\u200b\\-_,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"test/","title":"Test","text":"
Testing page.
\ud83d\uded2 DIGITAL Test Product 1A test product
$90.00
Buy Now \u2014 $90.00Secure payment via Stripe. Browse all products
View in Shop\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Make my donation anonymous DonateSecure payment via Stripe. Open full donate page
Donate Now\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Amount\ud83d\uded2
Browse Our ProductsReports, toolkits, event tickets, and more.
Shop Now Choose Your PlanGet access to exclusive content and features.
View Plans\u2764\ufe0f
Support Our CauseYour contribution helps us create lasting change in our community.
Donate Now 9:54 Testing This Sucker 0 views Watch \u2192"},{"location":"blog/","title":"Blog","text":""},{"location":"docs/","title":"Documentation","text":"Welcome to the Changemaker Lite documentation. Whether you're a campaign volunteer, an admin managing operations, or a sysadmin deploying the platform \u2014 start here.
"},{"location":"docs/#use-the-platform","title":"Use the Platform","text":"Getting Started
Install Changemaker Lite, create your first admin account, and explore the dashboard.
Getting Started
Feature Guides
Campaigns, email advocacy, response walls, map locations, landing pages, and media.
Feature Guides
Administration
User management, roles and permissions, site settings, email templates, and newsletters.
Administration
Volunteer Guide
Sign up for shifts, use the canvassing map, record visits, and track your activity.
Volunteer Guide
Deployment
Docker Compose setup, environment variables, SSL/TLS, backups, and production checklist.
Deployment
Architecture
Dual API design, database schema, authentication flow, and system diagram.
Architecture
Services
Nginx routing, Redis, PostgreSQL, Listmonk, MkDocs, Gitea, NocoDB, and more.
Services
Monitoring
Prometheus metrics, Grafana dashboards, Alertmanager rules, and health checks.
Monitoring Coming soon
API Reference
REST endpoints for auth, campaigns, locations, shifts, media, and more.
API Reference
Troubleshooting
Common errors, CORS issues, database problems, tunnel debugging, and FAQ.
Troubleshooting
Security
Password policy, rate limiting, token rotation, encryption, and audit report.
Security See Deployment
Contributing
Development setup, code style, git workflow, and pull request guidelines.
Contributing Coming soon
New here?
Start with the Getting Started guide to have the platform running in under 30 minutes.
Looking for the source?
Changemaker Lite is 100% open source. Browse the code on Gitea.
"},{"location":"docs/admin/","title":"Administration","text":"This section covers day-to-day administration of the Changemaker Lite platform.
Under Construction
Detailed admin documentation is being written. Check back soon.
"},{"location":"docs/admin/#topics","title":"Topics","text":"Changemaker Lite exposes two REST APIs sharing a single PostgreSQL database.
Server Framework Port Purpose Main API Express.js4000 Auth, campaigns, map, shifts, canvassing, pages, email, settings Media API Fastify 4100 Video library, analytics, playlists, reactions, comments Both APIs use JWT Bearer authentication and return JSON. All request/response bodies are application/json unless noted otherwise.
sequenceDiagram\n participant Client\n participant API\n participant DB\n\n Client->>API: POST /api/auth/login {email, password}\n API->>DB: Verify credentials\n DB-->>API: User record\n API-->>Client: {accessToken, refreshToken}\n Note over Client: Store tokens\n\n Client->>API: GET /api/campaigns (Authorization: Bearer <accessToken>)\n API-->>Client: 200 OK\n\n Note over Client: Access token expires (15 min)\n\n Client->>API: POST /api/auth/refresh {refreshToken}\n API->>DB: Rotate token (atomic transaction)\n DB-->>API: New token pair\n API-->>Client: {accessToken, refreshToken}"},{"location":"docs/api/#headers","title":"Headers","text":"All authenticated requests require:
Authorization: Bearer <accessToken>\n The Media API also accepts tokens via query parameter for SSE streams:
GET /api/public/:id/chat-stream?token=<accessToken>\n"},{"location":"docs/api/#roles","title":"Roles","text":"Role Access Level SUPER_ADMIN Full platform access INFLUENCE_ADMIN Campaign and advocacy management MAP_ADMIN Map, locations, shifts, canvassing USER Volunteer portal, public features TEMP Limited access (auto-created on public shift signup)"},{"location":"docs/api/#middleware-reference","title":"Middleware Reference","text":"Middleware Effect authenticate Requires valid JWT. Sets req.user with id, email, role. Returns 401 if missing or invalid. optionalAuth Same as authenticate but continues without user if token is absent. requireRole(...roles) Checks user role against allowed list. Returns 403 if not authorized. requireNonTemp Blocks TEMP users. Returns 403. validate(schema, source) Validates request body/query/params against a Zod schema. Returns 400 on failure."},{"location":"docs/api/#error-responses","title":"Error Responses","text":"All errors follow a consistent format:
{\n \"error\": {\n \"message\": \"Human-readable error description\",\n \"code\": \"ERROR_CODE\",\n \"statusCode\": 400\n }\n}\n Status Code Meaning 400 VALIDATION_ERROR Request body/query failed schema validation 401 UNAUTHORIZED Missing or invalid access token 403 FORBIDDEN Valid token but insufficient role 404 NOT_FOUND Resource does not exist 429 RATE_LIMITED Too many requests (see Rate Limits) 500 INTERNAL_ERROR Unexpected server error Enumeration Prevention
Auth endpoints (/login, /register, /forgot-password) return generic success messages to prevent user enumeration. A 401 from /api/auth/me does not reveal whether the user exists.
Rate limits are Redis-backed and keyed by IP address.
Endpoint Group Window Max Requests Redis Prefix Auth (login, register, refresh) 15 min 10rl:auth: Email sending 1 hour 30 rl:email: Response submission 1 hour 10 rl:response: Shift signup 1 hour 10 rl:shift-signup: Canvass visits 1 min 30 rl:canvass-visit: Canvass bulk visits 1 min 5 rl:canvass-visit-bulk: GPS tracking 1 min 6 rl:gps-tracking: Canvass geocode 1 min 10 rl:canvass-geocode: Observability 1 min 20 rl:observability: Health/metrics 1 min 30 rl:health-metrics: Global (all other) Configurable Configurable rl:global: When rate-limited, the API returns:
{\n \"error\": {\n \"message\": \"Too many requests, please try again later\",\n \"code\": \"RATE_LIMITED\",\n \"statusCode\": 429\n }\n}\n"},{"location":"docs/api/#main-api-express-port-4000","title":"Main API (Express \u2014 Port 4000)","text":""},{"location":"docs/api/#health-metrics","title":"Health & Metrics","text":"Method Path Auth Description GET /api/health Health check \u2014 PostgreSQL + Redis ping GET /api/metrics Prometheus metrics (text/plain) Health response {\n \"status\": \"healthy\",\n \"checks\": {\n \"database\": \"ok\",\n \"redis\": \"ok\"\n }\n}\n"},{"location":"docs/api/#auth","title":"Auth","text":"Prefix: /api/auth
/api/auth/login Email + password login POST /api/auth/register Create account (always USER role) POST /api/auth/verify-email Verify email with token POST /api/auth/resend-verification Resend verification email POST /api/auth/forgot-password Send password reset email POST /api/auth/reset-password Set new password with reset token POST /api/auth/refresh Rotate refresh token \u2192 new token pair POST /api/auth/logout Invalidate refresh token GET /api/auth/me Current user profile Login request & response Request:
{\n \"email\": \"admin@example.com\",\n \"password\": \"SecurePass123!\"\n}\n Response: {\n \"accessToken\": \"eyJhbG...\",\n \"refreshToken\": \"eyJhbG...\",\n \"user\": {\n \"id\": \"uuid\",\n \"email\": \"admin@example.com\",\n \"name\": \"Admin\",\n \"role\": \"SUPER_ADMIN\"\n }\n}\n Password Policy
Passwords must be at least 12 characters with at least one uppercase letter, one lowercase letter, and one digit.
"},{"location":"docs/api/#users","title":"Users","text":"Prefix: /api/users \u00b7 Auth: All routes require authentication
/api/users Admin Paginated user list with search, role, and status filters GET /api/users/:id Admin or self Single user profile POST /api/users Admin Create user PUT /api/users/:id Admin or self Update user (non-admins cannot change role/status) POST /api/users/:id/approve Admin Approve pending user; sends approval email POST /api/users/:id/reject Admin Reject pending user DELETE /api/users/:id Admin Delete user Query parameters for GET /api/users:
page number Page number (default 1) limit number Items per page (default 20) search string Search by name or email role string Filter by role status string Filter by status"},{"location":"docs/api/#dashboard","title":"Dashboard","text":"Prefix: /api/dashboard \u00b7 Auth: Admin roles required
/api/dashboard/summary Any admin Platform-wide counts (users, campaigns, locations, shifts) GET /api/dashboard/system SUPER_ADMIN Hardware + OS info (CPU, memory, disk) GET /api/dashboard/containers SUPER_ADMIN Docker container statuses GET /api/dashboard/weather Any admin Current weather at map center coordinates GET /api/dashboard/api-metrics SUPER_ADMIN Prometheus API performance metrics GET /api/dashboard/time-series SUPER_ADMIN Prometheus time-series data GET /api/dashboard/container-resources SUPER_ADMIN cAdvisor CPU/memory/network per container Query parameters for GET /api/dashboard/time-series:
metrics string Comma-separated metric keys (whitelist-validated) range string Time range (e.g., 1h, 24h, 7d) step string Sample interval (e.g., 5m, 1h)"},{"location":"docs/api/#campaigns","title":"Campaigns","text":""},{"location":"docs/api/#admin-crud","title":"Admin CRUD","text":"Prefix: /api/campaigns \u00b7 Auth: Admin roles
/api/campaigns Paginated campaign list GET /api/campaigns/:id Single campaign detail POST /api/campaigns Create campaign PUT /api/campaigns/:id Update campaign DELETE /api/campaigns/:id Delete campaign"},{"location":"docs/api/#public","title":"Public","text":"Method Path Auth Description GET /api/campaigns/public List all active campaigns GET /api/campaigns/:slug/details Campaign detail by slug (ACTIVE only)"},{"location":"docs/api/#user-submissions","title":"User Submissions","text":"Auth: Authenticated, non-TEMP users
Method Path Description POST/api/campaigns/user/submit Submit campaign for moderation (5/hour limit) GET /api/campaigns/user/my-campaigns List own submitted campaigns PUT /api/campaigns/user/:id Edit own pending campaign"},{"location":"docs/api/#moderation","title":"Moderation","text":"Auth: Admin roles
Method Path Description GET/api/campaigns/moderation/queue Campaigns pending moderation GET /api/campaigns/moderation/stats Moderation queue statistics PATCH /api/campaigns/moderation/:id Approve or reject campaign"},{"location":"docs/api/#campaign-emails","title":"Campaign Emails","text":"Method Path Auth Description POST /api/campaigns/:slug/send-email Send advocacy email to representatives (rate limited: 30/hour) POST /api/campaigns/:slug/track-mailto Track mailto link click GET /api/campaigns/:id/emails Admin Paginated emails for campaign GET /api/campaigns/:id/email-stats Admin Email statistics"},{"location":"docs/api/#responses","title":"Responses","text":"Prefix: /api/campaigns (public) and /api/responses (admin + actions)
/api/campaigns/:slug/responses List approved public responses GET /api/campaigns/:slug/response-stats Response statistics POST /api/campaigns/:slug/responses Submit response (rate limited: 10/hour) POST /api/responses/:id/upvote Optional Upvote a response DELETE /api/responses/:id/upvote Optional Remove upvote GET /api/responses/:id/verify/:token Verify response via email link"},{"location":"docs/api/#admin","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/responses All responses with filters PATCH /api/responses/:id/status Approve or reject response POST /api/responses/:id/resend-verification Resend verification email DELETE /api/responses/:id Delete response"},{"location":"docs/api/#representatives","title":"Representatives","text":"Prefix: /api/representatives
/api/representatives/by-postal/:postalCode Lookup representatives by postal code (cache-first) GET /api/representatives/test-connection Represent API health check GET /api/representatives/cache-stats Admin Cache statistics GET /api/representatives Admin Paginated cached representatives GET /api/representatives/:id Admin Single cached representative DELETE /api/representatives/by-postal/:postalCode Admin Clear cache for postal code DELETE /api/representatives/:id Admin Delete cached representative Query parameters for postal code lookup:
Param Type Descriptionrefresh boolean Force API call, bypass cache"},{"location":"docs/api/#email-queue","title":"Email Queue","text":"Prefix: /api/email-queue \u00b7 Auth: Admin roles
/api/email-queue/stats BullMQ queue statistics (waiting, active, completed, failed) POST /api/email-queue/pause Pause email processing POST /api/email-queue/resume Resume email processing POST /api/email-queue/clean Clean completed jobs"},{"location":"docs/api/#locations","title":"Locations","text":"Prefix: /api/map/locations
/api/map/locations/public All geocoded locations for map (no PII); optional ?bounds="},{"location":"docs/api/#admin_1","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/locations Paginated locations with filters GET /api/map/locations/stats Location statistics GET /api/map/locations/all All geocoded locations for admin map GET /api/map/locations/export-csv CSV export GET /api/map/locations/:id Single location GET /api/map/locations/:id/history Edit history POST /api/map/locations Create location PUT /api/map/locations/:id Update location DELETE /api/map/locations/:id Delete location POST /api/map/locations/bulk-delete Bulk delete POST /api/map/locations/geocode Geocode single address POST /api/map/locations/geocode-missing Batch geocode all ungeocoded POST /api/map/locations/reverse-geocode Reverse geocode lat/lng to address POST /api/map/locations/import-csv Import from CSV (10 MB limit) POST /api/map/locations/import-bulk Bulk NAR or standard CSV import (100 MB limit)"},{"location":"docs/api/#bulk-geocode","title":"Bulk Geocode","text":"Prefix: /api/map/locations/bulk-geocode \u00b7 Auth: Map admins
/api/map/locations/bulk-geocode Start BullMQ bulk geocoding job GET /api/map/locations/bulk-geocode/:jobId Poll job status GET /api/map/locations/bulk-geocode/stats Queue statistics"},{"location":"docs/api/#nar-import","title":"NAR Import","text":"Prefix: /api/map/nar-import \u00b7 Auth: Map admins
/api/map/nar-import/datasets Available NAR datasets by province POST /api/map/nar-import Start province import (fire-and-forget) GET /api/map/nar-import/status/:importId Poll import progress NAR Import body {\n \"provinceCode\": \"24\",\n \"filterType\": \"city\",\n \"filterCity\": \"Edmonton\",\n \"residentialOnly\": true,\n \"deduplicateRadius\": 10,\n \"batchSize\": 500\n}\n"},{"location":"docs/api/#area-import","title":"Area Import","text":"Prefix: /api/map/area-import \u00b7 Auth: Map admins
/api/map/area-import/preview Preview bounds + estimated record counts POST /api/map/area-import Start area import (fire-and-forget) GET /api/map/area-import/status/:importId Poll import progress"},{"location":"docs/api/#cuts-polygons","title":"Cuts (Polygons)","text":"Prefix: /api/map/cuts
/api/map/cuts/public All public cuts as GeoJSON GET /api/map/cuts Map admin Paginated cuts list GET /api/map/cuts/:id Map admin Single cut POST /api/map/cuts Map admin Create cut (polygon GeoJSON) PUT /api/map/cuts/:id Map admin Update cut DELETE /api/map/cuts/:id Map admin Delete cut GET /api/map/cuts/:id/locations Map admin All locations within cut polygon GET /api/map/cuts/:id/statistics Map admin Support level breakdown GET /api/map/cuts/export-geojson Map admin All cuts as GeoJSON FeatureCollection GET /api/map/cuts/:id/export-geojson Map admin Single cut as GeoJSON Feature POST /api/map/cuts/import-geojson Map admin Import cuts from GeoJSON file"},{"location":"docs/api/#shifts","title":"Shifts","text":"Prefix: /api/map/shifts
/api/map/shifts/public List upcoming public shifts POST /api/map/shifts/public/:id/signup Public signup (creates TEMP user if needed; rate limited: 10/hour)"},{"location":"docs/api/#volunteer","title":"Volunteer","text":"Auth: Any authenticated user
Method Path Description GET/api/map/shifts/volunteer/upcoming Upcoming shifts with signup status GET /api/map/shifts/volunteer/my-signups Own confirmed signups POST /api/map/shifts/volunteer/:id/signup Sign up for shift DELETE /api/map/shifts/volunteer/:id/signup Cancel signup"},{"location":"docs/api/#admin_2","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/shifts Paginated shifts with filters GET /api/map/shifts/stats Statistics GET /api/map/shifts/calendar Calendar data (?startDate=&endDate=) GET /api/map/shifts/:id Single shift with signups POST /api/map/shifts Create shift PUT /api/map/shifts/:id Update shift DELETE /api/map/shifts/:id Delete shift POST /api/map/shifts/:id/signups Admin-add volunteer DELETE /api/map/shifts/:id/signups/:signupId Remove volunteer POST /api/map/shifts/:id/email-details Email details to all volunteers"},{"location":"docs/api/#shift-series","title":"Shift Series","text":"Auth: Map admins
Method Path Description POST/api/map/shifts/series Create recurring shift series GET /api/map/shifts/series/:id Get series PUT /api/map/shifts/series/:id Update series DELETE /api/map/shifts/series/:id Delete series"},{"location":"docs/api/#canvassing","title":"Canvassing","text":"Prefix: /api/map/canvass
Auth: Any authenticated user
Method Path Description GET/api/map/canvass/my/assignments Shift assignments GET /api/map/canvass/my/stats Personal canvass statistics GET /api/map/canvass/my/visits Visit history GET /api/map/canvass/my/session Active canvass session POST /api/map/canvass/sessions Start canvass session POST /api/map/canvass/sessions/:id/end End session GET /api/map/canvass/cuts/:cutId/locations Locations in cut with visit annotations GET /api/map/canvass/cuts/:cutId/route Walking route algorithm for cut GET /api/map/canvass/locations All locations with visit annotations PUT /api/map/canvass/locations/:id Edit address (role-gated fields) POST /api/map/canvass/locations Create location POST /api/map/canvass/reverse-geocode Reverse geocode lat/lng POST /api/map/canvass/geocode-search Geocode address for map (rate limited: 10/min) POST /api/map/canvass/visits Record door knock (rate limited: 30/min) POST /api/map/canvass/visits/bulk Record visit for all unvisited units (rate limited: 5/min)"},{"location":"docs/api/#admin_3","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/canvass/stats Platform-wide canvass statistics GET /api/map/canvass/stats/cuts/:cutId Statistics for specific cut GET /api/map/canvass/activity Recent activity feed GET /api/map/canvass/volunteers All volunteers with canvass activity GET /api/map/canvass/volunteers/:userId Individual volunteer statistics GET /api/map/canvass/visits All visits with filters"},{"location":"docs/api/#gps-tracking","title":"GPS Tracking","text":"Prefix: /api/map/tracking
Auth: Any authenticated user
Method Path Description POST/api/map/tracking/sessions Start GPS tracking session POST /api/map/tracking/sessions/:id/end End tracking session POST /api/map/tracking/sessions/:id/points Submit GPS point batch (rate limited: 6/min) POST /api/map/tracking/sessions/:id/link-canvass Link to canvass session GET /api/map/tracking/my/session Active tracking session GET /api/map/tracking/my/sessions Own historical sessions GET /api/map/tracking/my/sessions/:id/route Full route for own session"},{"location":"docs/api/#admin_4","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/tracking/live Live volunteer positions + trails GET /api/map/tracking/sessions All historical tracking sessions GET /api/map/tracking/sessions/:id/route Full route for any session"},{"location":"docs/api/#map-settings","title":"Map Settings","text":"Prefix: /api/map/settings
/api/map/settings Public map settings (center, zoom, walk sheet config) PUT /api/map/settings Map admin Update map settings"},{"location":"docs/api/#geocoding","title":"Geocoding","text":"Prefix: /api/map/geocoding \u00b7 Auth: Map admins
/api/map/geocoding/search Geocode address search (?q=&limit=1-10)"},{"location":"docs/api/#landing-pages","title":"Landing Pages","text":"Prefix: /api/pages and /api/page-blocks
/api/pages/:slug/view Get published page by slug"},{"location":"docs/api/#admin_5","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/pages Paginated landing pages GET /api/pages/:id Single page POST /api/pages Create page PUT /api/pages/:id Update page DELETE /api/pages/:id Delete page POST /api/pages/sync Sync MkDocs overrides from filesystem POST /api/pages/validate Validate and repair MkDocs exports"},{"location":"docs/api/#block-library","title":"Block Library","text":"Auth: Admin roles
Method Path Description GET/api/page-blocks List blocks GET /api/page-blocks/:id Single block POST /api/page-blocks Create block PUT /api/page-blocks/:id Update block DELETE /api/page-blocks/:id Delete block"},{"location":"docs/api/#email-templates","title":"Email Templates","text":"Prefix: /api/email-templates \u00b7 Auth: Admin roles (seed/cache require SUPER_ADMIN)
/api/email-templates List templates GET /api/email-templates/:id Single template POST /api/email-templates Create template PUT /api/email-templates/:id Update template DELETE /api/email-templates/:id Delete template GET /api/email-templates/:id/versions Version history GET /api/email-templates/:id/versions/:versionNumber Specific version POST /api/email-templates/:id/rollback Rollback to prior version POST /api/email-templates/validate Validate Handlebars syntax POST /api/email-templates/:id/test Send test email (rate limited: 10/15min) GET /api/email-templates/:id/test-logs Test send logs POST /api/email-templates/seed Seed templates from filesystem POST /api/email-templates/clear-cache Clear template cache"},{"location":"docs/api/#qr-codes","title":"QR Codes","text":"Method Path Auth Description GET /api/qr Generate QR code PNG (?text=&size=50-500) Cached for 1 hour. Returns image/png.
Prefix: /api/settings
/api/settings Public site settings (SMTP credentials stripped) GET /api/settings/admin SUPER_ADMIN Full settings including SMTP credentials PUT /api/settings SUPER_ADMIN Update settings POST /api/settings/email/test-connection SUPER_ADMIN Test SMTP connection POST /api/settings/email/test-send SUPER_ADMIN Send test email"},{"location":"docs/api/#listmonk-newsletter-sync","title":"Listmonk (Newsletter Sync)","text":"Prefix: /api/listmonk \u00b7 Auth: SUPER_ADMIN
/api/listmonk Sync status + connection check GET /api/listmonk/stats Subscriber counts from Listmonk POST /api/listmonk/test-connection Health check POST /api/listmonk/sync/participants Sync campaign participants POST /api/listmonk/sync/locations Sync locations POST /api/listmonk/sync/users Sync users POST /api/listmonk/sync/all Run all sync operations POST /api/listmonk/reinitialize Reinitialize Listmonk lists GET /api/listmonk/proxy-url Proxy port + JWT for iframe"},{"location":"docs/api/#documentation-management","title":"Documentation Management","text":"Prefix: /api/docs \u00b7 Auth: Authenticated, non-TEMP (write operations require SUPER_ADMIN)
/api/docs/status MkDocs + Code Server availability GET /api/docs/config Port numbers for iframe URLs GET /api/docs/mkdocs-config Read raw mkdocs.yml PUT /api/docs/mkdocs-config Write mkdocs.yml POST /api/docs/build Trigger MkDocs build POST /api/docs/upload Upload asset (20 MB, whitelisted extensions) GET /api/docs/files File tree (?force=true bypasses cache) POST /api/docs/files/rename Rename or move file GET /api/docs/files/* Read file content PUT /api/docs/files/* Write file content POST /api/docs/files/* Create file or folder DELETE /api/docs/files/* Delete file or empty folder"},{"location":"docs/api/#services","title":"Services","text":"Prefix: /api/services \u00b7 Auth: SUPER_ADMIN
/api/services/status Health check all managed services (NocoDB, n8n, Gitea, MailHog, Mini QR, Excalidraw, Homepage) GET /api/services/config Port numbers + subdomain info"},{"location":"docs/api/#pangolin-tunnel-management","title":"Pangolin (Tunnel Management)","text":"Prefix: /api/pangolin \u00b7 Auth: SUPER_ADMIN
/api/pangolin/status Tunnel health + connection info GET /api/pangolin/config Current env configuration GET /api/pangolin/newt-status Newt container status POST /api/pangolin/newt-restart Restart Newt container GET /api/pangolin/sites List Pangolin sites GET /api/pangolin/exit-nodes Available exit nodes GET /api/pangolin/resource-definitions Resource definitions from YAML GET /api/pangolin/resources List resources POST /api/pangolin/setup Create site + all resources (rate limited: \u2157min) POST /api/pangolin/sync Sync resources (create missing, update changed) PUT /api/pangolin/resource/:id Update resource DELETE /api/pangolin/resource/:id Delete resource GET /api/pangolin/resource/:id/clients Connected clients GET /api/pangolin/certificate/:domainId/:domain Certificate info POST /api/pangolin/certificate/:certId Update certificate"},{"location":"docs/api/#observability","title":"Observability","text":"Prefix: /api/observability \u00b7 Auth: SUPER_ADMIN \u00b7 Rate limited: 20/min
/api/observability/status Check 7 monitoring services GET /api/observability/metrics-summary Key metrics from Prometheus GET /api/observability/alerts Active alerts from Alertmanager"},{"location":"docs/api/#payments","title":"Payments","text":"Prefix: /api/payments
/api/payments/config Stripe publishable key + donation settings GET /api/payments/plans Active subscription plans GET /api/payments/products Active products (?type=) POST /api/payments/subscribe Create subscription checkout POST /api/payments/purchase Optional Product checkout (guest or logged-in) POST /api/payments/donate Donation checkout GET /api/payments/my-subscription Current subscription POST /api/payments/my-subscription/cancel Cancel subscription POST /api/payments/webhook Stripe webhook (raw body)"},{"location":"docs/api/#admin_6","title":"Admin","text":"Auth: SUPER_ADMIN
/api/payments/admin/settings Payment settings (secrets masked) PUT /api/payments/admin/settings Update payment settings POST /api/payments/admin/settings/test-connection Test Stripe connection GET /api/payments/admin/dashboard Subscription + donation statistics GET /api/payments/admin/plans All subscription plans POST /api/payments/admin/plans Create plan PUT /api/payments/admin/plans/:id Update plan DELETE /api/payments/admin/plans/:id Delete plan POST /api/payments/admin/plans/:id/sync-stripe Sync plan to Stripe GET /api/payments/admin/subscriptions All subscriptions with filters POST /api/payments/admin/subscriptions/:id/cancel Cancel subscription GET /api/payments/admin/products All products POST /api/payments/admin/products Create product PUT /api/payments/admin/products/:id Update product DELETE /api/payments/admin/products/:id Delete product POST /api/payments/admin/products/:id/sync-stripe Sync product to Stripe GET /api/payments/admin/orders List orders POST /api/payments/admin/orders/:id/refund Refund order GET /api/payments/admin/donations List donations GET /api/payments/admin/export CSV export of completed orders"},{"location":"docs/api/#media-api-fastify-port-4100","title":"Media API (Fastify \u2014 Port 4100)","text":"The Media API is a separate Fastify server sharing the same PostgreSQL database. It handles all video-related functionality.
"},{"location":"docs/api/#health","title":"Health","text":"Method Path Auth Description GET/health Media API health check"},{"location":"docs/api/#videos-admin","title":"Videos (Admin)","text":"Prefix: /api/videos \u00b7 Auth: Admin roles
/api/videos List videos (?limit=&offset=&search=&orientation=&producers=&isShort=) GET /api/videos/producers Distinct producer list GET /api/videos/health Video count health check GET /api/videos/:id Single video detail PATCH /api/videos/:id Update metadata (title, producer, tags, quality, etc.) POST /api/videos/:id/publish Publish to category POST /api/videos/:id/unpublish Unpublish POST /api/videos/bulk-publish Bulk publish POST /api/videos/bulk-unpublish Bulk unpublish POST /api/videos/:id/lock Lock published video POST /api/videos/:id/unlock Unlock video POST /api/videos/:id/generate-thumbnail Generate thumbnail via FFmpeg POST /api/videos/bulk-generate-thumbnails Bulk thumbnail generation"},{"location":"docs/api/#upload","title":"Upload","text":"Method Path Description POST /api/videos/upload Single video upload (multipart, 10 GB limit, streams to disk) POST /api/videos/upload/batch Batch upload (returns 207 multi-status)"},{"location":"docs/api/#actions","title":"Actions","text":"Method Path Description POST /api/videos/:id/duplicate Duplicate video record POST /api/videos/:id/replace Replace video file, keep metadata GET /api/videos/:id/analytics Detailed analytics (?startDate=&endDate=) POST /api/videos/:id/reset-analytics Reset all analytics GET /api/videos/:id/preview-link Generate 24-hour JWT preview link GET /api/videos/analytics/top Top videos (?metric=views|watchTime&limit=) GET /api/videos/analytics/overview Global analytics overview"},{"location":"docs/api/#scheduling","title":"Scheduling","text":"Method Path Description POST /api/videos/:id/schedule-publish Schedule future publish ({publishAt, timezone?}) POST /api/videos/:id/schedule-unpublish Schedule future unpublish DELETE /api/videos/:id/schedule/:action Cancel scheduled operation GET /api/videos/schedules/upcoming Upcoming scheduled operations GET /api/videos/:id/schedule-history Schedule history for video GET /api/videos/schedules/stats Schedule queue statistics POST /api/videos/schedules/pause Pause schedule queue POST /api/videos/schedules/resume Resume schedule queue POST /api/videos/schedules/cleanup Clean old completed jobs"},{"location":"docs/api/#video-fetch","title":"Video Fetch","text":"Method Path Description POST /api/videos/fetch Submit fetch job ({urls: string[]}, 1\u201320 URLs) GET /api/videos/fetch/jobs List recent fetch jobs GET /api/videos/fetch/jobs/:jobId Job detail + log GET /api/videos/fetch/jobs/:jobId/log SSE log stream (Redis pub/sub) DELETE /api/videos/fetch/jobs/:jobId Cancel fetch job"},{"location":"docs/api/#streaming-public","title":"Streaming (Public)","text":"Prefix: /api/videos
/api/videos/stream/health Streaming health check GET /api/videos/:id/stream Optional HTTP range-supporting video stream GET /api/videos/:id/thumbnail Optional Serve thumbnail image GET /api/videos/:id/metadata Public video metadata for embedding Note
Admins can stream unpublished videos by providing a valid JWT.
"},{"location":"docs/api/#public-gallery","title":"Public Gallery","text":"Prefix: /api/public
/api/public Optional Published videos (?limit=&offset=&search=&sort=recent|popular|oldest&category=) GET /api/public/categories Optional Categories with video counts GET /api/public/producers Optional Published producers GET /api/public/:id Optional Single published video GET /api/public/:id/thumbnail Optional Published thumbnail GET /api/public/:id/stream Optional Published video stream"},{"location":"docs/api/#tracking","title":"Tracking","text":"Prefix: /api/track \u00b7 Auth: None required
/api/track/health Tracking health check POST /api/track/view Record video view (returns {viewId}) POST /api/track/event Record play/pause/seek/complete event POST /api/track/heartbeat Update watch time (10s interval, sendBeacon) POST /api/track/batch Batch up to 50 tracking events Tracking is GDPR-compliant IP addresses are hashed with a daily-rotating salt. Raw IPs are never stored. Tracking data is retained for 90 days.
"},{"location":"docs/api/#reactions","title":"Reactions","text":"Prefix: /api/reactions
/api/reactions/config Available reaction types + emoji mappings GET /api/reactions List reactions (?mediaId=&userId=&limit=) GET /api/reactions/:mediaId/chat Reactions in chat timeline format POST /api/reactions Add reaction (30s cooldown per type) Available types: like, love, laugh, wow, sad, angry
/api/public/:id/comments List comments (?limit=&offset=) POST /api/public/:id/comments Optional Create comment (word-filtered; rate limited: 5/min) GET /api/public/:id/chat-stream SSE stream for real-time chat (30s keepalive)"},{"location":"docs/api/#comment-admin","title":"Comment Admin","text":"Prefix: /api/media/admin/comments \u00b7 Auth: Admin roles
/api/media/admin/comments/stats Counts by status GET /api/media/admin/comments All comments with filters PATCH /api/media/admin/comments/:id/approve Approve comment PATCH /api/media/admin/comments/:id/hide Hide comment PATCH /api/media/admin/comments/:id/unhide Unhide comment PUT /api/media/admin/comments/:id/notes Update moderation notes DELETE /api/media/admin/comments/:id Delete comment"},{"location":"docs/api/#word-filters","title":"Word Filters","text":"Prefix: /api/media/admin/word-filters \u00b7 Auth: Admin roles
/api/media/admin/word-filters List filter entries grouped by level POST /api/media/admin/word-filters Add word ({word, level: low|medium|high|custom}) DELETE /api/media/admin/word-filters/:id Remove word"},{"location":"docs/api/#chat-threads-notifications","title":"Chat Threads & Notifications","text":"Auth: Authenticated
Method Path Description GET/api/media/chat/threads Videos with user's comments + unread counts POST /api/media/chat/threads/:mediaId/read Mark thread as read GET /api/media/notifications/stream Per-user SSE notification stream (?token=)"},{"location":"docs/api/#shorts","title":"Shorts","text":"Method Path Auth Description GET /api/shorts Optional Shorts feed (?sort=recent|popular|random) POST /api/shorts/scan Admin Auto-classify short videos by duration"},{"location":"docs/api/#upvotes","title":"Upvotes","text":"Method Path Auth Description POST /api/public/:id/upvote Toggle upvote (session-based via X-Session-ID header) GET /api/public/:id/upvote-status Check upvote status for current session"},{"location":"docs/api/#playlists","title":"Playlists","text":""},{"location":"docs/api/#public_6","title":"Public","text":"Prefix: /api/playlists
/api/playlists/featured Optional Featured playlists GET /api/playlists/popular Optional Popular public playlists (?search=) GET /api/playlists/share/:token Optional Playlist by share token GET /api/playlists/:id Optional Playlist detail (public, owner, or share token) POST /api/playlists/:id/view Optional Record playlist view"},{"location":"docs/api/#user-playlists","title":"User Playlists","text":"Auth: Authenticated
Method Path Description GET/api/playlists/my Own playlists POST /api/playlists Create playlist PUT /api/playlists/:id Update playlist (ownership check) DELETE /api/playlists/:id Delete playlist POST /api/playlists/:id/videos Add video ({mediaId}) DELETE /api/playlists/:id/videos/:mediaId Remove video PUT /api/playlists/:id/videos/reorder Reorder videos POST /api/playlists/:id/share Generate share token DELETE /api/playlists/:id/share Revoke share token"},{"location":"docs/api/#playlist-admin","title":"Playlist Admin","text":"Prefix: /api/media/playlists \u00b7 Auth: Admin roles
/api/media/playlists All playlists GET /api/media/playlists/featured Featured playlists with admin info POST /api/media/playlists/:id/feature Feature a playlist DELETE /api/media/playlists/:id/feature Unfeature a playlist PUT /api/media/playlists/featured/reorder Reorder featured playlists PUT /api/media/playlists/:id Admin update any playlist POST /api/media/playlists/:id/duplicate Duplicate playlist DELETE /api/media/playlists/:id Admin delete any playlist"},{"location":"docs/api/#user-profile","title":"User Profile","text":"Prefix: /api/media/me \u00b7 Auth: Authenticated
/api/media/me/stats User stats + 30-day activity + achievements GET /api/media/me/watch-history Paginated watch history POST /api/media/me/stats/recalculate Recompute stats from raw data GET /api/media/me/settings Privacy settings PUT /api/media/me/settings Update privacy settings PUT /api/media/me/profile Update display name PUT /api/media/me/password Change password"},{"location":"docs/api/#route-summary","title":"Route Summary","text":"API Module Endpoint Count Express Auth 9 Users 7 Dashboard 7 Campaigns (CRUD + public + user + moderation + emails) 16 Responses 10 Email Queue 4 Representatives 7 Locations (CRUD + geocode + import) 21 Cuts 11 Shifts (CRUD + series) 19 Canvassing 20 GPS Tracking 10 Map Settings + Geocoding 3 Pages + Blocks 12 Email Templates 13 QR Codes 1 Site Settings 5 Listmonk 9 Docs Management 11 Services 2 Pangolin 16 Observability 3 Payments (public + admin) 29 Health + Metrics 3 Express Total ~248 Fastify Videos (CRUD + upload + actions + schedule + fetch) 39 Streaming 4 Public Gallery 6 Tracking 5 Reactions 4 Comments + Chat 13 Shorts + Upvotes 4 Playlists (public + user + admin) 18 User Profile 7 Health 1 Fastify Total ~101 Grand Total ~349"},{"location":"docs/architecture/","title":"Architecture","text":"Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database.
Under Construction
Detailed architecture documentation is being written. Check back soon.
"},{"location":"docs/architecture/#system-overview","title":"System Overview","text":"Browser \u2500\u2500\u25ba Nginx (reverse proxy) \u2500\u2500\u252c\u2500\u2500\u25ba Express API (port 4000) \u2500\u2500\u25ba PostgreSQL\n \u251c\u2500\u2500\u25ba Fastify Media API (port 4100) \u2500\u2500\u2518\n \u251c\u2500\u2500\u25ba React Admin GUI (port 3000)\n \u2514\u2500\u2500\u25ba MkDocs / Other Services\n"},{"location":"docs/architecture/#key-components","title":"Key Components","text":"Component Technology Role Main API Express.js + Prisma Auth, campaigns, map, shifts, pages Media API Fastify + Prisma Video library, analytics, uploads Admin GUI React + Ant Design Single-page admin application Database PostgreSQL 16 Shared by both APIs (30+ tables) Cache Redis Rate limiting, job queues, geocoding Proxy Nginx Subdomain routing, security headers"},{"location":"docs/architecture/#authentication-flow","title":"Authentication Flow","text":"This guide covers how to take Changemaker Lite from a local development setup to a publicly accessible production deployment. The main decision is how to expose your services to the internet.
"},{"location":"docs/deployment/#architecture-overview","title":"Architecture Overview","text":"Regardless of which exposure method you choose, the internal architecture is the same:
Internet \u2192 [Your exposure method] \u2192 Nginx (port 80) \u2192 Backend Services\n Nginx handles all subdomain routing internally. Every service is accessed through nginx on port 80, which proxies to the correct container based on the Host header.
app.DOMAIN Admin GUI + public pages 3000 api.DOMAIN Express API 4000 media.DOMAIN Fastify Media API 4100 DOMAIN (root) MkDocs documentation site 4001 db.DOMAIN NocoDB 8091 docs.DOMAIN MkDocs live preview 4003 code.DOMAIN Code Server 8888 git.DOMAIN Gitea 3030 n8n.DOMAIN Workflow automation 5678 home.DOMAIN Homepage dashboard 3010 listmonk.DOMAIN Newsletter manager 9001 mail.DOMAIN MailHog (dev email) 8025 qr.DOMAIN Mini QR generator 8089 draw.DOMAIN Excalidraw whiteboard 8090 grafana.DOMAIN Monitoring dashboards 3001"},{"location":"docs/deployment/#exposure-methods","title":"Exposure Methods","text":""},{"location":"docs/deployment/#pangolin","title":"Option 1: Pangolin + Newt Tunnel (Recommended)","text":"Admin GUI: Tunnel Management Page
The admin dashboard includes a dedicated Tunnel Management page at Admin \u2192 Settings \u2192 Tunnel. This page provides:
If you're unsure about any step above, the Tunnel page walks you through the same process interactively.
Pangolin is a self-hosted tunnel server. The Newt client container runs alongside your stack and establishes an outbound connection to your Pangolin server, which then routes public traffic back through the tunnel. No port forwarding or static IP required.
Advantages:
Requirements:
If you used config.sh, you may have already set these. Otherwise, add to your .env:
PANGOLIN_API_URL=https://api.your-pangolin-server.org/v1\nPANGOLIN_API_KEY=your_api_key_here\nPANGOLIN_ORG_ID=your_org_id\n"},{"location":"docs/deployment/#step-2-create-a-site-in-pangolin","title":"Step 2: Create a Site in Pangolin","text":"Log in to your Pangolin dashboard and create a new site:
changemaker-yourdomain.org)100.90.128.3/24)Save the credentials
The Newt Secret is only shown once during site creation. Copy it immediately.
"},{"location":"docs/deployment/#step-3-update-env-with-site-credentials","title":"Step 3: Update.env with Site Credentials","text":"PANGOLIN_SITE_ID=your_site_id\nPANGOLIN_ENDPOINT=https://your-pangolin-server.org\nPANGOLIN_NEWT_ID=your_newt_id\nPANGOLIN_NEWT_SECRET=your_newt_secret\n"},{"location":"docs/deployment/#step-4-start-the-newt-container","title":"Step 4: Start the Newt Container","text":"docker compose up -d newt\n The Newt container connects to nginx (its only dependency) and establishes the tunnel:
# From docker-compose.yml\nnewt:\n image: fosrl/newt\n container_name: newt-changemaker\n restart: unless-stopped\n environment:\n - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT}\n - NEWT_ID=${PANGOLIN_NEWT_ID}\n - NEWT_SECRET=${PANGOLIN_NEWT_SECRET}\n depends_on:\n - nginx\n Verify the connection:
docker compose logs newt --tail 20\n You should see a successful connection message.
"},{"location":"docs/deployment/#step-5-create-public-http-resources","title":"Step 5: Create Public HTTP Resources","text":"In the Pangolin dashboard, create an HTTP resource for each service you want exposed. All resources point to nginx:80 \u2014 nginx handles the routing internally.
Required resources (minimum for a working deployment):
Resource Name Domain Target Auth Admin GUIapp.yourdomain.org nginx:80 Not Protected API Server api.yourdomain.org nginx:80 Not Protected Public Site yourdomain.org nginx:80 Not Protected Optional resources (add as needed):
Resource Name Domain Target Media APImedia.yourdomain.org nginx:80 NocoDB db.yourdomain.org nginx:80 Documentation docs.yourdomain.org nginx:80 Code Server code.yourdomain.org nginx:80 Gitea git.yourdomain.org nginx:80 Grafana grafana.yourdomain.org nginx:80 Set resources to Not Protected
By default, Pangolin may enable authentication on new resources. This causes 302 redirects to the Pangolin login page instead of reaching your services. Set each resource to Not Protected (public access) unless you intentionally want Pangolin SSO in front of it.
"},{"location":"docs/deployment/#step-6-update-cors-for-production","title":"Step 6: Update CORS for Production","text":"Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000,http://localhost\n Then restart the API:
docker compose restart api\n"},{"location":"docs/deployment/#step-7-verify","title":"Step 7: Verify","text":"# Should return JSON (not a 302 redirect)\ncurl https://api.yourdomain.org/api/health\n\n# Admin GUI should load\ncurl -I https://app.yourdomain.org\n"},{"location":"docs/deployment/#cloudflare","title":"Option 2: Cloudflare Tunnel","text":"Cloudflare Tunnel (cloudflared) provides a similar zero-trust tunnel approach using Cloudflare's network. No port forwarding needed, and you get Cloudflare's CDN and DDoS protection.
Advantages:
Disadvantages:
Create a Cloudflare Tunnel in the Zero Trust dashboard
Add a cloudflared service to your docker-compose.yml:
cloudflared:\n image: cloudflare/cloudflared:latest\n container_name: cloudflared-changemaker\n restart: unless-stopped\n command: tunnel run\n environment:\n - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}\n depends_on:\n - nginx\n networks:\n - changemaker-lite\n Add your tunnel token to .env:
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here\n Configure public hostnames in the Cloudflare dashboard, all pointing to http://nginx:80:
app.yourdomain.org http://nginx:80 api.yourdomain.org http://nginx:80 yourdomain.org http://nginx:80 (add more as needed) http://nginx:80 Start the tunnel:
docker compose up -d cloudflared\n Note
The cloudflared service is not included in the default docker-compose.yml. Add it manually if you choose this method. The Newt service can be removed or left stopped.
If your server has a public IP address (e.g., a VPS or dedicated server), you can point DNS directly to it and use nginx with SSL certificates.
Advantages:
Disadvantages:
Point DNS for your domain and all subdomains to your server's IP:
A yourdomain.org \u2192 YOUR_SERVER_IP\nA *.yourdomain.org \u2192 YOUR_SERVER_IP\n Or use individual A records for each subdomain if your DNS provider doesn't support wildcards.
Open ports 80 and 443 on your server's firewall.
Install Certbot (or another ACME client) for SSL certificates:
# Ubuntu/Debian\nsudo apt install certbot\n\n# Get a wildcard certificate with DNS challenge\nsudo certbot certonly --manual --preferred-challenges dns \\\n -d yourdomain.org -d '*.yourdomain.org'\n Alternatively, use the Certbot Docker image or a Let's Encrypt companion container.
Update nginx to listen on 443 with your certificates. Add an SSL server block to nginx/conf.d/ssl.conf:
server {\n listen 443 ssl;\n server_name app.yourdomain.org;\n\n ssl_certificate /etc/nginx/ssl/fullchain.pem;\n ssl_certificate_key /etc/nginx/ssl/privkey.pem;\n\n location / {\n proxy_pass http://changemaker-v2-admin:3000;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n }\n}\n\n# Repeat for api.yourdomain.org, media.yourdomain.org, etc.\n# Or use a single server block with $host matching\n Mount certificates into the nginx container via docker-compose.yml:
nginx:\n volumes:\n - /etc/letsencrypt/live/yourdomain.org:/etc/nginx/ssl:ro\n Set up auto-renewal with a cron job or systemd timer:
0 3 * * * certbot renew --quiet && docker compose restart nginx\n Traefik alternative
If you prefer automatic SSL and don't want to manage nginx SSL config manually, consider replacing nginx with Traefik. Traefik can auto-discover Docker containers and provision Let's Encrypt certificates automatically. This would require adapting the container labels and removing the nginx service.
"},{"location":"docs/deployment/#tailscale","title":"Option 4: Tailscale / WireGuard (Private Access)","text":"For deployments that should only be accessible to specific people (not the general public), a mesh VPN like Tailscale or plain WireGuard gives you private networking without exposing anything to the internet.
Use cases:
http://100.x.x.x:3000)Note
With private access methods, you may not need subdomain routing at all. Access services directly by port: http://server-ip:3000 (admin), http://server-ip:4000 (API), etc.
Before going live, verify each item:
"},{"location":"docs/deployment/#security","title":"Security","text":"grep -c \"REQUIRED_STRONG\" .env should return 0)NODE_ENV=production set in .envENCRYPTION_KEY set and differs from JWT secretsEMAIL_TEST_MODE=false (unless you want MailHog in production)CORS_ORIGINS includes your production domainREDIS_PASSWORD)curl https://api.yourdomain.org/api/health returns JSONdocker compose ps shows api, admin, v2-postgres, redis, nginx healthydocker compose exec api npx prisma migrate deploydocker compose exec api npx prisma db seedhttps://app.yourdomain.org./scripts/backup.shdocker compose --profile monitoring up -dThe included backup script dumps PostgreSQL databases, archives uploads, and optionally uploads to S3.
"},{"location":"docs/deployment/#running-a-backup","title":"Running a Backup","text":"./scripts/backup.sh\n This creates a timestamped directory under ./backups/ containing:
changemaker_v2.sql.gz \u2014 Main PostgreSQL dump (compressed)listmonk.sql.gz \u2014 Listmonk database dump (if running)uploads.tar.gz \u2014 Media uploads archivemanifest.json \u2014 Backup metadata# Upload to S3 (requires AWS CLI + S3_BUCKET env var)\n./scripts/backup.sh --s3\n\n# Custom retention (delete local backups older than N days)\n./scripts/backup.sh --retention 14\n"},{"location":"docs/deployment/#automated-backups","title":"Automated Backups","text":"Add a cron job for daily backups:
# Edit crontab\ncrontab -e\n\n# Add daily backup at 3 AM\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh >> /var/log/changemaker-backup.log 2>&1\n\n# With S3 upload\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh --s3 >> /var/log/changemaker-backup.log 2>&1\n"},{"location":"docs/deployment/#restore","title":"Restore","text":"# Restore main database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/changemaker_v2.sql.gz | \\\n docker compose exec -T v2-postgres psql -U changemaker changemaker_v2\n\n# Restore Listmonk database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/listmonk.sql.gz | \\\n docker compose exec -T listmonk-db psql -U listmonk listmonk\n\n# Restore uploads\ntar xzf backups/changemaker-v2-backup-TIMESTAMP/uploads.tar.gz -C ./\n"},{"location":"docs/deployment/#monitoring","title":"Monitoring","text":"The monitoring stack runs behind a Docker Compose profile and is not started by default.
"},{"location":"docs/deployment/#starting-the-monitoring-stack","title":"Starting the Monitoring Stack","text":"docker compose --profile monitoring up -d\n This starts:
Service Port Purpose Prometheus 9090 Metrics collection and queries Grafana 3001 Dashboards and visualization Alertmanager 9093 Alert routing and notifications cAdvisor 8080 Container resource metrics Node Exporter 9100 Host system metrics Redis Exporter 9121 Redis metrics Gotify 8889 Push notifications"},{"location":"docs/deployment/#pre-configured-dashboards","title":"Pre-configured Dashboards","text":"Grafana includes 3 auto-provisioned dashboards:
The API exposes 12 custom Prometheus metrics with the cm_ prefix:
cm_api_uptime_seconds \u2014 API uptimecm_email_queue_size \u2014 BullMQ pending emailscm_active_canvass_sessions \u2014 Active canvassing sessionscm_locations_total \u2014 Total locations in databaseapi/src/utils/metrics.tsPre-configured alerts in configs/prometheus/alerts.yml:
# Pull latest code\ngit pull origin v2\n\n# Rebuild and restart containers\ndocker compose build api admin\ndocker compose up -d api admin\n\n# Run any new migrations\ndocker compose exec api npx prisma migrate deploy\n"},{"location":"docs/deployment/#database-migrations","title":"Database Migrations","text":"Always run migrations after pulling updates:
docker compose exec api npx prisma migrate deploy\n Back up first
Always run ./scripts/backup.sh before applying migrations in production. Migrations may alter table structures and are not easily reversible.
Symptom: API returns 302 redirects to the Pangolin authentication page.
Fix: In the Pangolin dashboard, edit each resource and set Authentication to Not Protected.
"},{"location":"docs/deployment/#cors-errors","title":"CORS Errors","text":"Symptom: Browser console shows CORS errors when accessing the production domain.
Fix: Add your production app. subdomain to CORS_ORIGINS in .env, then docker compose restart api.
Check in order:
PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET in .envPANGOLIN_ENDPOINT matches your Pangolin server URLdocker compose logs newt --tail 50docker compose ps nginxdocker compose ps nginxcurl http://localhost:4000/api/healthdocker compose logs nginx --tail 50dig app.yourdomain.org should point to your Pangolin serverSee Troubleshooting for more common issues.
"},{"location":"docs/features/","title":"Feature Guides","text":"Changemaker Lite bundles advocacy campaigns, geographic mapping, volunteer management, media hosting, and landing pages into a single self-hosted platform. Every feature can be toggled on or off from Settings in the admin panel.
"},{"location":"docs/features/#advocacy-campaigns","title":"Advocacy Campaigns","text":"Help supporters contact their elected representatives through email campaigns.
"},{"location":"docs/features/#how-it-works","title":"How It Works","text":"/app/campaigns \u2014 create, edit, and manage campaigns/app/responses \u2014 moderate submitted responses/app/email-queue \u2014 monitor outgoing email delivery/campaigns \u2014 browse active campaigns/campaign/:slug \u2014 take action on a specific campaign/campaign/:slug/responses \u2014 view the response wallManage locations, organize canvassing territories, and coordinate volunteer door-to-door outreach.
"},{"location":"docs/features/#locations","title":"Locations","text":"Import addresses via CSV or the Canadian NAR (National Address Register) dataset. Each location can be geocoded using multiple providers (Nominatim, ArcGIS, Photon, Mapbox, and more). Locations appear as color-coded markers on admin and public maps.
"},{"location":"docs/features/#areas-cuts","title":"Areas (Cuts)","text":"Draw polygon regions on the map to define canvassing territories. Areas help organize locations into manageable chunks for volunteers. Each area shows stats like total addresses, visit counts, and coverage percentage.
"},{"location":"docs/features/#shifts","title":"Shifts","text":"Schedule volunteer time slots and let people sign up through a public page. Shifts can be linked to specific areas so volunteers know where they'll be canvassing.
"},{"location":"docs/features/#canvassing","title":"Canvassing","text":"The volunteer canvass map is a full-screen GPS-tracked experience:
/app/map \u2014 manage locations (CRUD, CSV import/export, geocoding)/app/map/cuts \u2014 draw and manage canvassing areas/app/map/shifts \u2014 schedule shifts and view signups/app/map/canvass \u2014 canvass dashboard (stats, activity feed, leaderboard)/map \u2014 public map view (address locations only, no canvass data)/shifts \u2014 sign up for volunteer shifts/volunteer \u2014 full-screen canvass map with GPS and visit recording/volunteer/shifts \u2014 view assigned shifts/volunteer/activity \u2014 visit history and outcome breakdown/volunteer/routes \u2014 past canvassing routesUpload, organize, and share campaign videos with built-in analytics.
"},{"location":"docs/features/#key-features_1","title":"Key Features","text":"/gallery with category browsing/app/media/library \u2014 upload and manage videos/app/media/curated \u2014 manage playlists/app/media/jobs \u2014 monitor processing jobs/gallery \u2014 browse the public video gallery/gallery/watch/:id \u2014 watch a specific videoBuild campaign microsites with a drag-and-drop visual editor.
"},{"location":"docs/features/#how-it-works_1","title":"How It Works","text":"/p/:slug/app/pages \u2014 list and manage landing pages/app/pages/:id/edit \u2014 full-screen GrapesJS editor/p/:slug \u2014 view a published landing pageIntegrated with Listmonk for opt-in mailing lists and newsletter campaigns.
"},{"location":"docs/features/#sync","title":"Sync","text":"When enabled (LISTMONK_SYNC_ENABLED=true), the platform syncs shift participants, location contacts, and user accounts to Listmonk subscriber lists. Sync is triggered manually from the admin panel.
/app/listmonk (sidebar: \"Newsletter\") \u2014 sync status, subscriber counts, and manual sync triggerCreate reusable email templates with variable substitution for campaign communications.
"},{"location":"docs/features/#admin-routes_5","title":"Admin Routes","text":"/app/email-templates \u2014 create and manage email templates with a visual editorThis guide walks you through installing Changemaker Lite, running your first deployment, and logging into the admin dashboard.
"},{"location":"docs/getting-started/#prerequisites","title":"Prerequisites","text":"git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n"},{"location":"docs/getting-started/#2-run-the-configuration-wizard","title":"2. Run the Configuration Wizard","text":"The fastest way to get a working .env file is the interactive configuration wizard:
bash config.sh\n The wizard walks you through each step:
Step What it does Prerequisites check Verifies Docker, Docker Compose, and OpenSSL are installed Domain Sets your root domain and updates all subdomain references (nginx, Gitea, n8n, MkDocs, etc.) Admin credentials Prompts for the initial super-admin email and password (enforces 12+ chars, uppercase, lowercase, digit) Secret generation Auto-generates 16 unique secrets \u2014 JWT keys, encryption key, database passwords, Redis password, API tokens SMTP Optionally configures production SMTP (defaults to MailHog for development) Feature flags Enable/disable Media Manager and Listmonk newsletter sync Pangolin tunnel Optionally configures tunnel credentials for public access CORS Auto-sets allowed origins based on your domain Homepage Generatesconfigs/homepage/services.yaml with all service links for your domain Permissions Creates required directories and sets container-friendly permissions After completion you'll have a fully populated .env with no placeholder passwords remaining.
Already have a .env?
If a .env file exists, the wizard offers to back it up before creating a fresh one, or update values in place.
\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\n \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557\n \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255a\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255d\n \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n \u255a\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\n \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\n \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2554\u255d \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\n \u2588\u2588\u2551\u255a\u2588\u2588\u2554\u255d\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\n \u2588\u2588\u2551 \u255a\u2550\u255d \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\n \u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\n V2 Configuration Wizard\n\n[INFO] This wizard will create your .env file, generate secure secrets,\n[INFO] and prepare your system to run the full Changemaker Lite stack.\n"},{"location":"docs/getting-started/#3-manual-setup-alternative","title":"3. Manual Setup (Alternative)","text":"If you prefer to configure things by hand:
cp .env.example .env\n Then edit .env and at minimum set these values:
V2_POSTGRES_PASSWORD=<strong password>\nREDIS_PASSWORD=<strong password>\nJWT_ACCESS_SECRET=<openssl rand -hex 32>\nJWT_REFRESH_SECRET=<openssl rand -hex 32>\nENCRYPTION_KEY=<openssl rand -hex 32>\nINITIAL_ADMIN_PASSWORD=<12+ chars, mixed case + digit>\n See Environment Variables for every available option.
"},{"location":"docs/getting-started/#4-start-services","title":"4. Start Services","text":"# Start core services\ndocker compose up -d v2-postgres redis api admin\n\n# Run database migrations and seed the initial admin account\ndocker compose exec api npx prisma migrate deploy\ndocker compose exec api npx prisma db seed\n"},{"location":"docs/getting-started/#5-log-in","title":"5. Log In","text":"Open http://localhost:3000 and sign in with the admin email and password you configured.
Change your password
If you used the wizard's generated password, change it immediately from the admin dashboard.
"},{"location":"docs/getting-started/#optional-services","title":"Optional Services","text":"Once the core is running, add more services as needed:
# Reverse proxy (required for subdomain routing)\ndocker compose up -d nginx\n\n# Video library\ndocker compose up -d media-api\n\n# Newsletters\ndocker compose up -d listmonk-app\n\n# Service dashboard\ndocker compose up -d homepage\n\n# All services at once\ndocker compose up -d\n\n# Monitoring stack (Prometheus, Grafana, Alertmanager)\ndocker compose --profile monitoring up -d\n"},{"location":"docs/getting-started/#next-steps","title":"Next Steps","text":".env reference with every configurable optionChangemaker Lite uses a single .env file at the project root to configure all services. Copy the example file to get started:
cp .env.example .env\n Security Essentials
REQUIRED_STRONG_PASSWORD_CHANGE_THIS value before starting servicesopenssl rand -hex 32 (or -hex 16 where noted).env to version controlVariables 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 Must be set before first run Has a working default; change for production Feature flag \u2014 opt-in"},{"location":"docs/getting-started/environment-variables/#general","title":"General","text":"Variable Default DescriptionNODE_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."},{"location":"docs/getting-started/environment-variables/#postgresql-main-database","title":"PostgreSQL (Main Database)","text":"The primary database for both the Express API and the Fastify Media API (shared).
Variable Default DescriptionV2_POSTGRES_USER changemaker Database username. V2_POSTGRES_PASSWORD \u2014 Must change. Database password. V2_POSTGRES_DB changemaker_v2 Database name. V2_POSTGRES_PORT 5433 Host port mapping. The container listens on 5432 internally. Connection string
The DATABASE_URL is constructed automatically inside Docker. If running locally, set:
DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2\n"},{"location":"docs/getting-started/environment-variables/#jwt-authentication","title":"JWT Authentication","text":"Variable Default Description JWT_ACCESS_SECRET \u2014 Secret for signing access tokens. Generate with openssl rand -hex 32. JWT_REFRESH_SECRET \u2014 Secret for signing refresh tokens. Must differ from the access secret. 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."},{"location":"docs/getting-started/environment-variables/#encryption-key","title":"Encryption Key","text":"Variable Default Description ENCRYPTION_KEY \u2014 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 production (NODE_ENV=production)."},{"location":"docs/getting-started/environment-variables/#initial-admin-account","title":"Initial Admin Account","text":"These credentials create the first super-admin user during database seeding (npx prisma db seed).
INITIAL_ADMIN_EMAIL admin@cmlite.org Email address for the initial admin. INITIAL_ADMIN_PASSWORD \u2014 Must change. Must be 12+ characters with uppercase, lowercase, and a digit. Change this password after first login."},{"location":"docs/getting-started/environment-variables/#api-server","title":"API Server","text":"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. 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\n Then restart the API: docker compose restart api"},{"location":"docs/getting-started/environment-variables/#admin-gui","title":"Admin GUI","text":"Variable Default Description ADMIN_PORT 3000 Host port for the React admin dashboard. ADMIN_URL http://localhost:3000 Public URL of the admin GUI."},{"location":"docs/getting-started/environment-variables/#nginx-reverse-proxy","title":"Nginx Reverse Proxy","text":"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)."},{"location":"docs/getting-started/environment-variables/#redis","title":"Redis","text":"Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.
Variable Default DescriptionREDIS_PASSWORD \u2014 Must change. Redis requires authentication. REDIS_URL redis://:${REDIS_PASSWORD}@redis-changemaker:6379 Full connection URL. Uses the password variable automatically."},{"location":"docs/getting-started/environment-variables/#email-smtp","title":"Email / SMTP","text":"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. Development email
With EMAIL_TEST_MODE=true, all outgoing email is captured in MailHog at http://localhost:8025. No real emails are sent.
Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.
Variable Default DescriptionLISTMONK_PORT 9001 Listmonk web UI port. LISTMONK_DB_PORT 5432 Listmonk's own PostgreSQL port (separate from the main DB). LISTMONK_DB_USER listmonk Listmonk database user. LISTMONK_DB_PASSWORD \u2014 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 \u2014 Password for the Listmonk web dashboard. LISTMONK_API_USER v2-api API user for programmatic access (auto-created by init container). LISTMONK_API_TOKEN \u2014 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 \u2014 Same as LISTMONK_API_TOKEN. LISTMONK_SYNC_ENABLED false Set to true to sync participants/locations/users to Listmonk lists. LISTMONK_PROXY_PORT 9002 Nginx proxy port for Listmonk. Listmonk SMTP settings Listmonk has its own SMTP configuration, separate from the main platform's:
Variable Default DescriptionLISTMONK_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."},{"location":"docs/getting-started/environment-variables/#represent-api-canadian-electoral-data","title":"Represent API (Canadian Electoral Data)","text":"Variable Default Description REPRESENT_API_URL https://represent.opennorth.ca OpenNorth Represent API endpoint. Used for postal code \u2192 representative lookups. No API key required."},{"location":"docs/getting-started/environment-variables/#nocodb-data-browser","title":"NocoDB (Data Browser)","text":"Read-only database browser. Useful for inspecting data without SQL.
Variable Default DescriptionNOCODB_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 \u2014 NocoDB admin password."},{"location":"docs/getting-started/environment-variables/#media-manager","title":"Media Manager","text":"Video library with upload, analytics, scheduling, and a public gallery.
Variable Default DescriptionENABLE_MEDIA_FEATURES false 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. VIDEO_PLAYER_DEBUG false Enable verbose video player logging. 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)."},{"location":"docs/getting-started/environment-variables/#gitea-git-hosting","title":"Gitea (Git Hosting)","text":"Self-hosted Git repository. Optional service.
Variable Default DescriptionGITEA_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 \u2014 Gitea database password. GITEA_DB_ROOT_PASSWORD \u2014 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."},{"location":"docs/getting-started/environment-variables/#n8n-workflow-automation","title":"n8n (Workflow Automation)","text":"Variable Default Description N8N_PORT 5678 n8n web UI port. N8N_HOST n8n.cmlite.org Public hostname for n8n. N8N_ENCRYPTION_KEY \u2014 Encryption key for n8n credentials storage. N8N_USER_EMAIL admin@example.com Initial n8n admin email. N8N_USER_PASSWORD \u2014 Initial n8n admin password. GENERIC_TIMEZONE UTC Timezone for n8n cron triggers."},{"location":"docs/getting-started/environment-variables/#mkdocs-documentation","title":"MkDocs (Documentation)","text":"Variable Default Description MKDOCS_PORT 4003 MkDocs dev server port (live preview). MKDOCS_SITE_SERVER_PORT 4001 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."},{"location":"docs/getting-started/environment-variables/#code-server-web-ide","title":"Code Server (Web IDE)","text":"Variable Default Description CODE_SERVER_PORT 8888 Code Server web UI port. CODE_SERVER_URL http://code-server:8080 Internal container URL. USER_NAME coder User account inside the Code Server container."},{"location":"docs/getting-started/environment-variables/#homepage-service-dashboard","title":"Homepage (Service Dashboard)","text":"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."},{"location":"docs/getting-started/environment-variables/#mini-qr-qr-code-generator","title":"Mini QR (QR Code Generator)","text":"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)."},{"location":"docs/getting-started/environment-variables/#excalidraw-whiteboard","title":"Excalidraw (Whiteboard)","text":"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."},{"location":"docs/getting-started/environment-variables/#mailhog-development-email","title":"MailHog (Development Email)","text":"Variable Default Description MAILHOG_SMTP_PORT 1025 SMTP port for capturing emails. MAILHOG_WEB_PORT 8025 Web UI to view captured emails."},{"location":"docs/getting-started/environment-variables/#nar-national-address-register","title":"NAR (National Address Register)","text":"Canadian address data import for geographic canvassing.
Variable Default DescriptionNAR_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.
"},{"location":"docs/getting-started/environment-variables/#geocoding","title":"Geocoding","text":"Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.
Variable Default DescriptionMAPBOX_API_KEY (empty) Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. Sign up. 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."},{"location":"docs/getting-started/environment-variables/#overpass-area-import","title":"Overpass / Area Import","text":"OpenStreetMap data import for map enrichment.
Variable Default DescriptionOVERPASS_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."},{"location":"docs/getting-started/environment-variables/#pangolin-tunnel","title":"Pangolin Tunnel","text":"Expose services to the internet without port forwarding, using a self-hosted Pangolin instance.
Variable Default DescriptionPANGOLIN_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). Setup flow
Configure the tunnel from Admin \u2192 Settings \u2192 Pangolin. The setup wizard walks you through creating a site, copying credentials, and connecting the Newt container. See Deployment for the full guide.
"},{"location":"docs/getting-started/environment-variables/#monitoring","title":"Monitoring","text":"These services are behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Variable Default Description PROMETHEUS_PORT 9090 Prometheus web UI / query port. GRAFANA_PORT 3001 Grafana dashboard port. GRAFANA_ADMIN_PASSWORD admin Change in production. GRAFANA_ROOT_URL http://localhost:3001 Public URL for Grafana (used in links). CADVISOR_PORT 8080 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 Change in production."},{"location":"docs/getting-started/environment-variables/#generating-secrets","title":"Generating Secrets","text":"Use these commands to generate all required secrets at once:
# JWT secrets (two separate values)\necho \"JWT_ACCESS_SECRET=$(openssl rand -hex 32)\"\necho \"JWT_REFRESH_SECRET=$(openssl rand -hex 32)\"\n\n# Encryption key (must differ from JWT secrets)\necho \"ENCRYPTION_KEY=$(openssl rand -hex 32)\"\n\n# Database and Redis passwords\necho \"V2_POSTGRES_PASSWORD=$(openssl rand -hex 24)\"\necho \"REDIS_PASSWORD=$(openssl rand -hex 24)\"\n\n# Listmonk\necho \"LISTMONK_DB_PASSWORD=$(openssl rand -hex 24)\"\necho \"LISTMONK_WEB_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\nLISTMONK_TOKEN=$(openssl rand -hex 16)\necho \"LISTMONK_API_TOKEN=$LISTMONK_TOKEN\"\necho \"LISTMONK_ADMIN_PASSWORD=$LISTMONK_TOKEN\"\n\n# Supporting services\necho \"GITEA_DB_PASSWD=$(openssl rand -hex 24)\"\necho \"GITEA_DB_ROOT_PASSWORD=$(openssl rand -hex 24)\"\necho \"N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)\"\necho \"N8N_USER_PASSWORD=$(openssl rand -hex 16)\"\necho \"NC_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\necho \"INITIAL_ADMIN_PASSWORD=$(openssl rand -base64 18)\"\n 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).
For a basic deployment with campaigns, map, and admin:
Required variablesV2_POSTGRES_PASSWORD=...\nREDIS_PASSWORD=...\nJWT_ACCESS_SECRET=...\nJWT_REFRESH_SECRET=...\nENCRYPTION_KEY=...\nINITIAL_ADMIN_PASSWORD=...\n Start servicesdocker compose up -d v2-postgres redis api admin\n For the complete platform including media, newsletters, monitoring, and all services:
Additional variables needed# Everything above, plus:\nENABLE_MEDIA_FEATURES=true\nLISTMONK_SYNC_ENABLED=true\nLISTMONK_DB_PASSWORD=...\nLISTMONK_WEB_ADMIN_PASSWORD=...\nLISTMONK_API_TOKEN=...\nNC_ADMIN_PASSWORD=...\nGITEA_DB_PASSWD=...\nGITEA_DB_ROOT_PASSWORD=...\nN8N_ENCRYPTION_KEY=...\nN8N_USER_PASSWORD=...\nEMAIL_TEST_MODE=false\nSMTP_HOST=smtp.your-provider.com\nSMTP_PORT=587\nSMTP_USER=you@example.com\nSMTP_PASS=your-smtp-password\n Start servicesdocker compose up -d\ndocker compose --profile monitoring up -d\n"},{"location":"docs/services/","title":"Services","text":"Changemaker Lite orchestrates 20+ services via Docker Compose. This page is your map to every service: what it does, how to reach it, and where to find its upstream documentation.
"},{"location":"docs/services/#core-platform","title":"Core Platform","text":"The essential services that power the application.
Express API
Main V2 API server. Handles authentication, campaigns, map, shifts, pages, email, and all business logic. Prisma ORM with PostgreSQL.
Port: 4000 \u00b7 Container: changemaker-v2-api
API Reference
Fastify Media API
Video library server. Upload, metadata extraction (FFprobe), analytics, scheduled publishing, and public gallery. Shares the same PostgreSQL database.
Port: 4100 \u00b7 Container: changemaker-media-api
Media Guide
Admin GUI
React single-page application (Vite + Ant Design + Zustand). Serves the admin dashboard, public campaign pages, volunteer portal, and media gallery \u2014 all from one build.
Port: 3000 \u00b7 Container: changemaker-v2-admin
Feature Guides
PostgreSQL 16
Primary database shared by both APIs. Managed by Prisma migrations. Contains 30+ tables covering users, campaigns, locations, shifts, media, and more.
Port: 5433 (host) / 5432 (container) \u00b7 Container: changemaker-v2-postgres
PostgreSQL Docs
Redis
In-memory store for rate limiting, BullMQ job queues (email, video scheduling), geocoding cache, and session data. Requires authentication.
Port: 6379 \u00b7 Container: redis-changemaker
Redis Docs
Nginx
Reverse proxy handling all subdomain routing (app., api., media., docs., etc.). Includes security headers (HSTS, CSP, Permissions-Policy) and WebSocket support.
Port: 80 / 443 \u00b7 Container: changemaker-v2-nginx
Nginx Docs
Listmonk
Self-hosted newsletter and mailing list manager. Drop-in replacement for Mailchimp. Opt-in sync with the main platform imports participants, locations, and users as subscriber lists.
Port: 9001 \u00b7 Container: listmonk-app \u00b7 Subdomain: listmonk.DOMAIN
Listmonk Docs
MailHog
Email capture for development. All outgoing email is intercepted and displayed in a web UI when EMAIL_TEST_MODE=true. No real emails are sent.
Port: 8025 (web) / 1025 (SMTP) \u00b7 Container: mailhog-changemaker \u00b7 Subdomain: mail.DOMAIN
MailHog GitHub
MkDocs
Material-themed documentation site with full-text search, blog, social cards, and Jinja2 template overrides. Two containers: live preview (dev) and static site (production).
Port: 4003 (dev) / 4001 (static) \u00b7 Container: mkdocs-changemaker \u00b7 Subdomain: docs.DOMAIN
MkDocs Material
Code Server
Full VS Code in the browser. Edit configuration files, templates, and documentation from anywhere without SSH. Supports extensions.
Port: 8888 \u00b7 Container: code-server-changemaker \u00b7 Subdomain: code.DOMAIN
Code Server Docs
NocoDB
Airtable-alternative database browser. Provides a spreadsheet-like interface to browse, filter, sort, and export campaign data. Read-only access to the main database.
Port: 8091 \u00b7 Container: changemaker-v2-nocodb \u00b7 Subdomain: db.DOMAIN
NocoDB Docs
n8n
Visual workflow automation platform. Connect APIs, trigger actions on events, schedule tasks, and build custom integrations \u2014 all without code. 400+ built-in integrations.
Port: 5678 \u00b7 Container: n8n-changemaker \u00b7 Subdomain: n8n.DOMAIN
n8n Docs
Gitea
Self-hosted Git repository hosting. Version control for campaign code, configuration, templates, and documentation. Includes issues, pull requests, and CI/CD.
Port: 3030 (web) / 2222 (SSH) \u00b7 Container: gitea-changemaker \u00b7 Subdomain: git.DOMAIN
Gitea Docs
Mini QR
Lightweight QR code generator. Produces PNG images for walk sheets, campaign materials, and event signage. Embedded in the admin dashboard via iframe.
Port: 8089 \u00b7 Container: mini-qr \u00b7 Subdomain: qr.DOMAIN
Homepage
Service dashboard showing the status of all containers at a glance. Auto-generated services.yaml from config.sh provides both production and local links.
Port: 3010 \u00b7 Container: homepage-changemaker \u00b7 Subdomain: home.DOMAIN
Homepage Docs
Excalidraw
Collaborative whiteboard for brainstorming, diagramming, and visual planning. Real-time collaboration via WebSocket.
Port: 8090 \u00b7 Container: excalidraw-changemaker \u00b7 Subdomain: draw.DOMAIN
Excalidraw
Pangolin + Newt
Self-hosted tunnel server with the Newt client container. Exposes your services to the internet without port forwarding. Handles SSL/TLS, works behind CGNAT and double NAT.
Container: newt-changemaker \u00b7 Managed from Admin \u2192 Settings \u2192 Tunnel
Deployment Guide \u00b7 Pangolin GitHub
These services run behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Prometheus
Metrics collection and time-series database. Scrapes 12 custom cm_* application metrics plus container, host, and Redis metrics. Pre-configured alert rules.
Port: 9090 \u00b7 Container: prometheus-changemaker
Prometheus Docs
Grafana
Metrics visualization with 3 auto-provisioned dashboards: API Overview, Infrastructure, and Campaign Activity. Supports custom dashboards and alerting.
Port: 3001 \u00b7 Container: grafana-changemaker \u00b7 Subdomain: grafana.DOMAIN
Grafana Docs
Alertmanager
Alert routing and notification delivery. Receives alerts from Prometheus and dispatches to Gotify, email, or webhooks based on configurable rules.
Port: 9093 \u00b7 Container: alertmanager-changemaker
Alertmanager Docs
cAdvisor
Container resource metrics. Exposes CPU, memory, network, and filesystem usage per container for Prometheus to scrape.
Port: 8080 \u00b7 Container: cadvisor-changemaker
cAdvisor GitHub
Node Exporter
Host system metrics. Reports CPU, memory, disk, and network stats for the underlying server.
Port: 9100 \u00b7 Container: node-exporter-changemaker
Node Exporter
Redis Exporter
Redis metrics for Prometheus. Exposes connection counts, memory usage, command stats, and keyspace info.
Port: 9121 \u00b7 Container: redis-exporter-changemaker
Redis Exporter GitHub
Gotify
Self-hosted push notification server. Receives alerts from Alertmanager and delivers them to mobile/desktop clients.
Port: 8889 \u00b7 Container: gotify-changemaker
Gotify Docs
All services at a glance with their default ports and subdomains.
Service Port Subdomain Docker Profile Express API 4000api. default Media API 4100 media. default Admin GUI 3000 app. default PostgreSQL 5433 \u2014 default Redis 6379 \u2014 default Nginx 80/443 (all) default Listmonk 9001 listmonk. default MailHog 8025 mail. default MkDocs (dev) 4003 docs. default MkDocs (static) 4001 (root) default Code Server 8888 code. default NocoDB 8091 db. default n8n 5678 n8n. default Gitea 3030 git. default Mini QR 8089 qr. default Homepage 3010 home. default Excalidraw 8090 draw. default Newt (tunnel) \u2014 \u2014 default Prometheus 9090 \u2014 monitoring Grafana 3001 grafana. monitoring Alertmanager 9093 \u2014 monitoring cAdvisor 8080 \u2014 monitoring Node Exporter 9100 \u2014 monitoring Redis Exporter 9121 \u2014 monitoring Gotify 8889 \u2014 monitoring Starting services selectively
You don't need to run everything. Start only what you need:
# Core only\ndocker compose up -d v2-postgres redis api admin\n\n# Add nginx for subdomain routing\ndocker compose up -d nginx\n\n# Add monitoring\ndocker compose --profile monitoring up -d\n See Getting Started for the recommended startup order.
"},{"location":"docs/troubleshooting/","title":"Troubleshooting","text":"Common issues and their solutions when running Changemaker Lite.
Under Construction
This troubleshooting guide is being expanded. Check back soon for more solutions.
"},{"location":"docs/troubleshooting/#cors-errors-in-production","title":"CORS Errors in Production","text":"Symptom: Browser console shows CORS errors when accessing production domain.
Fix: Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000\n Then restart the API: docker compose restart api
Symptom: All API endpoints return 302 redirects to Pangolin auth page.
Fix: In the Pangolin dashboard, set each resource to \"Not Protected\" (public access).
"},{"location":"docs/troubleshooting/#database-connection-failures","title":"Database Connection Failures","text":"docker compose ps v2-postgresDATABASE_URL in .envdocker compose logs v2-postgres --tail 50docker compose ps redis-changemakerREDIS_PASSWORD and REDIS_URL format in .envdocker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD pingdocker compose logs api --tail 100.env.example)docker compose exec api npx prisma migrate deployWelcome! This guide walks you through everything you need to know as a campaign volunteer \u2014 from signing up for your first shift to canvassing door-to-door with the GPS map.
"},{"location":"docs/volunteer/#getting-started","title":"Getting Started","text":""},{"location":"docs/volunteer/#1-sign-up-for-a-shift","title":"1. Sign Up for a Shift","text":"Visit the Shifts page (your organizer will share the link, or find it at /shifts). Browse available time slots, pick one that works for you, and fill in your name and email. You'll receive a confirmation email with your login credentials.
Go to the login page and sign in with the email and password from your confirmation. If you already have an account, just sign in normally.
"},{"location":"docs/volunteer/#3-open-the-volunteer-portal","title":"3. Open the Volunteer Portal","text":"After logging in, you'll land on the volunteer map \u2014 a full-screen view of your assigned canvassing area. This is your home base.
"},{"location":"docs/volunteer/#the-volunteer-map","title":"The Volunteer Map","text":"The volunteer map is your main tool for canvassing. It shows all the addresses in your assigned area and tracks your position with GPS.
"},{"location":"docs/volunteer/#what-you-see","title":"What You See","text":"Visit Shifts in the bottom navigation to see your upcoming and past shifts. Each shift shows:
The Activity tab shows your complete visit history:
The Routes tab shows your past canvassing routes on a map. This helps you see which areas you've covered and plan your next outing.
"},{"location":"docs/volunteer/#browsing-public-pages","title":"Browsing Public Pages","text":"Tap your name/avatar in the header and select Browse Site to visit the public pages \u2014 campaigns, the public map, and shift signups. This is useful for sharing links with friends or checking campaign progress.
"},{"location":"docs/volunteer/#faq","title":"FAQ","text":"Q: I can't find my assigned area on the map. A: Make sure your shift has an area assigned. Check with your organizer if nothing appears.
Q: My GPS isn't working. A: Make sure you've allowed location access in your browser. Try moving to a window or stepping outside for better signal.
Q: I accidentally recorded the wrong outcome. A: Visit the same address again and record the correct outcome. The most recent visit is what counts.
Q: How do I sign up for more shifts? A: Visit the public shifts page (ask your organizer for the link, or go to /shifts).