changemaker.lite/mkdocs/site/search/search_index.json
2026-02-18 10:01:54 -07:00

1 line
127 KiB
JSON

{"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":"<p>Testing page. </p> <p>\ud83d\uded2</p> Browse Our Products <p>Reports, toolkits, event tickets, and more.</p> Shop Now Choose Your Plan <p>Get access to exclusive content and features.</p> View Plans <p>\u2764\ufe0f</p> Support Our Cause <p>Your contribution helps us create lasting change in our community.</p> Donate Now 9:54 Testing This Sucker 0 views Watch \u2192"},{"location":"blog/","title":"Blog","text":""},{"location":"docs/","title":"Documentation","text":"<p>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.</p>"},{"location":"docs/#use-the-platform","title":"Use the Platform","text":"<ul> <li> <p> Getting Started</p> <p>Install Changemaker Lite, create your first admin account, and explore the dashboard.</p> <p> Getting Started</p> </li> <li> <p> Feature Guides</p> <p>Campaigns, email advocacy, response walls, map locations, landing pages, and media.</p> <p> Feature Guides</p> </li> <li> <p> Administration</p> <p>User management, roles and permissions, site settings, email templates, and newsletters.</p> <p> Administration</p> </li> <li> <p> Volunteer Guide</p> <p>Sign up for shifts, use the canvassing map, record visits, and track your activity.</p> <p> Volunteer Guide</p> </li> </ul>"},{"location":"docs/#deploy-operate","title":"Deploy &amp; Operate","text":"<ul> <li> <p> Deployment</p> <p>Docker Compose setup, environment variables, SSL/TLS, backups, and production checklist.</p> <p> Deployment</p> </li> <li> <p> Architecture</p> <p>Dual API design, database schema, authentication flow, and system diagram.</p> <p> Architecture</p> </li> <li> <p> Services</p> <p>Nginx routing, Redis, PostgreSQL, Listmonk, MkDocs, Gitea, NocoDB, and more.</p> <p> Services</p> </li> <li> <p> Monitoring</p> <p>Prometheus metrics, Grafana dashboards, Alertmanager rules, and health checks.</p> <p> Monitoring Coming soon</p> </li> </ul>"},{"location":"docs/#reference","title":"Reference","text":"<ul> <li> <p> API Reference</p> <p>REST endpoints for auth, campaigns, locations, shifts, media, and more.</p> <p> API Reference</p> </li> <li> <p> Troubleshooting</p> <p>Common errors, CORS issues, database problems, tunnel debugging, and FAQ.</p> <p> Troubleshooting</p> </li> <li> <p> Security</p> <p>Password policy, rate limiting, token rotation, encryption, and audit report.</p> <p> Security See Deployment</p> </li> <li> <p> Contributing</p> <p>Development setup, code style, git workflow, and pull request guidelines.</p> <p> Contributing Coming soon</p> </li> </ul>"},{"location":"docs/#platform-at-a-glance","title":"Platform at a Glance","text":"Component Technology Purpose Main API Express.js + Prisma Auth, campaigns, map, shifts, pages, email Media API Fastify + Prisma Video library, analytics, upload, scheduling Admin GUI React + Ant Design + Zustand Dashboard for admins and organizers Database PostgreSQL 16 Single shared database for both APIs Cache Redis Rate limiting, BullMQ jobs, geocoding queue Proxy Nginx Subdomain routing, security headers, SSL Tunnel Pangolin + Newt Expose services without port forwarding Monitoring Prometheus + Grafana Metrics, dashboards, alerts <p>New here?</p> <p>Start with the Getting Started guide to have the platform running in under 30 minutes.</p> <p>Looking for the source?</p> <p>Changemaker Lite is 100% open source. Browse the code on Gitea.</p>"},{"location":"docs/admin/","title":"Administration","text":"<p>This section covers day-to-day administration of the Changemaker Lite platform.</p> <p>Under Construction</p> <p>Detailed admin documentation is being written. Check back soon.</p>"},{"location":"docs/admin/#topics","title":"Topics","text":"<ul> <li>User Management \u2014 create, edit, and deactivate user accounts</li> <li>Roles &amp; Permissions \u2014 SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN, USER, TEMP</li> <li>Site Settings \u2014 configure site name, contact info, and feature flags</li> <li>Email Templates \u2014 manage reusable email templates</li> <li>Newsletter Sync \u2014 Listmonk integration for subscriber management</li> <li>Email Queue \u2014 monitor and manage the BullMQ advocacy email queue</li> <li>Landing Pages \u2014 create and publish pages with the GrapesJS editor</li> </ul>"},{"location":"docs/api/","title":"API Reference","text":"<p>Changemaker Lite exposes two REST APIs sharing a single PostgreSQL database.</p> Server Framework Port Purpose Main API Express.js <code>4000</code> Auth, campaigns, map, shifts, canvassing, pages, email, settings Media API Fastify <code>4100</code> Video library, analytics, playlists, reactions, comments <p>Both APIs use JWT Bearer authentication and return JSON. All request/response bodies are <code>application/json</code> unless noted otherwise.</p>"},{"location":"docs/api/#authentication","title":"Authentication","text":""},{"location":"docs/api/#token-flow","title":"Token Flow","text":"<pre><code>sequenceDiagram\n participant Client\n participant API\n participant DB\n\n Client-&gt;&gt;API: POST /api/auth/login {email, password}\n API-&gt;&gt;DB: Verify credentials\n DB--&gt;&gt;API: User record\n API--&gt;&gt;Client: {accessToken, refreshToken}\n Note over Client: Store tokens\n\n Client-&gt;&gt;API: GET /api/campaigns (Authorization: Bearer &lt;accessToken&gt;)\n API--&gt;&gt;Client: 200 OK\n\n Note over Client: Access token expires (15 min)\n\n Client-&gt;&gt;API: POST /api/auth/refresh {refreshToken}\n API-&gt;&gt;DB: Rotate token (atomic transaction)\n DB--&gt;&gt;API: New token pair\n API--&gt;&gt;Client: {accessToken, refreshToken}</code></pre>"},{"location":"docs/api/#headers","title":"Headers","text":"<p>All authenticated requests require:</p> <pre><code>Authorization: Bearer &lt;accessToken&gt;\n</code></pre> <p>The Media API also accepts tokens via query parameter for SSE streams:</p> <pre><code>GET /api/public/:id/chat-stream?token=&lt;accessToken&gt;\n</code></pre>"},{"location":"docs/api/#roles","title":"Roles","text":"Role Access Level <code>SUPER_ADMIN</code> Full platform access <code>INFLUENCE_ADMIN</code> Campaign and advocacy management <code>MAP_ADMIN</code> Map, locations, shifts, canvassing <code>USER</code> Volunteer portal, public features <code>TEMP</code> Limited access (auto-created on public shift signup)"},{"location":"docs/api/#middleware-reference","title":"Middleware Reference","text":"Middleware Effect <code>authenticate</code> Requires valid JWT. Sets <code>req.user</code> with <code>id</code>, <code>email</code>, <code>role</code>. Returns <code>401</code> if missing or invalid. <code>optionalAuth</code> Same as <code>authenticate</code> but continues without user if token is absent. <code>requireRole(...roles)</code> Checks user role against allowed list. Returns <code>403</code> if not authorized. <code>requireNonTemp</code> Blocks <code>TEMP</code> users. Returns <code>403</code>. <code>validate(schema, source)</code> Validates request body/query/params against a Zod schema. Returns <code>400</code> on failure."},{"location":"docs/api/#error-responses","title":"Error Responses","text":"<p>All errors follow a consistent format:</p> <pre><code>{\n \"error\": {\n \"message\": \"Human-readable error description\",\n \"code\": \"ERROR_CODE\",\n \"statusCode\": 400\n }\n}\n</code></pre> Status Code Meaning <code>400</code> <code>VALIDATION_ERROR</code> Request body/query failed schema validation <code>401</code> <code>UNAUTHORIZED</code> Missing or invalid access token <code>403</code> <code>FORBIDDEN</code> Valid token but insufficient role <code>404</code> <code>NOT_FOUND</code> Resource does not exist <code>429</code> <code>RATE_LIMITED</code> Too many requests (see Rate Limits) <code>500</code> <code>INTERNAL_ERROR</code> Unexpected server error <p>Enumeration Prevention</p> <p>Auth endpoints (<code>/login</code>, <code>/register</code>, <code>/forgot-password</code>) return generic success messages to prevent user enumeration. A <code>401</code> from <code>/api/auth/me</code> does not reveal whether the user exists.</p>"},{"location":"docs/api/#rate-limits","title":"Rate Limits","text":"<p>Rate limits are Redis-backed and keyed by IP address.</p> Endpoint Group Window Max Requests Redis Prefix Auth (login, register, refresh) 15 min 10 <code>rl:auth:</code> Email sending 1 hour 30 <code>rl:email:</code> Response submission 1 hour 10 <code>rl:response:</code> Shift signup 1 hour 10 <code>rl:shift-signup:</code> Canvass visits 1 min 30 <code>rl:canvass-visit:</code> Canvass bulk visits 1 min 5 <code>rl:canvass-visit-bulk:</code> GPS tracking 1 min 6 <code>rl:gps-tracking:</code> Canvass geocode 1 min 10 <code>rl:canvass-geocode:</code> Observability 1 min 20 <code>rl:observability:</code> Health/metrics 1 min 30 <code>rl:health-metrics:</code> Global (all other) Configurable Configurable <code>rl:global:</code> <p>When rate-limited, the API returns:</p> <pre><code>{\n \"error\": {\n \"message\": \"Too many requests, please try again later\",\n \"code\": \"RATE_LIMITED\",\n \"statusCode\": 429\n }\n}\n</code></pre>"},{"location":"docs/api/#main-api-express-port-4000","title":"Main API (Express \u2014 Port 4000)","text":""},{"location":"docs/api/#health-metrics","title":"Health &amp; Metrics","text":"Method Path Auth Description GET <code>/api/health</code> Health check \u2014 PostgreSQL + Redis ping GET <code>/api/metrics</code> Prometheus metrics (text/plain) Health response <pre><code>{\n \"status\": \"healthy\",\n \"checks\": {\n \"database\": \"ok\",\n \"redis\": \"ok\"\n }\n}\n</code></pre>"},{"location":"docs/api/#auth","title":"Auth","text":"<p>Prefix: <code>/api/auth</code></p> Method Path Auth Rate Limited Description POST <code>/api/auth/login</code> Email + password login POST <code>/api/auth/register</code> Create account (always <code>USER</code> role) POST <code>/api/auth/verify-email</code> Verify email with token POST <code>/api/auth/resend-verification</code> Resend verification email POST <code>/api/auth/forgot-password</code> Send password reset email POST <code>/api/auth/reset-password</code> Set new password with reset token POST <code>/api/auth/refresh</code> Rotate refresh token \u2192 new token pair POST <code>/api/auth/logout</code> Invalidate refresh token GET <code>/api/auth/me</code> Current user profile Login request &amp; response <p>Request: <pre><code>{\n \"email\": \"admin@example.com\",\n \"password\": \"SecurePass123!\"\n}\n</code></pre> Response: <pre><code>{\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</code></pre></p> <p>Password Policy</p> <p>Passwords must be at least 12 characters with at least one uppercase letter, one lowercase letter, and one digit.</p>"},{"location":"docs/api/#users","title":"Users","text":"<p>Prefix: <code>/api/users</code> \u00b7 Auth: All routes require authentication</p> Method Path Role Description GET <code>/api/users</code> Admin Paginated user list with search, role, and status filters GET <code>/api/users/:id</code> Admin or self Single user profile POST <code>/api/users</code> Admin Create user PUT <code>/api/users/:id</code> Admin or self Update user (non-admins cannot change role/status) POST <code>/api/users/:id/approve</code> Admin Approve pending user; sends approval email POST <code>/api/users/:id/reject</code> Admin Reject pending user DELETE <code>/api/users/:id</code> Admin Delete user <p>Query parameters for <code>GET /api/users</code>:</p> Param Type Description <code>page</code> number Page number (default 1) <code>limit</code> number Items per page (default 20) <code>search</code> string Search by name or email <code>role</code> string Filter by role <code>status</code> string Filter by status"},{"location":"docs/api/#dashboard","title":"Dashboard","text":"<p>Prefix: <code>/api/dashboard</code> \u00b7 Auth: Admin roles required</p> Method Path Role Description GET <code>/api/dashboard/summary</code> Any admin Platform-wide counts (users, campaigns, locations, shifts) GET <code>/api/dashboard/system</code> <code>SUPER_ADMIN</code> Hardware + OS info (CPU, memory, disk) GET <code>/api/dashboard/containers</code> <code>SUPER_ADMIN</code> Docker container statuses GET <code>/api/dashboard/weather</code> Any admin Current weather at map center coordinates GET <code>/api/dashboard/api-metrics</code> <code>SUPER_ADMIN</code> Prometheus API performance metrics GET <code>/api/dashboard/time-series</code> <code>SUPER_ADMIN</code> Prometheus time-series data GET <code>/api/dashboard/container-resources</code> <code>SUPER_ADMIN</code> cAdvisor CPU/memory/network per container <p>Query parameters for <code>GET /api/dashboard/time-series</code>:</p> Param Type Description <code>metrics</code> string Comma-separated metric keys (whitelist-validated) <code>range</code> string Time range (e.g., <code>1h</code>, <code>24h</code>, <code>7d</code>) <code>step</code> string Sample interval (e.g., <code>5m</code>, <code>1h</code>)"},{"location":"docs/api/#campaigns","title":"Campaigns","text":""},{"location":"docs/api/#admin-crud","title":"Admin CRUD","text":"<p>Prefix: <code>/api/campaigns</code> \u00b7 Auth: Admin roles</p> Method Path Description GET <code>/api/campaigns</code> Paginated campaign list GET <code>/api/campaigns/:id</code> Single campaign detail POST <code>/api/campaigns</code> Create campaign PUT <code>/api/campaigns/:id</code> Update campaign DELETE <code>/api/campaigns/:id</code> Delete campaign"},{"location":"docs/api/#public","title":"Public","text":"Method Path Auth Description GET <code>/api/campaigns/public</code> List all active campaigns GET <code>/api/campaigns/:slug/details</code> Campaign detail by slug (ACTIVE only)"},{"location":"docs/api/#user-submissions","title":"User Submissions","text":"<p>Auth: Authenticated, non-TEMP users</p> Method Path Description POST <code>/api/campaigns/user/submit</code> Submit campaign for moderation (5/hour limit) GET <code>/api/campaigns/user/my-campaigns</code> List own submitted campaigns PUT <code>/api/campaigns/user/:id</code> Edit own pending campaign"},{"location":"docs/api/#moderation","title":"Moderation","text":"<p>Auth: Admin roles</p> Method Path Description GET <code>/api/campaigns/moderation/queue</code> Campaigns pending moderation GET <code>/api/campaigns/moderation/stats</code> Moderation queue statistics PATCH <code>/api/campaigns/moderation/:id</code> Approve or reject campaign"},{"location":"docs/api/#campaign-emails","title":"Campaign Emails","text":"Method Path Auth Description POST <code>/api/campaigns/:slug/send-email</code> Send advocacy email to representatives (rate limited: 30/hour) POST <code>/api/campaigns/:slug/track-mailto</code> Track mailto link click GET <code>/api/campaigns/:id/emails</code> Admin Paginated emails for campaign GET <code>/api/campaigns/:id/email-stats</code> Admin Email statistics"},{"location":"docs/api/#responses","title":"Responses","text":"<p>Prefix: <code>/api/campaigns</code> (public) and <code>/api/responses</code> (admin + actions)</p>"},{"location":"docs/api/#public_1","title":"Public","text":"Method Path Auth Description GET <code>/api/campaigns/:slug/responses</code> List approved public responses GET <code>/api/campaigns/:slug/response-stats</code> Response statistics POST <code>/api/campaigns/:slug/responses</code> Submit response (rate limited: 10/hour) POST <code>/api/responses/:id/upvote</code> Optional Upvote a response DELETE <code>/api/responses/:id/upvote</code> Optional Remove upvote GET <code>/api/responses/:id/verify/:token</code> Verify response via email link"},{"location":"docs/api/#admin","title":"Admin","text":"<p>Auth: Admin roles</p> Method Path Description GET <code>/api/responses</code> All responses with filters PATCH <code>/api/responses/:id/status</code> Approve or reject response POST <code>/api/responses/:id/resend-verification</code> Resend verification email DELETE <code>/api/responses/:id</code> Delete response"},{"location":"docs/api/#representatives","title":"Representatives","text":"<p>Prefix: <code>/api/representatives</code></p> Method Path Auth Description GET <code>/api/representatives/by-postal/:postalCode</code> Lookup representatives by postal code (cache-first) GET <code>/api/representatives/test-connection</code> Represent API health check GET <code>/api/representatives/cache-stats</code> Admin Cache statistics GET <code>/api/representatives</code> Admin Paginated cached representatives GET <code>/api/representatives/:id</code> Admin Single cached representative DELETE <code>/api/representatives/by-postal/:postalCode</code> Admin Clear cache for postal code DELETE <code>/api/representatives/:id</code> Admin Delete cached representative <p>Query parameters for postal code lookup:</p> Param Type Description <code>refresh</code> boolean Force API call, bypass cache"},{"location":"docs/api/#email-queue","title":"Email Queue","text":"<p>Prefix: <code>/api/email-queue</code> \u00b7 Auth: Admin roles</p> Method Path Description GET <code>/api/email-queue/stats</code> BullMQ queue statistics (waiting, active, completed, failed) POST <code>/api/email-queue/pause</code> Pause email processing POST <code>/api/email-queue/resume</code> Resume email processing POST <code>/api/email-queue/clean</code> Clean completed jobs"},{"location":"docs/api/#locations","title":"Locations","text":"<p>Prefix: <code>/api/map/locations</code></p>"},{"location":"docs/api/#public_2","title":"Public","text":"Method Path Description GET <code>/api/map/locations/public</code> All geocoded locations for map (no PII); optional <code>?bounds=</code>"},{"location":"docs/api/#admin_1","title":"Admin","text":"<p>Auth: <code>SUPER_ADMIN</code> or <code>MAP_ADMIN</code></p> Method Path Description GET <code>/api/map/locations</code> Paginated locations with filters GET <code>/api/map/locations/stats</code> Location statistics GET <code>/api/map/locations/all</code> All geocoded locations for admin map GET <code>/api/map/locations/export-csv</code> CSV export GET <code>/api/map/locations/:id</code> Single location GET <code>/api/map/locations/:id/history</code> Edit history POST <code>/api/map/locations</code> Create location PUT <code>/api/map/locations/:id</code> Update location DELETE <code>/api/map/locations/:id</code> Delete location POST <code>/api/map/locations/bulk-delete</code> Bulk delete POST <code>/api/map/locations/geocode</code> Geocode single address POST <code>/api/map/locations/geocode-missing</code> Batch geocode all ungeocoded POST <code>/api/map/locations/reverse-geocode</code> Reverse geocode lat/lng to address POST <code>/api/map/locations/import-csv</code> Import from CSV (10 MB limit) POST <code>/api/map/locations/import-bulk</code> Bulk NAR or standard CSV import (100 MB limit)"},{"location":"docs/api/#bulk-geocode","title":"Bulk Geocode","text":"<p>Prefix: <code>/api/map/locations/bulk-geocode</code> \u00b7 Auth: Map admins</p> Method Path Description POST <code>/api/map/locations/bulk-geocode</code> Start BullMQ bulk geocoding job GET <code>/api/map/locations/bulk-geocode/:jobId</code> Poll job status GET <code>/api/map/locations/bulk-geocode/stats</code> Queue statistics"},{"location":"docs/api/#nar-import","title":"NAR Import","text":"<p>Prefix: <code>/api/map/nar-import</code> \u00b7 Auth: Map admins</p> Method Path Description GET <code>/api/map/nar-import/datasets</code> Available NAR datasets by province POST <code>/api/map/nar-import</code> Start province import (fire-and-forget) GET <code>/api/map/nar-import/status/:importId</code> Poll import progress NAR Import body <pre><code>{\n \"provinceCode\": \"24\",\n \"filterType\": \"city\",\n \"filterCity\": \"Edmonton\",\n \"residentialOnly\": true,\n \"deduplicateRadius\": 10,\n \"batchSize\": 500\n}\n</code></pre>"},{"location":"docs/api/#area-import","title":"Area Import","text":"<p>Prefix: <code>/api/map/area-import</code> \u00b7 Auth: Map admins</p> Method Path Description POST <code>/api/map/area-import/preview</code> Preview bounds + estimated record counts POST <code>/api/map/area-import</code> Start area import (fire-and-forget) GET <code>/api/map/area-import/status/:importId</code> Poll import progress"},{"location":"docs/api/#cuts-polygons","title":"Cuts (Polygons)","text":"<p>Prefix: <code>/api/map/cuts</code></p> Method Path Auth Description GET <code>/api/map/cuts/public</code> All public cuts as GeoJSON GET <code>/api/map/cuts</code> Map admin Paginated cuts list GET <code>/api/map/cuts/:id</code> Map admin Single cut POST <code>/api/map/cuts</code> Map admin Create cut (polygon GeoJSON) PUT <code>/api/map/cuts/:id</code> Map admin Update cut DELETE <code>/api/map/cuts/:id</code> Map admin Delete cut GET <code>/api/map/cuts/:id/locations</code> Map admin All locations within cut polygon GET <code>/api/map/cuts/:id/statistics</code> Map admin Support level breakdown GET <code>/api/map/cuts/export-geojson</code> Map admin All cuts as GeoJSON FeatureCollection GET <code>/api/map/cuts/:id/export-geojson</code> Map admin Single cut as GeoJSON Feature POST <code>/api/map/cuts/import-geojson</code> Map admin Import cuts from GeoJSON file"},{"location":"docs/api/#shifts","title":"Shifts","text":"<p>Prefix: <code>/api/map/shifts</code></p>"},{"location":"docs/api/#public_3","title":"Public","text":"Method Path Description GET <code>/api/map/shifts/public</code> List upcoming public shifts POST <code>/api/map/shifts/public/:id/signup</code> Public signup (creates TEMP user if needed; rate limited: 10/hour)"},{"location":"docs/api/#volunteer","title":"Volunteer","text":"<p>Auth: Any authenticated user</p> Method Path Description GET <code>/api/map/shifts/volunteer/upcoming</code> Upcoming shifts with signup status GET <code>/api/map/shifts/volunteer/my-signups</code> Own confirmed signups POST <code>/api/map/shifts/volunteer/:id/signup</code> Sign up for shift DELETE <code>/api/map/shifts/volunteer/:id/signup</code> Cancel signup"},{"location":"docs/api/#admin_2","title":"Admin","text":"<p>Auth: Map admins</p> Method Path Description GET <code>/api/map/shifts</code> Paginated shifts with filters GET <code>/api/map/shifts/stats</code> Statistics GET <code>/api/map/shifts/calendar</code> Calendar data (<code>?startDate=&amp;endDate=</code>) GET <code>/api/map/shifts/:id</code> Single shift with signups POST <code>/api/map/shifts</code> Create shift PUT <code>/api/map/shifts/:id</code> Update shift DELETE <code>/api/map/shifts/:id</code> Delete shift POST <code>/api/map/shifts/:id/signups</code> Admin-add volunteer DELETE <code>/api/map/shifts/:id/signups/:signupId</code> Remove volunteer POST <code>/api/map/shifts/:id/email-details</code> Email details to all volunteers"},{"location":"docs/api/#shift-series","title":"Shift Series","text":"<p>Auth: Map admins</p> Method Path Description POST <code>/api/map/shifts/series</code> Create recurring shift series GET <code>/api/map/shifts/series/:id</code> Get series PUT <code>/api/map/shifts/series/:id</code> Update series DELETE <code>/api/map/shifts/series/:id</code> Delete series"},{"location":"docs/api/#canvassing","title":"Canvassing","text":"<p>Prefix: <code>/api/map/canvass</code></p>"},{"location":"docs/api/#volunteer_1","title":"Volunteer","text":"<p>Auth: Any authenticated user</p> Method Path Description GET <code>/api/map/canvass/my/assignments</code> Shift assignments GET <code>/api/map/canvass/my/stats</code> Personal canvass statistics GET <code>/api/map/canvass/my/visits</code> Visit history GET <code>/api/map/canvass/my/session</code> Active canvass session POST <code>/api/map/canvass/sessions</code> Start canvass session POST <code>/api/map/canvass/sessions/:id/end</code> End session GET <code>/api/map/canvass/cuts/:cutId/locations</code> Locations in cut with visit annotations GET <code>/api/map/canvass/cuts/:cutId/route</code> Walking route algorithm for cut GET <code>/api/map/canvass/locations</code> All locations with visit annotations PUT <code>/api/map/canvass/locations/:id</code> Edit address (role-gated fields) POST <code>/api/map/canvass/locations</code> Create location POST <code>/api/map/canvass/reverse-geocode</code> Reverse geocode lat/lng POST <code>/api/map/canvass/geocode-search</code> Geocode address for map (rate limited: 10/min) POST <code>/api/map/canvass/visits</code> Record door knock (rate limited: 30/min) POST <code>/api/map/canvass/visits/bulk</code> Record visit for all unvisited units (rate limited: 5/min)"},{"location":"docs/api/#admin_3","title":"Admin","text":"<p>Auth: <code>SUPER_ADMIN</code> or <code>MAP_ADMIN</code></p> Method Path Description GET <code>/api/map/canvass/stats</code> Platform-wide canvass statistics GET <code>/api/map/canvass/stats/cuts/:cutId</code> Statistics for specific cut GET <code>/api/map/canvass/activity</code> Recent activity feed GET <code>/api/map/canvass/volunteers</code> All volunteers with canvass activity GET <code>/api/map/canvass/volunteers/:userId</code> Individual volunteer statistics GET <code>/api/map/canvass/visits</code> All visits with filters"},{"location":"docs/api/#gps-tracking","title":"GPS Tracking","text":"<p>Prefix: <code>/api/map/tracking</code></p>"},{"location":"docs/api/#volunteer_2","title":"Volunteer","text":"<p>Auth: Any authenticated user</p> Method Path Description POST <code>/api/map/tracking/sessions</code> Start GPS tracking session POST <code>/api/map/tracking/sessions/:id/end</code> End tracking session POST <code>/api/map/tracking/sessions/:id/points</code> Submit GPS point batch (rate limited: 6/min) POST <code>/api/map/tracking/sessions/:id/link-canvass</code> Link to canvass session GET <code>/api/map/tracking/my/session</code> Active tracking session GET <code>/api/map/tracking/my/sessions</code> Own historical sessions GET <code>/api/map/tracking/my/sessions/:id/route</code> Full route for own session"},{"location":"docs/api/#admin_4","title":"Admin","text":"<p>Auth: Map admins</p> Method Path Description GET <code>/api/map/tracking/live</code> Live volunteer positions + trails GET <code>/api/map/tracking/sessions</code> All historical tracking sessions GET <code>/api/map/tracking/sessions/:id/route</code> Full route for any session"},{"location":"docs/api/#map-settings","title":"Map Settings","text":"<p>Prefix: <code>/api/map/settings</code></p> Method Path Auth Description GET <code>/api/map/settings</code> Public map settings (center, zoom, walk sheet config) PUT <code>/api/map/settings</code> Map admin Update map settings"},{"location":"docs/api/#geocoding","title":"Geocoding","text":"<p>Prefix: <code>/api/map/geocoding</code> \u00b7 Auth: Map admins</p> Method Path Description GET <code>/api/map/geocoding/search</code> Geocode address search (<code>?q=&amp;limit=1-10</code>)"},{"location":"docs/api/#landing-pages","title":"Landing Pages","text":"<p>Prefix: <code>/api/pages</code> and <code>/api/page-blocks</code></p>"},{"location":"docs/api/#public_4","title":"Public","text":"Method Path Auth Description GET <code>/api/pages/:slug/view</code> Get published page by slug"},{"location":"docs/api/#admin_5","title":"Admin","text":"<p>Auth: Admin roles</p> Method Path Description GET <code>/api/pages</code> Paginated landing pages GET <code>/api/pages/:id</code> Single page POST <code>/api/pages</code> Create page PUT <code>/api/pages/:id</code> Update page DELETE <code>/api/pages/:id</code> Delete page POST <code>/api/pages/sync</code> Sync MkDocs overrides from filesystem POST <code>/api/pages/validate</code> Validate and repair MkDocs exports"},{"location":"docs/api/#block-library","title":"Block Library","text":"<p>Auth: Admin roles</p> Method Path Description GET <code>/api/page-blocks</code> List blocks GET <code>/api/page-blocks/:id</code> Single block POST <code>/api/page-blocks</code> Create block PUT <code>/api/page-blocks/:id</code> Update block DELETE <code>/api/page-blocks/:id</code> Delete block"},{"location":"docs/api/#email-templates","title":"Email Templates","text":"<p>Prefix: <code>/api/email-templates</code> \u00b7 Auth: Admin roles (seed/cache require <code>SUPER_ADMIN</code>)</p> Method Path Description GET <code>/api/email-templates</code> List templates GET <code>/api/email-templates/:id</code> Single template POST <code>/api/email-templates</code> Create template PUT <code>/api/email-templates/:id</code> Update template DELETE <code>/api/email-templates/:id</code> Delete template GET <code>/api/email-templates/:id/versions</code> Version history GET <code>/api/email-templates/:id/versions/:versionNumber</code> Specific version POST <code>/api/email-templates/:id/rollback</code> Rollback to prior version POST <code>/api/email-templates/validate</code> Validate Handlebars syntax POST <code>/api/email-templates/:id/test</code> Send test email (rate limited: 10/15min) GET <code>/api/email-templates/:id/test-logs</code> Test send logs POST <code>/api/email-templates/seed</code> Seed templates from filesystem POST <code>/api/email-templates/clear-cache</code> Clear template cache"},{"location":"docs/api/#qr-codes","title":"QR Codes","text":"Method Path Auth Description GET <code>/api/qr</code> Generate QR code PNG (<code>?text=&amp;size=50-500</code>) <p>Cached for 1 hour. Returns <code>image/png</code>.</p>"},{"location":"docs/api/#site-settings","title":"Site Settings","text":"<p>Prefix: <code>/api/settings</code></p> Method Path Auth Description GET <code>/api/settings</code> Public site settings (SMTP credentials stripped) GET <code>/api/settings/admin</code> <code>SUPER_ADMIN</code> Full settings including SMTP credentials PUT <code>/api/settings</code> <code>SUPER_ADMIN</code> Update settings POST <code>/api/settings/email/test-connection</code> <code>SUPER_ADMIN</code> Test SMTP connection POST <code>/api/settings/email/test-send</code> <code>SUPER_ADMIN</code> Send test email"},{"location":"docs/api/#listmonk-newsletter-sync","title":"Listmonk (Newsletter Sync)","text":"<p>Prefix: <code>/api/listmonk</code> \u00b7 Auth: <code>SUPER_ADMIN</code></p> Method Path Description GET <code>/api/listmonk</code> Sync status + connection check GET <code>/api/listmonk/stats</code> Subscriber counts from Listmonk POST <code>/api/listmonk/test-connection</code> Health check POST <code>/api/listmonk/sync/participants</code> Sync campaign participants POST <code>/api/listmonk/sync/locations</code> Sync locations POST <code>/api/listmonk/sync/users</code> Sync users POST <code>/api/listmonk/sync/all</code> Run all sync operations POST <code>/api/listmonk/reinitialize</code> Reinitialize Listmonk lists GET <code>/api/listmonk/proxy-url</code> Proxy port + JWT for iframe"},{"location":"docs/api/#documentation-management","title":"Documentation Management","text":"<p>Prefix: <code>/api/docs</code> \u00b7 Auth: Authenticated, non-TEMP (write operations require <code>SUPER_ADMIN</code>)</p> Method Path Description GET <code>/api/docs/status</code> MkDocs + Code Server availability GET <code>/api/docs/config</code> Port numbers for iframe URLs GET <code>/api/docs/mkdocs-config</code> Read raw <code>mkdocs.yml</code> PUT <code>/api/docs/mkdocs-config</code> Write <code>mkdocs.yml</code> POST <code>/api/docs/build</code> Trigger MkDocs build POST <code>/api/docs/upload</code> Upload asset (20 MB, whitelisted extensions) GET <code>/api/docs/files</code> File tree (<code>?force=true</code> bypasses cache) POST <code>/api/docs/files/rename</code> Rename or move file GET <code>/api/docs/files/*</code> Read file content PUT <code>/api/docs/files/*</code> Write file content POST <code>/api/docs/files/*</code> Create file or folder DELETE <code>/api/docs/files/*</code> Delete file or empty folder"},{"location":"docs/api/#services","title":"Services","text":"<p>Prefix: <code>/api/services</code> \u00b7 Auth: <code>SUPER_ADMIN</code></p> Method Path Description GET <code>/api/services/status</code> Health check all managed services (NocoDB, n8n, Gitea, MailHog, Mini QR, Excalidraw, Homepage) GET <code>/api/services/config</code> Port numbers + subdomain info"},{"location":"docs/api/#pangolin-tunnel-management","title":"Pangolin (Tunnel Management)","text":"<p>Prefix: <code>/api/pangolin</code> \u00b7 Auth: <code>SUPER_ADMIN</code></p> Method Path Description GET <code>/api/pangolin/status</code> Tunnel health + connection info GET <code>/api/pangolin/config</code> Current env configuration GET <code>/api/pangolin/newt-status</code> Newt container status POST <code>/api/pangolin/newt-restart</code> Restart Newt container GET <code>/api/pangolin/sites</code> List Pangolin sites GET <code>/api/pangolin/exit-nodes</code> Available exit nodes GET <code>/api/pangolin/resource-definitions</code> Resource definitions from YAML GET <code>/api/pangolin/resources</code> List resources POST <code>/api/pangolin/setup</code> Create site + all resources (rate limited: \u2157min) POST <code>/api/pangolin/sync</code> Sync resources (create missing, update changed) PUT <code>/api/pangolin/resource/:id</code> Update resource DELETE <code>/api/pangolin/resource/:id</code> Delete resource GET <code>/api/pangolin/resource/:id/clients</code> Connected clients GET <code>/api/pangolin/certificate/:domainId/:domain</code> Certificate info POST <code>/api/pangolin/certificate/:certId</code> Update certificate"},{"location":"docs/api/#observability","title":"Observability","text":"<p>Prefix: <code>/api/observability</code> \u00b7 Auth: <code>SUPER_ADMIN</code> \u00b7 Rate limited: 20/min</p> Method Path Description GET <code>/api/observability/status</code> Check 7 monitoring services GET <code>/api/observability/metrics-summary</code> Key metrics from Prometheus GET <code>/api/observability/alerts</code> Active alerts from Alertmanager"},{"location":"docs/api/#payments","title":"Payments","text":"<p>Prefix: <code>/api/payments</code></p>"},{"location":"docs/api/#public_5","title":"Public","text":"Method Path Auth Description GET <code>/api/payments/config</code> Stripe publishable key + donation settings GET <code>/api/payments/plans</code> Active subscription plans GET <code>/api/payments/products</code> Active products (<code>?type=</code>) POST <code>/api/payments/subscribe</code> Create subscription checkout POST <code>/api/payments/purchase</code> Optional Product checkout (guest or logged-in) POST <code>/api/payments/donate</code> Donation checkout GET <code>/api/payments/my-subscription</code> Current subscription POST <code>/api/payments/my-subscription/cancel</code> Cancel subscription POST <code>/api/payments/webhook</code> Stripe webhook (raw body)"},{"location":"docs/api/#admin_6","title":"Admin","text":"<p>Auth: <code>SUPER_ADMIN</code></p> Method Path Description GET <code>/api/payments/admin/settings</code> Payment settings (secrets masked) PUT <code>/api/payments/admin/settings</code> Update payment settings POST <code>/api/payments/admin/settings/test-connection</code> Test Stripe connection GET <code>/api/payments/admin/dashboard</code> Subscription + donation statistics GET <code>/api/payments/admin/plans</code> All subscription plans POST <code>/api/payments/admin/plans</code> Create plan PUT <code>/api/payments/admin/plans/:id</code> Update plan DELETE <code>/api/payments/admin/plans/:id</code> Delete plan POST <code>/api/payments/admin/plans/:id/sync-stripe</code> Sync plan to Stripe GET <code>/api/payments/admin/subscriptions</code> All subscriptions with filters POST <code>/api/payments/admin/subscriptions/:id/cancel</code> Cancel subscription GET <code>/api/payments/admin/products</code> All products POST <code>/api/payments/admin/products</code> Create product PUT <code>/api/payments/admin/products/:id</code> Update product DELETE <code>/api/payments/admin/products/:id</code> Delete product POST <code>/api/payments/admin/products/:id/sync-stripe</code> Sync product to Stripe GET <code>/api/payments/admin/orders</code> List orders POST <code>/api/payments/admin/orders/:id/refund</code> Refund order GET <code>/api/payments/admin/donations</code> List donations GET <code>/api/payments/admin/export</code> CSV export of completed orders"},{"location":"docs/api/#media-api-fastify-port-4100","title":"Media API (Fastify \u2014 Port 4100)","text":"<p>The Media API is a separate Fastify server sharing the same PostgreSQL database. It handles all video-related functionality.</p>"},{"location":"docs/api/#health","title":"Health","text":"Method Path Auth Description GET <code>/health</code> Media API health check"},{"location":"docs/api/#videos-admin","title":"Videos (Admin)","text":"<p>Prefix: <code>/api/videos</code> \u00b7 Auth: Admin roles</p>"},{"location":"docs/api/#crud-publishing","title":"CRUD &amp; Publishing","text":"Method Path Description GET <code>/api/videos</code> List videos (<code>?limit=&amp;offset=&amp;search=&amp;orientation=&amp;producers=&amp;isShort=</code>) GET <code>/api/videos/producers</code> Distinct producer list GET <code>/api/videos/health</code> Video count health check GET <code>/api/videos/:id</code> Single video detail PATCH <code>/api/videos/:id</code> Update metadata (title, producer, tags, quality, etc.) POST <code>/api/videos/:id/publish</code> Publish to category POST <code>/api/videos/:id/unpublish</code> Unpublish POST <code>/api/videos/bulk-publish</code> Bulk publish POST <code>/api/videos/bulk-unpublish</code> Bulk unpublish POST <code>/api/videos/:id/lock</code> Lock published video POST <code>/api/videos/:id/unlock</code> Unlock video POST <code>/api/videos/:id/generate-thumbnail</code> Generate thumbnail via FFmpeg POST <code>/api/videos/bulk-generate-thumbnails</code> Bulk thumbnail generation"},{"location":"docs/api/#upload","title":"Upload","text":"Method Path Description POST <code>/api/videos/upload</code> Single video upload (multipart, 10 GB limit, streams to disk) POST <code>/api/videos/upload/batch</code> Batch upload (returns 207 multi-status)"},{"location":"docs/api/#actions","title":"Actions","text":"Method Path Description POST <code>/api/videos/:id/duplicate</code> Duplicate video record POST <code>/api/videos/:id/replace</code> Replace video file, keep metadata GET <code>/api/videos/:id/analytics</code> Detailed analytics (<code>?startDate=&amp;endDate=</code>) POST <code>/api/videos/:id/reset-analytics</code> Reset all analytics GET <code>/api/videos/:id/preview-link</code> Generate 24-hour JWT preview link GET <code>/api/videos/analytics/top</code> Top videos (<code>?metric=views|watchTime&amp;limit=</code>) GET <code>/api/videos/analytics/overview</code> Global analytics overview"},{"location":"docs/api/#scheduling","title":"Scheduling","text":"Method Path Description POST <code>/api/videos/:id/schedule-publish</code> Schedule future publish (<code>{publishAt, timezone?}</code>) POST <code>/api/videos/:id/schedule-unpublish</code> Schedule future unpublish DELETE <code>/api/videos/:id/schedule/:action</code> Cancel scheduled operation GET <code>/api/videos/schedules/upcoming</code> Upcoming scheduled operations GET <code>/api/videos/:id/schedule-history</code> Schedule history for video GET <code>/api/videos/schedules/stats</code> Schedule queue statistics POST <code>/api/videos/schedules/pause</code> Pause schedule queue POST <code>/api/videos/schedules/resume</code> Resume schedule queue POST <code>/api/videos/schedules/cleanup</code> Clean old completed jobs"},{"location":"docs/api/#video-fetch","title":"Video Fetch","text":"Method Path Description POST <code>/api/videos/fetch</code> Submit fetch job (<code>{urls: string[]}</code>, 1\u201320 URLs) GET <code>/api/videos/fetch/jobs</code> List recent fetch jobs GET <code>/api/videos/fetch/jobs/:jobId</code> Job detail + log GET <code>/api/videos/fetch/jobs/:jobId/log</code> SSE log stream (Redis pub/sub) DELETE <code>/api/videos/fetch/jobs/:jobId</code> Cancel fetch job"},{"location":"docs/api/#streaming-public","title":"Streaming (Public)","text":"<p>Prefix: <code>/api/videos</code></p> Method Path Auth Description GET <code>/api/videos/stream/health</code> Streaming health check GET <code>/api/videos/:id/stream</code> Optional HTTP range-supporting video stream GET <code>/api/videos/:id/thumbnail</code> Optional Serve thumbnail image GET <code>/api/videos/:id/metadata</code> Public video metadata for embedding <p>Note</p> <p>Admins can stream unpublished videos by providing a valid JWT.</p>"},{"location":"docs/api/#public-gallery","title":"Public Gallery","text":"<p>Prefix: <code>/api/public</code></p> Method Path Auth Description GET <code>/api/public</code> Optional Published videos (<code>?limit=&amp;offset=&amp;search=&amp;sort=recent|popular|oldest&amp;category=</code>) GET <code>/api/public/categories</code> Optional Categories with video counts GET <code>/api/public/producers</code> Optional Published producers GET <code>/api/public/:id</code> Optional Single published video GET <code>/api/public/:id/thumbnail</code> Optional Published thumbnail GET <code>/api/public/:id/stream</code> Optional Published video stream"},{"location":"docs/api/#tracking","title":"Tracking","text":"<p>Prefix: <code>/api/track</code> \u00b7 Auth: None required</p> Method Path Description GET <code>/api/track/health</code> Tracking health check POST <code>/api/track/view</code> Record video view (returns <code>{viewId}</code>) POST <code>/api/track/event</code> Record play/pause/seek/complete event POST <code>/api/track/heartbeat</code> Update watch time (10s interval, <code>sendBeacon</code>) POST <code>/api/track/batch</code> Batch up to 50 tracking events Tracking is GDPR-compliant <p>IP addresses are hashed with a daily-rotating salt. Raw IPs are never stored. Tracking data is retained for 90 days.</p>"},{"location":"docs/api/#reactions","title":"Reactions","text":"<p>Prefix: <code>/api/reactions</code></p> Method Path Auth Description GET <code>/api/reactions/config</code> Available reaction types + emoji mappings GET <code>/api/reactions</code> List reactions (<code>?mediaId=&amp;userId=&amp;limit=</code>) GET <code>/api/reactions/:mediaId/chat</code> Reactions in chat timeline format POST <code>/api/reactions</code> Add reaction (30s cooldown per type) <p>Available types: <code>like</code>, <code>love</code>, <code>laugh</code>, <code>wow</code>, <code>sad</code>, <code>angry</code></p>"},{"location":"docs/api/#comments-chat","title":"Comments &amp; Chat","text":""},{"location":"docs/api/#public-comments","title":"Public Comments","text":"Method Path Auth Description GET <code>/api/public/:id/comments</code> List comments (<code>?limit=&amp;offset=</code>) POST <code>/api/public/:id/comments</code> Optional Create comment (word-filtered; rate limited: 5/min) GET <code>/api/public/:id/chat-stream</code> SSE stream for real-time chat (30s keepalive)"},{"location":"docs/api/#comment-admin","title":"Comment Admin","text":"<p>Prefix: <code>/api/media/admin/comments</code> \u00b7 Auth: Admin roles</p> Method Path Description GET <code>/api/media/admin/comments/stats</code> Counts by status GET <code>/api/media/admin/comments</code> All comments with filters PATCH <code>/api/media/admin/comments/:id/approve</code> Approve comment PATCH <code>/api/media/admin/comments/:id/hide</code> Hide comment PATCH <code>/api/media/admin/comments/:id/unhide</code> Unhide comment PUT <code>/api/media/admin/comments/:id/notes</code> Update moderation notes DELETE <code>/api/media/admin/comments/:id</code> Delete comment"},{"location":"docs/api/#word-filters","title":"Word Filters","text":"<p>Prefix: <code>/api/media/admin/word-filters</code> \u00b7 Auth: Admin roles</p> Method Path Description GET <code>/api/media/admin/word-filters</code> List filter entries grouped by level POST <code>/api/media/admin/word-filters</code> Add word (<code>{word, level: low|medium|high|custom}</code>) DELETE <code>/api/media/admin/word-filters/:id</code> Remove word"},{"location":"docs/api/#chat-threads-notifications","title":"Chat Threads &amp; Notifications","text":"<p>Auth: Authenticated</p> Method Path Description GET <code>/api/media/chat/threads</code> Videos with user's comments + unread counts POST <code>/api/media/chat/threads/:mediaId/read</code> Mark thread as read GET <code>/api/media/notifications/stream</code> Per-user SSE notification stream (<code>?token=</code>)"},{"location":"docs/api/#shorts","title":"Shorts","text":"Method Path Auth Description GET <code>/api/shorts</code> Optional Shorts feed (<code>?sort=recent|popular|random</code>) POST <code>/api/shorts/scan</code> Admin Auto-classify short videos by duration"},{"location":"docs/api/#upvotes","title":"Upvotes","text":"Method Path Auth Description POST <code>/api/public/:id/upvote</code> Toggle upvote (session-based via <code>X-Session-ID</code> header) GET <code>/api/public/:id/upvote-status</code> Check upvote status for current session"},{"location":"docs/api/#playlists","title":"Playlists","text":""},{"location":"docs/api/#public_6","title":"Public","text":"<p>Prefix: <code>/api/playlists</code></p> Method Path Auth Description GET <code>/api/playlists/featured</code> Optional Featured playlists GET <code>/api/playlists/popular</code> Optional Popular public playlists (<code>?search=</code>) GET <code>/api/playlists/share/:token</code> Optional Playlist by share token GET <code>/api/playlists/:id</code> Optional Playlist detail (public, owner, or share token) POST <code>/api/playlists/:id/view</code> Optional Record playlist view"},{"location":"docs/api/#user-playlists","title":"User Playlists","text":"<p>Auth: Authenticated</p> Method Path Description GET <code>/api/playlists/my</code> Own playlists POST <code>/api/playlists</code> Create playlist PUT <code>/api/playlists/:id</code> Update playlist (ownership check) DELETE <code>/api/playlists/:id</code> Delete playlist POST <code>/api/playlists/:id/videos</code> Add video (<code>{mediaId}</code>) DELETE <code>/api/playlists/:id/videos/:mediaId</code> Remove video PUT <code>/api/playlists/:id/videos/reorder</code> Reorder videos POST <code>/api/playlists/:id/share</code> Generate share token DELETE <code>/api/playlists/:id/share</code> Revoke share token"},{"location":"docs/api/#playlist-admin","title":"Playlist Admin","text":"<p>Prefix: <code>/api/media/playlists</code> \u00b7 Auth: Admin roles</p> Method Path Description GET <code>/api/media/playlists</code> All playlists GET <code>/api/media/playlists/featured</code> Featured playlists with admin info POST <code>/api/media/playlists/:id/feature</code> Feature a playlist DELETE <code>/api/media/playlists/:id/feature</code> Unfeature a playlist PUT <code>/api/media/playlists/featured/reorder</code> Reorder featured playlists PUT <code>/api/media/playlists/:id</code> Admin update any playlist POST <code>/api/media/playlists/:id/duplicate</code> Duplicate playlist DELETE <code>/api/media/playlists/:id</code> Admin delete any playlist"},{"location":"docs/api/#user-profile","title":"User Profile","text":"<p>Prefix: <code>/api/media/me</code> \u00b7 Auth: Authenticated</p> Method Path Description GET <code>/api/media/me/stats</code> User stats + 30-day activity + achievements GET <code>/api/media/me/watch-history</code> Paginated watch history POST <code>/api/media/me/stats/recalculate</code> Recompute stats from raw data GET <code>/api/media/me/settings</code> Privacy settings PUT <code>/api/media/me/settings</code> Update privacy settings PUT <code>/api/media/me/profile</code> Update display name PUT <code>/api/media/me/password</code> 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":"<p>Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database.</p> <p>Under Construction</p> <p>Detailed architecture documentation is being written. Check back soon.</p>"},{"location":"docs/architecture/#system-overview","title":"System Overview","text":"<pre><code>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</code></pre>"},{"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":"<ul> <li>JWT access tokens (15 min) + refresh tokens (7 days)</li> <li>Refresh token rotation with atomic database transaction</li> <li>Role-based access control (5 roles)</li> <li>Rate limiting on auth endpoints (10/min per IP)</li> </ul>"},{"location":"docs/deployment/","title":"Deployment","text":"<p>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.</p>"},{"location":"docs/deployment/#architecture-overview","title":"Architecture Overview","text":"<p>Regardless of which exposure method you choose, the internal architecture is the same:</p> <pre><code>Internet \u2192 [Your exposure method] \u2192 Nginx (port 80) \u2192 Backend Services\n</code></pre> <p>Nginx handles all subdomain routing internally. Every service is accessed through nginx on port 80, which proxies to the correct container based on the <code>Host</code> header.</p> Subdomain Service Container Port <code>app.DOMAIN</code> Admin GUI + public pages 3000 <code>api.DOMAIN</code> Express API 4000 <code>media.DOMAIN</code> Fastify Media API 4100 <code>DOMAIN</code> (root) MkDocs documentation site 4001 <code>db.DOMAIN</code> NocoDB 8091 <code>docs.DOMAIN</code> MkDocs live preview 4003 <code>code.DOMAIN</code> Code Server 8888 <code>git.DOMAIN</code> Gitea 3030 <code>n8n.DOMAIN</code> Workflow automation 5678 <code>home.DOMAIN</code> Homepage dashboard 3010 <code>listmonk.DOMAIN</code> Newsletter manager 9001 <code>mail.DOMAIN</code> MailHog (dev email) 8025 <code>qr.DOMAIN</code> Mini QR generator 8089 <code>draw.DOMAIN</code> Excalidraw whiteboard 8090 <code>grafana.DOMAIN</code> Monitoring dashboards 3001"},{"location":"docs/deployment/#exposure-methods","title":"Exposure Methods","text":""},{"location":"docs/deployment/#pangolin","title":"Option 1: Pangolin + Newt Tunnel (Recommended)","text":"<p>Admin GUI: Tunnel Management Page</p> <p>The admin dashboard includes a dedicated Tunnel Management page at Admin \u2192 Settings \u2192 Tunnel. This page provides:</p> <ul> <li>Live status of the Pangolin connection and Newt container health</li> <li>Step-by-step setup instructions if credentials aren't configured yet</li> <li>Full resource table listing every service, its domain, and target \u2014 useful as a reference when creating resources in the Pangolin dashboard</li> <li>API-based site creation as an alternative to the Pangolin dashboard UI</li> <li>Restart Newt button for quick container restarts without the terminal</li> </ul> <p>If you're unsure about any step above, the Tunnel page walks you through the same process interactively.</p> <p>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.</p> <p>Advantages:</p> <ul> <li>No port forwarding needed on your router/firewall</li> <li>Works behind CGNAT, double NAT, or restrictive networks</li> <li>SSL/TLS handled by the Pangolin server</li> <li>Self-hosted \u2014 you control the tunnel infrastructure</li> <li>Built-in access control (optional per-resource authentication)</li> </ul> <p>Requirements:</p> <ul> <li>A Pangolin server (self-hosted on a VPS with a public IP)</li> <li>A domain with DNS pointing to the Pangolin server</li> <li>Pangolin API key and organization ID</li> </ul>"},{"location":"docs/deployment/#step-1-configure-pangolin-credentials","title":"Step 1: Configure Pangolin Credentials","text":"<p>If you used <code>config.sh</code>, you may have already set these. Otherwise, add to your <code>.env</code>:</p> <pre><code>PANGOLIN_API_URL=https://api.your-pangolin-server.org/v1\nPANGOLIN_API_KEY=your_api_key_here\nPANGOLIN_ORG_ID=your_org_id\n</code></pre>"},{"location":"docs/deployment/#step-2-create-a-site-in-pangolin","title":"Step 2: Create a Site in Pangolin","text":"<p>Log in to your Pangolin dashboard and create a new site:</p> <ol> <li>Navigate to Sites \u2192 Create New Site</li> <li>Choose type: Newt</li> <li>Enter a name (e.g., <code>changemaker-yourdomain.org</code>)</li> <li>Choose a subnet (e.g., <code>100.90.128.3/24</code>)</li> <li>Select an exit node (if applicable)</li> <li>Click Create Site</li> <li>Copy the credentials \u2014 you'll need the Site ID, Newt ID, and Newt Secret</li> </ol> <p>Save the credentials</p> <p>The Newt Secret is only shown once during site creation. Copy it immediately.</p>"},{"location":"docs/deployment/#step-3-update-env-with-site-credentials","title":"Step 3: Update <code>.env</code> with Site Credentials","text":"<pre><code>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</code></pre>"},{"location":"docs/deployment/#step-4-start-the-newt-container","title":"Step 4: Start the Newt Container","text":"<pre><code>docker compose up -d newt\n</code></pre> <p>The Newt container connects to nginx (its only dependency) and establishes the tunnel:</p> <pre><code># 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</code></pre> <p>Verify the connection:</p> <pre><code>docker compose logs newt --tail 20\n</code></pre> <p>You should see a successful connection message.</p>"},{"location":"docs/deployment/#step-5-create-public-http-resources","title":"Step 5: Create Public HTTP Resources","text":"<p>In the Pangolin dashboard, create an HTTP resource for each service you want exposed. All resources point to <code>nginx:80</code> \u2014 nginx handles the routing internally.</p> <p>Required resources (minimum for a working deployment):</p> Resource Name Domain Target Auth Admin GUI <code>app.yourdomain.org</code> <code>nginx:80</code> Not Protected API Server <code>api.yourdomain.org</code> <code>nginx:80</code> Not Protected Public Site <code>yourdomain.org</code> <code>nginx:80</code> Not Protected <p>Optional resources (add as needed):</p> Resource Name Domain Target Media API <code>media.yourdomain.org</code> <code>nginx:80</code> NocoDB <code>db.yourdomain.org</code> <code>nginx:80</code> Documentation <code>docs.yourdomain.org</code> <code>nginx:80</code> Code Server <code>code.yourdomain.org</code> <code>nginx:80</code> Gitea <code>git.yourdomain.org</code> <code>nginx:80</code> Grafana <code>grafana.yourdomain.org</code> <code>nginx:80</code> <p>Set resources to Not Protected</p> <p>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.</p>"},{"location":"docs/deployment/#step-6-update-cors-for-production","title":"Step 6: Update CORS for Production","text":"<p>Add your production domain to <code>CORS_ORIGINS</code> in <code>.env</code>:</p> <pre><code>CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000,http://localhost\n</code></pre> <p>Then restart the API:</p> <pre><code>docker compose restart api\n</code></pre>"},{"location":"docs/deployment/#step-7-verify","title":"Step 7: Verify","text":"<pre><code># 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</code></pre>"},{"location":"docs/deployment/#cloudflare","title":"Option 2: Cloudflare Tunnel","text":"<p>Cloudflare Tunnel (<code>cloudflared</code>) provides a similar zero-trust tunnel approach using Cloudflare's network. No port forwarding needed, and you get Cloudflare's CDN and DDoS protection.</p> <p>Advantages:</p> <ul> <li>Free tier available</li> <li>Built-in CDN and DDoS protection</li> <li>No port forwarding needed</li> <li>Managed SSL certificates</li> </ul> <p>Disadvantages:</p> <ul> <li>Proprietary service (not self-hosted)</li> <li>Cloudflare sees all traffic (no end-to-end encryption to your origin)</li> <li>Subject to Cloudflare's Terms of Service</li> </ul>"},{"location":"docs/deployment/#setup","title":"Setup","text":"<ol> <li> <p>Create a Cloudflare Tunnel in the Zero Trust dashboard</p> </li> <li> <p>Add a <code>cloudflared</code> service to your <code>docker-compose.yml</code>:</p> <pre><code>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</code></pre> </li> <li> <p>Add your tunnel token to <code>.env</code>:</p> <pre><code>CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here\n</code></pre> </li> <li> <p>Configure public hostnames in the Cloudflare dashboard, all pointing to <code>http://nginx:80</code>:</p> Hostname Service <code>app.yourdomain.org</code> <code>http://nginx:80</code> <code>api.yourdomain.org</code> <code>http://nginx:80</code> <code>yourdomain.org</code> <code>http://nginx:80</code> (add more as needed) <code>http://nginx:80</code> </li> <li> <p>Start the tunnel:</p> <pre><code>docker compose up -d cloudflared\n</code></pre> </li> </ol> <p>Note</p> <p>The <code>cloudflared</code> service is not included in the default <code>docker-compose.yml</code>. Add it manually if you choose this method. The Newt service can be removed or left stopped.</p>"},{"location":"docs/deployment/#direct","title":"Option 3: Direct DNS + Reverse Proxy","text":"<p>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.</p> <p>Advantages:</p> <ul> <li>No tunnel overhead or third-party dependency</li> <li>Full control over the network path</li> <li>Lowest latency</li> </ul> <p>Disadvantages:</p> <ul> <li>Requires a public IP and open ports (80, 443)</li> <li>You manage SSL certificates yourself</li> <li>Server IP is exposed</li> </ul>"},{"location":"docs/deployment/#setup_1","title":"Setup","text":"<ol> <li> <p>Point DNS for your domain and all subdomains to your server's IP:</p> <pre><code>A yourdomain.org \u2192 YOUR_SERVER_IP\nA *.yourdomain.org \u2192 YOUR_SERVER_IP\n</code></pre> <p>Or use individual A records for each subdomain if your DNS provider doesn't support wildcards.</p> </li> <li> <p>Open ports 80 and 443 on your server's firewall.</p> </li> <li> <p>Install Certbot (or another ACME client) for SSL certificates:</p> <pre><code># 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</code></pre> <p>Alternatively, use the Certbot Docker image or a Let's Encrypt companion container.</p> </li> <li> <p>Update nginx to listen on 443 with your certificates. Add an SSL server block to <code>nginx/conf.d/ssl.conf</code>:</p> <pre><code>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</code></pre> </li> <li> <p>Mount certificates into the nginx container via <code>docker-compose.yml</code>:</p> <pre><code>nginx:\n volumes:\n - /etc/letsencrypt/live/yourdomain.org:/etc/nginx/ssl:ro\n</code></pre> </li> <li> <p>Set up auto-renewal with a cron job or systemd timer:</p> <pre><code>0 3 * * * certbot renew --quiet &amp;&amp; docker compose restart nginx\n</code></pre> </li> </ol> <p>Traefik alternative</p> <p>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.</p>"},{"location":"docs/deployment/#tailscale","title":"Option 4: Tailscale / WireGuard (Private Access)","text":"<p>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.</p> <p>Use cases:</p> <ul> <li>Internal team deployments</li> <li>Development/staging servers</li> <li>Access from mobile devices without public exposure</li> </ul>"},{"location":"docs/deployment/#tailscale-setup","title":"Tailscale Setup","text":"<ol> <li>Install Tailscale on your server and client devices</li> <li>Access services via Tailscale IP (e.g., <code>http://100.x.x.x:3000</code>)</li> <li>Optionally use Tailscale Funnel to selectively expose specific services publicly</li> </ol>"},{"location":"docs/deployment/#wireguard-setup","title":"WireGuard Setup","text":"<ol> <li>Set up a WireGuard server on your host</li> <li>Connect client devices via WireGuard config</li> <li>Access services via the WireGuard interface IP</li> </ol> <p>Note</p> <p>With private access methods, you may not need subdomain routing at all. Access services directly by port: <code>http://server-ip:3000</code> (admin), <code>http://server-ip:4000</code> (API), etc.</p>"},{"location":"docs/deployment/#production-checklist","title":"Production Checklist","text":"<p>Before going live, verify each item:</p>"},{"location":"docs/deployment/#security","title":"Security","text":"<ul> <li> All placeholder passwords changed (<code>grep -c \"REQUIRED_STRONG\" .env</code> should return <code>0</code>)</li> <li> <code>NODE_ENV=production</code> set in <code>.env</code></li> <li> <code>ENCRYPTION_KEY</code> set and differs from JWT secrets</li> <li> <code>EMAIL_TEST_MODE=false</code> (unless you want MailHog in production)</li> <li> <code>CORS_ORIGINS</code> includes your production domain</li> <li> Admin password changed after first login</li> <li> Redis password set (<code>REDIS_PASSWORD</code>)</li> </ul>"},{"location":"docs/deployment/#networking","title":"Networking","text":"<ul> <li> DNS records configured for your domain and subdomains</li> <li> SSL/TLS working (tunnel handles this, or manual certs)</li> <li> All Pangolin resources set to \"Not Protected\" (if using Pangolin)</li> <li> <code>curl https://api.yourdomain.org/api/health</code> returns JSON</li> </ul>"},{"location":"docs/deployment/#services","title":"Services","text":"<ul> <li> Core services running: <code>docker compose ps</code> shows <code>api</code>, <code>admin</code>, <code>v2-postgres</code>, <code>redis</code>, <code>nginx</code> healthy</li> <li> Database migrated: <code>docker compose exec api npx prisma migrate deploy</code></li> <li> Database seeded: <code>docker compose exec api npx prisma db seed</code></li> <li> Admin GUI accessible at <code>https://app.yourdomain.org</code></li> </ul>"},{"location":"docs/deployment/#backups","title":"Backups","text":"<ul> <li> Backup script tested: <code>./scripts/backup.sh</code></li> <li> Backup cron job configured (see Backups below)</li> <li> Restore procedure tested at least once</li> </ul>"},{"location":"docs/deployment/#monitoring-optional","title":"Monitoring (Optional)","text":"<ul> <li> Monitoring stack started: <code>docker compose --profile monitoring up -d</code></li> <li> Grafana accessible and dashboards loading</li> <li> Alert rules configured in Alertmanager</li> </ul>"},{"location":"docs/deployment/#backups_1","title":"Backups","text":"<p>The included backup script dumps PostgreSQL databases, archives uploads, and optionally uploads to S3.</p>"},{"location":"docs/deployment/#running-a-backup","title":"Running a Backup","text":"<pre><code>./scripts/backup.sh\n</code></pre> <p>This creates a timestamped directory under <code>./backups/</code> containing:</p> <ul> <li><code>changemaker_v2.sql.gz</code> \u2014 Main PostgreSQL dump (compressed)</li> <li><code>listmonk.sql.gz</code> \u2014 Listmonk database dump (if running)</li> <li><code>uploads.tar.gz</code> \u2014 Media uploads archive</li> <li><code>manifest.json</code> \u2014 Backup metadata</li> </ul>"},{"location":"docs/deployment/#options","title":"Options","text":"<pre><code># 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</code></pre>"},{"location":"docs/deployment/#automated-backups","title":"Automated Backups","text":"<p>Add a cron job for daily backups:</p> <pre><code># Edit crontab\ncrontab -e\n\n# Add daily backup at 3 AM\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh &gt;&gt; /var/log/changemaker-backup.log 2&gt;&amp;1\n\n# With S3 upload\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh --s3 &gt;&gt; /var/log/changemaker-backup.log 2&gt;&amp;1\n</code></pre>"},{"location":"docs/deployment/#restore","title":"Restore","text":"<pre><code># 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</code></pre>"},{"location":"docs/deployment/#monitoring","title":"Monitoring","text":"<p>The monitoring stack runs behind a Docker Compose profile and is not started by default.</p>"},{"location":"docs/deployment/#starting-the-monitoring-stack","title":"Starting the Monitoring Stack","text":"<pre><code>docker compose --profile monitoring up -d\n</code></pre> <p>This starts:</p> 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":"<p>Grafana includes 3 auto-provisioned dashboards:</p> <ol> <li>API Overview \u2014 HTTP request rates, latency, error rates, active sessions</li> <li>Infrastructure \u2014 Container CPU/memory, PostgreSQL connections, Redis memory</li> <li>Campaign Activity \u2014 Email queue size, campaign sends, response submissions</li> </ol>"},{"location":"docs/deployment/#custom-metrics","title":"Custom Metrics","text":"<p>The API exposes 12 custom Prometheus metrics with the <code>cm_</code> prefix:</p> <ul> <li><code>cm_api_uptime_seconds</code> \u2014 API uptime</li> <li><code>cm_email_queue_size</code> \u2014 BullMQ pending emails</li> <li><code>cm_active_canvass_sessions</code> \u2014 Active canvassing sessions</li> <li><code>cm_locations_total</code> \u2014 Total locations in database</li> <li>And more \u2014 see <code>api/src/utils/metrics.ts</code></li> </ul>"},{"location":"docs/deployment/#alert-rules","title":"Alert Rules","text":"<p>Pre-configured alerts in <code>configs/prometheus/alerts.yml</code>:</p> <ul> <li>API down for more than 5 minutes</li> <li>High error rate (&gt;5% of requests returning 5xx)</li> <li>Database connection failures</li> <li>Redis connection failures</li> <li>Email queue backlog</li> <li>Disk space warnings</li> </ul>"},{"location":"docs/deployment/#upgrading","title":"Upgrading","text":""},{"location":"docs/deployment/#pulling-updates","title":"Pulling Updates","text":"<pre><code># 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</code></pre>"},{"location":"docs/deployment/#database-migrations","title":"Database Migrations","text":"<p>Always run migrations after pulling updates:</p> <pre><code>docker compose exec api npx prisma migrate deploy\n</code></pre> <p>Back up first</p> <p>Always run <code>./scripts/backup.sh</code> before applying migrations in production. Migrations may alter table structures and are not easily reversible.</p>"},{"location":"docs/deployment/#troubleshooting-production-issues","title":"Troubleshooting Production Issues","text":""},{"location":"docs/deployment/#pangolin-302-redirects-instead-of-content","title":"Pangolin: 302 Redirects Instead of Content","text":"<p>Symptom: API returns 302 redirects to the Pangolin authentication page.</p> <p>Fix: In the Pangolin dashboard, edit each resource and set Authentication to Not Protected.</p>"},{"location":"docs/deployment/#cors-errors","title":"CORS Errors","text":"<p>Symptom: Browser console shows CORS errors when accessing the production domain.</p> <p>Fix: Add your production <code>app.</code> subdomain to <code>CORS_ORIGINS</code> in <code>.env</code>, then <code>docker compose restart api</code>.</p>"},{"location":"docs/deployment/#newt-wont-connect","title":"Newt Won't Connect","text":"<p>Check in order:</p> <ol> <li>Credentials: Verify <code>PANGOLIN_NEWT_ID</code> and <code>PANGOLIN_NEWT_SECRET</code> in <code>.env</code></li> <li>Endpoint: Confirm <code>PANGOLIN_ENDPOINT</code> matches your Pangolin server URL</li> <li>Logs: <code>docker compose logs newt --tail 50</code></li> <li>Nginx running: Newt depends on nginx \u2014 <code>docker compose ps nginx</code></li> <li>Network: Ensure outbound HTTPS is not blocked by your firewall</li> </ol>"},{"location":"docs/deployment/#services-unreachable-via-tunnel","title":"Services Unreachable via Tunnel","text":"<ol> <li>Verify nginx is running: <code>docker compose ps nginx</code></li> <li>Test locally first: <code>curl http://localhost:4000/api/health</code></li> <li>Check nginx logs: <code>docker compose logs nginx --tail 50</code></li> <li>Verify DNS: <code>dig app.yourdomain.org</code> should point to your Pangolin server</li> </ol> <p>See Troubleshooting for more common issues.</p>"},{"location":"docs/features/","title":"Feature Guides","text":"<p>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.</p>"},{"location":"docs/features/#advocacy-campaigns","title":"Advocacy Campaigns","text":"<p>Help supporters contact their elected representatives through email campaigns.</p>"},{"location":"docs/features/#how-it-works","title":"How It Works","text":"<ol> <li>An admin creates a campaign \u2014 writes the email subject and body, selects which government levels to target (federal, provincial, municipal, school board), and publishes it.</li> <li>A supporter visits the campaign page \u2014 enters their postal code to look up their representatives.</li> <li>The supporter sends the email \u2014 either directly through the platform (\"Send Now\") or by opening it in their own email app (Gmail, Outlook, etc.).</li> <li>Responses get tracked \u2014 supporters and admins can share representative responses on the Response Wall, with upvoting and moderation.</li> </ol>"},{"location":"docs/features/#key-features","title":"Key Features","text":"<ul> <li>Postal code lookup \u2014 powered by the Represent API, returns representatives at all government levels</li> <li>Two send methods \u2014 server-sent SMTP (tracked) or mailto link (opens user's email app)</li> <li>Email editing \u2014 optionally let supporters personalize the email before sending</li> <li>Response Wall \u2014 public wall where people share how their representatives responded, with moderation and verification</li> <li>Campaign stats \u2014 track emails sent, responses received, and upvotes</li> <li>Featured campaigns \u2014 highlight important campaigns on the public listing page</li> </ul>"},{"location":"docs/features/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/campaigns</code> \u2014 create, edit, and manage campaigns</li> <li><code>/app/responses</code> \u2014 moderate submitted responses</li> <li><code>/app/email-queue</code> \u2014 monitor outgoing email delivery</li> </ul>"},{"location":"docs/features/#public-routes","title":"Public Routes","text":"<ul> <li><code>/campaigns</code> \u2014 browse active campaigns</li> <li><code>/campaign/:slug</code> \u2014 take action on a specific campaign</li> <li><code>/campaign/:slug/responses</code> \u2014 view the response wall</li> </ul>"},{"location":"docs/features/#map-canvassing","title":"Map &amp; Canvassing","text":"<p>Manage locations, organize canvassing territories, and coordinate volunteer door-to-door outreach.</p>"},{"location":"docs/features/#locations","title":"Locations","text":"<p>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.</p>"},{"location":"docs/features/#areas-cuts","title":"Areas (Cuts)","text":"<p>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.</p>"},{"location":"docs/features/#shifts","title":"Shifts","text":"<p>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.</p>"},{"location":"docs/features/#canvassing","title":"Canvassing","text":"<p>The volunteer canvass map is a full-screen GPS-tracked experience:</p> <ul> <li>Walking routes \u2014 the system generates an efficient route through assigned locations</li> <li>Visit recording \u2014 tap a marker, record the outcome (not home, supportive, opposed, etc.)</li> <li>GPS tracking \u2014 real-time position shown on the map</li> <li>Cluster markers \u2014 addresses grouped intelligently for performance at scale</li> </ul>"},{"location":"docs/features/#admin-routes_1","title":"Admin Routes","text":"<ul> <li><code>/app/map</code> \u2014 manage locations (CRUD, CSV import/export, geocoding)</li> <li><code>/app/map/cuts</code> \u2014 draw and manage canvassing areas</li> <li><code>/app/map/shifts</code> \u2014 schedule shifts and view signups</li> <li><code>/app/map/canvass</code> \u2014 canvass dashboard (stats, activity feed, leaderboard)</li> </ul>"},{"location":"docs/features/#public-routes_1","title":"Public Routes","text":"<ul> <li><code>/map</code> \u2014 public map view (address locations only, no canvass data)</li> <li><code>/shifts</code> \u2014 sign up for volunteer shifts</li> </ul>"},{"location":"docs/features/#volunteer-routes","title":"Volunteer Routes","text":"<ul> <li><code>/volunteer</code> \u2014 full-screen canvass map with GPS and visit recording</li> <li><code>/volunteer/shifts</code> \u2014 view assigned shifts</li> <li><code>/volunteer/activity</code> \u2014 visit history and outcome breakdown</li> <li><code>/volunteer/routes</code> \u2014 past canvassing routes</li> </ul>"},{"location":"docs/features/#media-manager","title":"Media Manager","text":"<p>Upload, organize, and share campaign videos with built-in analytics.</p>"},{"location":"docs/features/#key-features_1","title":"Key Features","text":"<ul> <li>Video upload \u2014 drag-and-drop upload with automatic metadata extraction (duration, dimensions, quality)</li> <li>Public gallery \u2014 shareable video gallery at <code>/gallery</code> with category browsing</li> <li>Analytics \u2014 view counts, watch time, completion rates, and traffic sources (GDPR-compliant)</li> <li>Scheduled publishing \u2014 automate publish/unpublish with timezone support and a calendar view</li> <li>Curated playlists \u2014 organize videos into shareable collections</li> <li>Quick actions \u2014 keyboard shortcuts (E/P/A/S) for edit, preview, analytics, schedule</li> </ul>"},{"location":"docs/features/#admin-routes_2","title":"Admin Routes","text":"<ul> <li><code>/app/media/library</code> \u2014 upload and manage videos</li> <li><code>/app/media/curated</code> \u2014 manage playlists</li> <li><code>/app/media/jobs</code> \u2014 monitor processing jobs</li> </ul>"},{"location":"docs/features/#public-routes_2","title":"Public Routes","text":"<ul> <li><code>/gallery</code> \u2014 browse the public video gallery</li> <li><code>/gallery/watch/:id</code> \u2014 watch a specific video</li> </ul>"},{"location":"docs/features/#landing-pages","title":"Landing Pages","text":"<p>Build campaign microsites with a drag-and-drop visual editor.</p>"},{"location":"docs/features/#how-it-works_1","title":"How It Works","text":"<ol> <li>Create a new page from the admin panel</li> <li>Open the GrapesJS visual editor \u2014 drag blocks, edit text, adjust styles</li> <li>Save and publish \u2014 the page goes live at <code>/p/:slug</code></li> <li>Optionally export to MkDocs for inclusion in the documentation site</li> </ol>"},{"location":"docs/features/#admin-routes_3","title":"Admin Routes","text":"<ul> <li><code>/app/pages</code> \u2014 list and manage landing pages</li> <li><code>/app/pages/:id/edit</code> \u2014 full-screen GrapesJS editor</li> </ul>"},{"location":"docs/features/#public-routes_3","title":"Public Routes","text":"<ul> <li><code>/p/:slug</code> \u2014 view a published landing page</li> </ul>"},{"location":"docs/features/#newsletter-listmonk","title":"Newsletter (Listmonk)","text":"<p>Integrated with Listmonk for opt-in mailing lists and newsletter campaigns.</p>"},{"location":"docs/features/#sync","title":"Sync","text":"<p>When enabled (<code>LISTMONK_SYNC_ENABLED=true</code>), the platform syncs shift participants, location contacts, and user accounts to Listmonk subscriber lists. Sync is triggered manually from the admin panel.</p>"},{"location":"docs/features/#admin-routes_4","title":"Admin Routes","text":"<ul> <li><code>/app/listmonk</code> (sidebar: \"Newsletter\") \u2014 sync status, subscriber counts, and manual sync trigger</li> </ul>"},{"location":"docs/features/#email-templates","title":"Email Templates","text":"<p>Create reusable email templates with variable substitution for campaign communications.</p>"},{"location":"docs/features/#admin-routes_5","title":"Admin Routes","text":"<ul> <li><code>/app/email-templates</code> \u2014 create and manage email templates with a visual editor</li> </ul>"},{"location":"docs/getting-started/","title":"Getting Started","text":"<p>This guide walks you through installing Changemaker Lite, running your first deployment, and logging into the admin dashboard.</p>"},{"location":"docs/getting-started/#prerequisites","title":"Prerequisites","text":"<ul> <li>Docker 24+ and Docker Compose v2</li> <li>OpenSSL (for secret generation)</li> <li>A Linux server (Ubuntu 22.04+ recommended) or macOS for development</li> <li>At least 2 GB RAM and 10 GB disk space</li> <li>A domain name (optional, but recommended for production)</li> </ul>"},{"location":"docs/getting-started/#installation","title":"Installation","text":""},{"location":"docs/getting-started/#1-clone-the-repository","title":"1. Clone the Repository","text":"<pre><code>git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n</code></pre>"},{"location":"docs/getting-started/#2-run-the-configuration-wizard","title":"2. Run the Configuration Wizard","text":"<p>The fastest way to get a working <code>.env</code> file is the interactive configuration wizard:</p> <pre><code>bash config.sh\n</code></pre> <p>The wizard walks you through each step:</p> 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 Generates <code>configs/homepage/services.yaml</code> with all service links for your domain Permissions Creates required directories and sets container-friendly permissions <p>After completion you'll have a fully populated <code>.env</code> with no placeholder passwords remaining.</p> <p>Already have a <code>.env</code>?</p> <p>If a <code>.env</code> file exists, the wizard offers to back it up before creating a fresh one, or update values in place.</p> What the wizard looks like <pre><code> \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</code></pre>"},{"location":"docs/getting-started/#3-manual-setup-alternative","title":"3. Manual Setup (Alternative)","text":"<p>If you prefer to configure things by hand:</p> <pre><code>cp .env.example .env\n</code></pre> <p>Then edit <code>.env</code> and at minimum set these values:</p> <pre><code>V2_POSTGRES_PASSWORD=&lt;strong password&gt;\nREDIS_PASSWORD=&lt;strong password&gt;\nJWT_ACCESS_SECRET=&lt;openssl rand -hex 32&gt;\nJWT_REFRESH_SECRET=&lt;openssl rand -hex 32&gt;\nENCRYPTION_KEY=&lt;openssl rand -hex 32&gt;\nINITIAL_ADMIN_PASSWORD=&lt;12+ chars, mixed case + digit&gt;\n</code></pre> <p>See Environment Variables for every available option.</p>"},{"location":"docs/getting-started/#4-start-services","title":"4. Start Services","text":"<pre><code># 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</code></pre>"},{"location":"docs/getting-started/#5-log-in","title":"5. Log In","text":"<p>Open http://localhost:3000 and sign in with the admin email and password you configured.</p> <p>Change your password</p> <p>If you used the wizard's generated password, change it immediately from the admin dashboard.</p>"},{"location":"docs/getting-started/#optional-services","title":"Optional Services","text":"<p>Once the core is running, add more services as needed:</p> <pre><code># 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</code></pre>"},{"location":"docs/getting-started/#next-steps","title":"Next Steps","text":"<ul> <li>Environment Variables \u2014 complete <code>.env</code> reference with every configurable option</li> <li>Feature Guides \u2014 explore campaigns, map, and media</li> <li>Deployment \u2014 production setup with SSL and tunneling</li> <li>Architecture \u2014 understand the system design</li> </ul>"},{"location":"docs/getting-started/environment-variables/","title":"Environment Variables","text":"<p>Changemaker Lite uses a single <code>.env</code> file at the project root to configure all services. Copy the example file to get started:</p> <pre><code>cp .env.example .env\n</code></pre> <p>Security Essentials</p> <ul> <li>Change every <code>REQUIRED_STRONG_PASSWORD_CHANGE_THIS</code> value before starting services</li> <li>Generate secrets with <code>openssl rand -hex 32</code> (or <code>-hex 16</code> where noted)</li> <li>Never commit <code>.env</code> to version control</li> <li>Use unique values for each secret \u2014 do not reuse JWT secrets as encryption keys</li> </ul>"},{"location":"docs/getting-started/environment-variables/#quick-reference","title":"Quick Reference","text":"<p>Variables are grouped by service. Each table marks whether a variable is required for a basic deployment or optional (has a sensible default or only needed for specific features).</p> 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 Description <code>NODE_ENV</code> <code>development</code> Set to <code>production</code> for production deployments. Controls logging, error detail, and security checks. <code>DOMAIN</code> <code>cmlite.org</code> Root domain. Used for nginx subdomain routing (<code>app.DOMAIN</code>, <code>api.DOMAIN</code>, etc.). The root domain serves the MkDocs documentation site; all application routes live under <code>app.DOMAIN</code>. <code>USER_ID</code> <code>1000</code> UID for container file ownership. Match your host user's UID (<code>id -u</code>). <code>GROUP_ID</code> <code>1000</code> GID for container file ownership. Match your host user's GID (<code>id -g</code>). <code>DOCKER_GROUP_ID</code> <code>984</code> GID of the <code>docker</code> group on the host. Needed for containers that access the Docker socket. Find with <code>getent group docker</code>."},{"location":"docs/getting-started/environment-variables/#postgresql-main-database","title":"PostgreSQL (Main Database)","text":"<p>The primary database for both the Express API and the Fastify Media API (shared).</p> Variable Default Description <code>V2_POSTGRES_USER</code> <code>changemaker</code> Database username. <code>V2_POSTGRES_PASSWORD</code> \u2014 Must change. Database password. <code>V2_POSTGRES_DB</code> <code>changemaker_v2</code> Database name. <code>V2_POSTGRES_PORT</code> <code>5433</code> Host port mapping. The container listens on <code>5432</code> internally. <p>Connection string</p> <p>The <code>DATABASE_URL</code> is constructed automatically inside Docker. If running locally, set: <pre><code>DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2\n</code></pre></p>"},{"location":"docs/getting-started/environment-variables/#jwt-authentication","title":"JWT Authentication","text":"Variable Default Description <code>JWT_ACCESS_SECRET</code> \u2014 Secret for signing access tokens. Generate with <code>openssl rand -hex 32</code>. <code>JWT_REFRESH_SECRET</code> \u2014 Secret for signing refresh tokens. Must differ from the access secret. <code>JWT_ACCESS_EXPIRY</code> <code>15m</code> Access token lifetime. Short-lived by design. <code>JWT_REFRESH_EXPIRY</code> <code>7d</code> 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 <code>ENCRYPTION_KEY</code> \u2014 AES key for encrypting secrets stored in the database (SMTP passwords, API keys, etc.). Generate with <code>openssl rand -hex 32</code>. Must not reuse a JWT secret. Required in production (<code>NODE_ENV=production</code>)."},{"location":"docs/getting-started/environment-variables/#initial-admin-account","title":"Initial Admin Account","text":"<p>These credentials create the first super-admin user during database seeding (<code>npx prisma db seed</code>).</p> Variable Default Description <code>INITIAL_ADMIN_EMAIL</code> <code>admin@cmlite.org</code> Email address for the initial admin. <code>INITIAL_ADMIN_PASSWORD</code> \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 <code>API_PORT</code> <code>4000</code> Host port for the Express API. <code>API_URL</code> <code>http://localhost:4000</code> Public URL of the API. Used for generating links in emails and QR codes. <code>CORS_ORIGINS</code> <code>http://localhost:3000,http://localhost</code> Comma-separated list of allowed CORS origins. Add your production domain (e.g., <code>https://app.yourdomain.org</code>) for production. <p>Production CORS</p> <p>If you deploy behind a tunnel (Pangolin, Cloudflare) and API requests fail with CORS errors, add your production <code>app.</code> subdomain here: <pre><code>CORS_ORIGINS=https://app.betteredmonton.org,http://localhost:3000,http://localhost\n</code></pre> Then restart the API: <code>docker compose restart api</code></p>"},{"location":"docs/getting-started/environment-variables/#admin-gui","title":"Admin GUI","text":"Variable Default Description <code>ADMIN_PORT</code> <code>3000</code> Host port for the React admin dashboard. <code>ADMIN_URL</code> <code>http://localhost:3000</code> Public URL of the admin GUI."},{"location":"docs/getting-started/environment-variables/#nginx-reverse-proxy","title":"Nginx Reverse Proxy","text":"Variable Default Description <code>NGINX_HTTP_PORT</code> <code>80</code> HTTP port. All subdomains route through nginx. <code>NGINX_HTTPS_PORT</code> <code>443</code> HTTPS port. SSL is typically handled by the tunnel provider (Pangolin/Cloudflare)."},{"location":"docs/getting-started/environment-variables/#redis","title":"Redis","text":"<p>Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.</p> Variable Default Description <code>REDIS_PASSWORD</code> \u2014 Must change. Redis requires authentication. <code>REDIS_URL</code> <code>redis://:${REDIS_PASSWORD}@redis-changemaker:6379</code> Full connection URL. Uses the password variable automatically."},{"location":"docs/getting-started/environment-variables/#email-smtp","title":"Email / SMTP","text":"Variable Default Description <code>SMTP_HOST</code> <code>mailhog-changemaker</code> SMTP server. Default points to the MailHog dev container. <code>SMTP_PORT</code> <code>1025</code> SMTP port. <code>1025</code> for MailHog, <code>587</code> for most production SMTP. <code>SMTP_USER</code> (empty) SMTP username. Not needed for MailHog. <code>SMTP_PASS</code> (empty) SMTP password. <code>SMTP_FROM</code> <code>noreply@cmlite.org</code> \"From\" address on outgoing emails. <code>SMTP_FROM_NAME</code> <code>Changemaker Lite</code> Display name for the \"From\" header. <code>EMAIL_TEST_MODE</code> <code>true</code> When <code>true</code>, all emails go to MailHog instead of real SMTP. Set to <code>false</code> in production. <code>TEST_EMAIL_RECIPIENT</code> <code>admin@cmlite.org</code> Catch-all recipient when test mode is on. <p>Development email</p> <p>With <code>EMAIL_TEST_MODE=true</code>, all outgoing email is captured in MailHog at <code>http://localhost:8025</code>. No real emails are sent.</p>"},{"location":"docs/getting-started/environment-variables/#listmonk-newsletters","title":"Listmonk (Newsletters)","text":"<p>Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.</p> Variable Default Description <code>LISTMONK_PORT</code> <code>9001</code> Listmonk web UI port. <code>LISTMONK_DB_PORT</code> <code>5432</code> Listmonk's own PostgreSQL port (separate from the main DB). <code>LISTMONK_DB_USER</code> <code>listmonk</code> Listmonk database user. <code>LISTMONK_DB_PASSWORD</code> \u2014 Listmonk database password. <code>LISTMONK_DB_NAME</code> <code>listmonk</code> Listmonk database name. <code>LISTMONK_WEB_ADMIN_USER</code> <code>admin</code> Login for the Listmonk web dashboard. <code>LISTMONK_WEB_ADMIN_PASSWORD</code> \u2014 Password for the Listmonk web dashboard. <code>LISTMONK_API_USER</code> <code>v2-api</code> API user for programmatic access (auto-created by init container). <code>LISTMONK_API_TOKEN</code> \u2014 Token for API user. Generate with <code>openssl rand -hex 16</code>. <code>LISTMONK_ADMIN_USER</code> <code>v2-api</code> Same as <code>LISTMONK_API_USER</code> (used by the sync service). <code>LISTMONK_ADMIN_PASSWORD</code> \u2014 Same as <code>LISTMONK_API_TOKEN</code>. <code>LISTMONK_SYNC_ENABLED</code> <code>false</code> Set to <code>true</code> to sync participants/locations/users to Listmonk lists. <code>LISTMONK_PROXY_PORT</code> <code>9002</code> Nginx proxy port for Listmonk. Listmonk SMTP settings <p>Listmonk has its own SMTP configuration, separate from the main platform's:</p> Variable Default Description <code>LISTMONK_SMTP_HOST</code> <code>mailhog-changemaker</code> SMTP host for Listmonk. <code>LISTMONK_SMTP_PORT</code> <code>1025</code> SMTP port. <code>LISTMONK_SMTP_USER</code> (empty) SMTP username. <code>LISTMONK_SMTP_PASSWORD</code> (empty) SMTP password. <code>LISTMONK_SMTP_TLS_TYPE</code> <code>none</code> TLS mode: <code>none</code>, <code>STARTTLS</code>, or <code>TLS</code>. <code>LISTMONK_SMTP_FROM</code> <code>Changemaker Lite &lt;noreply@cmlite.org&gt;</code> 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 <code>REPRESENT_API_URL</code> <code>https://represent.opennorth.ca</code> 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":"<p>Read-only database browser. Useful for inspecting data without SQL.</p> Variable Default Description <code>NOCODB_V2_PORT</code> / <code>NOCODB_PORT</code> <code>8091</code> Host port for the NocoDB web UI. <code>NOCODB_URL</code> <code>http://changemaker-v2-nocodb:8080</code> Internal Docker URL. <code>NC_ADMIN_EMAIL</code> <code>admin@cmlite.org</code> NocoDB admin email. <code>NC_ADMIN_PASSWORD</code> \u2014 NocoDB admin password."},{"location":"docs/getting-started/environment-variables/#media-manager","title":"Media Manager","text":"<p>Video library with upload, analytics, scheduling, and a public gallery.</p> Variable Default Description <code>ENABLE_MEDIA_FEATURES</code> <code>false</code> Set to <code>true</code> to enable the media system. <code>MEDIA_API_PORT</code> <code>4100</code> Fastify media API port. <code>MEDIA_API_PUBLIC_URL</code> <code>http://media-api:4100</code> Internal URL for the media API container. <code>MEDIA_ROOT</code> <code>/media/library</code> Path to the video library inside the container. <code>MEDIA_UPLOADS</code> <code>/media/uploads</code> Path for upload processing. <code>MAX_UPLOAD_SIZE_GB</code> <code>10</code> Maximum single-file upload size in gigabytes. <code>VIDEO_PLAYER_DEBUG</code> <code>false</code> Enable verbose video player logging. Analytics &amp; scheduling settings Variable Default Description <code>VIDEO_ANALYTICS_RETENTION_DAYS</code> <code>90</code> Days to retain analytics data. GDPR-compliant with IP hashing. <code>VIDEO_ANALYTICS_IP_HASHING_ENABLED</code> <code>true</code> Hash viewer IPs for privacy. <code>VIDEO_SCHEDULE_DEFAULT_TIMEZONE</code> <code>UTC</code> Default timezone for scheduled publishing. <code>VIDEO_SCHEDULE_NOTIFICATION_ENABLED</code> <code>true</code> Notify on scheduled publish/unpublish. <code>VIDEO_PREVIEW_LINK_EXPIRY_HOURS</code> <code>24</code> Preview link JWT expiry (hours)."},{"location":"docs/getting-started/environment-variables/#gitea-git-hosting","title":"Gitea (Git Hosting)","text":"<p>Self-hosted Git repository. Optional service.</p> Variable Default Description <code>GITEA_PORT</code> / <code>GITEA_WEB_PORT</code> <code>3030</code> Gitea web UI port. <code>GITEA_SSH_PORT</code> <code>2222</code> Gitea SSH port for git operations. <code>GITEA_DB_TYPE</code> <code>mysql</code> Database type (Gitea uses its own MySQL). <code>GITEA_DB_HOST</code> <code>gitea-db:3306</code> Internal database host. <code>GITEA_DB_NAME</code> <code>gitea</code> Database name. <code>GITEA_DB_USER</code> <code>gitea</code> Database user. <code>GITEA_DB_PASSWD</code> \u2014 Gitea database password. <code>GITEA_DB_ROOT_PASSWORD</code> \u2014 MySQL root password for Gitea. <code>GITEA_ROOT_URL</code> <code>https://git.cmlite.org</code> Public-facing URL for Gitea. <code>GITEA_DOMAIN</code> <code>git.cmlite.org</code> Domain used in git clone URLs."},{"location":"docs/getting-started/environment-variables/#n8n-workflow-automation","title":"n8n (Workflow Automation)","text":"Variable Default Description <code>N8N_PORT</code> <code>5678</code> n8n web UI port. <code>N8N_HOST</code> <code>n8n.cmlite.org</code> Public hostname for n8n. <code>N8N_ENCRYPTION_KEY</code> \u2014 Encryption key for n8n credentials storage. <code>N8N_USER_EMAIL</code> <code>admin@example.com</code> Initial n8n admin email. <code>N8N_USER_PASSWORD</code> \u2014 Initial n8n admin password. <code>GENERIC_TIMEZONE</code> <code>UTC</code> Timezone for n8n cron triggers."},{"location":"docs/getting-started/environment-variables/#mkdocs-documentation","title":"MkDocs (Documentation)","text":"Variable Default Description <code>MKDOCS_PORT</code> <code>4003</code> MkDocs dev server port (live preview). <code>MKDOCS_SITE_SERVER_PORT</code> <code>4001</code> MkDocs static site server port. <code>BASE_DOMAIN</code> <code>https://cmlite.org</code> Base URL for generated documentation links. <code>MKDOCS_PREVIEW_URL</code> <code>http://mkdocs:8000</code> Internal container URL. <code>MKDOCS_DOCS_PATH</code> <code>/mkdocs/docs</code> 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>CODE_SERVER_PORT</code> <code>8888</code> Code Server web UI port. <code>CODE_SERVER_URL</code> <code>http://code-server:8080</code> Internal container URL. <code>USER_NAME</code> <code>coder</code> User account inside the Code Server container."},{"location":"docs/getting-started/environment-variables/#homepage-service-dashboard","title":"Homepage (Service Dashboard)","text":"Variable Default Description <code>HOMEPAGE_PORT</code> <code>3010</code> Homepage web UI port. <code>HOMEPAGE_EMBED_PORT</code> <code>8887</code> Port for iframe embedding in admin. <code>HOMEPAGE_VAR_BASE_URL</code> <code>http://localhost</code> 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 <code>MINI_QR_PORT</code> <code>8089</code> Mini QR direct access port. <code>MINI_QR_URL</code> <code>http://mini-qr:8080</code> Internal container URL. <code>MINI_QR_EMBED_PORT</code> <code>8885</code> Port for iframe embedding (walk sheets, cut exports)."},{"location":"docs/getting-started/environment-variables/#excalidraw-whiteboard","title":"Excalidraw (Whiteboard)","text":"Variable Default Description <code>EXCALIDRAW_PORT</code> <code>8090</code> Excalidraw web UI port. <code>EXCALIDRAW_URL</code> <code>http://excalidraw-changemaker:80</code> Internal container URL. <code>EXCALIDRAW_EMBED_PORT</code> <code>8886</code> Port for iframe embedding. <code>EXCALIDRAW_WS_URL</code> <code>wss://draw.cmlite.org</code> WebSocket URL for real-time collaboration."},{"location":"docs/getting-started/environment-variables/#mailhog-development-email","title":"MailHog (Development Email)","text":"Variable Default Description <code>MAILHOG_SMTP_PORT</code> <code>1025</code> SMTP port for capturing emails. <code>MAILHOG_WEB_PORT</code> <code>8025</code> Web UI to view captured emails."},{"location":"docs/getting-started/environment-variables/#nar-national-address-register","title":"NAR (National Address Register)","text":"<p>Canadian address data import for geographic canvassing.</p> Variable Default Description <code>NAR_DATA_DIR</code> <code>/data</code> Path to extracted NAR data inside the container. Expects <code>YYYYMM/Addresses/</code> and <code>YYYYMM/Locations/</code> subdirectories. Mount via <code>./data:/data:ro</code> in Docker Compose. <p>Download NAR data from Statistics Canada.</p>"},{"location":"docs/getting-started/environment-variables/#geocoding","title":"Geocoding","text":"<p>Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.</p> Variable Default Description <code>MAPBOX_API_KEY</code> (empty) Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. Sign up. <code>GEOCODING_RATE_LIMIT_MS</code> <code>1100</code> Delay between requests to free providers (ms). Respects rate limits. <code>GEOCODING_CACHE_ENABLED</code> <code>true</code> Enable Redis-backed geocoding cache. <code>GEOCODING_CACHE_TTL_HOURS</code> <code>24</code> Cache lifetime in hours. <code>GOOGLE_MAPS_API_KEY</code> (empty) Google Maps API key. Most accurate but $0.005/request after free tier. <code>GOOGLE_MAPS_ENABLED</code> <code>false</code> Enable Google Maps as a geocoding provider. <code>GEOCODING_PARALLEL_ENABLED</code> <code>true</code> Enable parallel geocoding for bulk imports (~10x speedup). <code>GEOCODING_BATCH_SIZE</code> <code>10</code> Number of concurrent geocoding requests during bulk operations. <code>BULK_GEOCODE_ENABLED</code> <code>true</code> Enable bulk re-geocoding from the admin UI. <code>BULK_GEOCODE_MAX_BATCH</code> <code>5000</code> Maximum locations per bulk geocoding run."},{"location":"docs/getting-started/environment-variables/#overpass-area-import","title":"Overpass / Area Import","text":"<p>OpenStreetMap data import for map enrichment.</p> Variable Default Description <code>OVERPASS_API_URL</code> <code>https://overpass-api.de/api/interpreter</code> Overpass API endpoint. Use a private instance for heavy usage. <code>OVERPASS_MIN_DELAY_MS</code> <code>30000</code> Minimum delay between requests (ms). The public API requires 30 seconds. <code>AREA_IMPORT_MAX_GRID_POINTS</code> <code>500</code> Maximum reverse-geocode grid points per area import."},{"location":"docs/getting-started/environment-variables/#pangolin-tunnel","title":"Pangolin Tunnel","text":"<p>Expose services to the internet without port forwarding, using a self-hosted Pangolin instance.</p> Variable Default Description <code>PANGOLIN_API_URL</code> <code>https://api.bnkserve.org/v1</code> Pangolin server API endpoint. <code>PANGOLIN_API_KEY</code> (empty) API key for Pangolin management. <code>PANGOLIN_ORG_ID</code> (empty) Organization ID in Pangolin. <code>PANGOLIN_SITE_ID</code> (empty) Site ID (populated after setup via admin GUI). <code>PANGOLIN_ENDPOINT</code> <code>https://pangolin.bnkserve.org</code> Pangolin tunnel endpoint. <code>PANGOLIN_NEWT_ID</code> (empty) Newt client ID (populated after setup). <code>PANGOLIN_NEWT_SECRET</code> (empty) Newt client secret (populated after setup). <p>Setup flow</p> <p>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.</p>"},{"location":"docs/getting-started/environment-variables/#monitoring","title":"Monitoring","text":"<p>These services are behind the <code>monitoring</code> Docker Compose profile. Start them with:</p> <pre><code>docker compose --profile monitoring up -d\n</code></pre> Variable Default Description <code>PROMETHEUS_PORT</code> <code>9090</code> Prometheus web UI / query port. <code>GRAFANA_PORT</code> <code>3001</code> Grafana dashboard port. <code>GRAFANA_ADMIN_PASSWORD</code> <code>admin</code> Change in production. <code>GRAFANA_ROOT_URL</code> <code>http://localhost:3001</code> Public URL for Grafana (used in links). <code>CADVISOR_PORT</code> <code>8080</code> cAdvisor container metrics port. <code>NODE_EXPORTER_PORT</code> <code>9100</code> Prometheus node exporter port. <code>REDIS_EXPORTER_PORT</code> <code>9121</code> Redis metrics exporter port. <code>ALERTMANAGER_PORT</code> <code>9093</code> Alertmanager web UI port. <code>GOTIFY_PORT</code> <code>8889</code> Gotify push notification port. <code>GOTIFY_ADMIN_USER</code> <code>admin</code> Gotify admin username. <code>GOTIFY_ADMIN_PASSWORD</code> <code>admin</code> Change in production."},{"location":"docs/getting-started/environment-variables/#generating-secrets","title":"Generating Secrets","text":"<p>Use these commands to generate all required secrets at once:</p> <pre><code># 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</code></pre> <p>Tip</p> <p>Copy the output and paste the values into your <code>.env</code> file. The <code>INITIAL_ADMIN_PASSWORD</code> uses base64 encoding to ensure it contains uppercase, lowercase, and digits (meeting the password policy).</p>"},{"location":"docs/getting-started/environment-variables/#minimal-vs-full-deployment","title":"Minimal vs Full Deployment","text":"Minimal (Core Only)Full Stack <p>For a basic deployment with campaigns, map, and admin:</p> Required variables<pre><code>V2_POSTGRES_PASSWORD=...\nREDIS_PASSWORD=...\nJWT_ACCESS_SECRET=...\nJWT_REFRESH_SECRET=...\nENCRYPTION_KEY=...\nINITIAL_ADMIN_PASSWORD=...\n</code></pre> Start services<pre><code>docker compose up -d v2-postgres redis api admin\n</code></pre> <p>For the complete platform including media, newsletters, monitoring, and all services:</p> Additional variables needed<pre><code># 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</code></pre> Start services<pre><code>docker compose up -d\ndocker compose --profile monitoring up -d\n</code></pre>"},{"location":"docs/services/","title":"Services","text":"<p>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.</p>"},{"location":"docs/services/#core-platform","title":"Core Platform","text":"<p>The essential services that power the application.</p> <ul> <li> <p> Express API</p> <p>Main V2 API server. Handles authentication, campaigns, map, shifts, pages, email, and all business logic. Prisma ORM with PostgreSQL.</p> <p>Port: <code>4000</code> \u00b7 Container: <code>changemaker-v2-api</code></p> <p> API Reference</p> </li> <li> <p> Fastify Media API</p> <p>Video library server. Upload, metadata extraction (FFprobe), analytics, scheduled publishing, and public gallery. Shares the same PostgreSQL database.</p> <p>Port: <code>4100</code> \u00b7 Container: <code>changemaker-media-api</code></p> <p> Media Guide</p> </li> <li> <p> Admin GUI</p> <p>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.</p> <p>Port: <code>3000</code> \u00b7 Container: <code>changemaker-v2-admin</code></p> <p> Feature Guides</p> </li> <li> <p> PostgreSQL 16</p> <p>Primary database shared by both APIs. Managed by Prisma migrations. Contains 30+ tables covering users, campaigns, locations, shifts, media, and more.</p> <p>Port: <code>5433</code> (host) / <code>5432</code> (container) \u00b7 Container: <code>changemaker-v2-postgres</code></p> <p> PostgreSQL Docs</p> </li> <li> <p> Redis</p> <p>In-memory store for rate limiting, BullMQ job queues (email, video scheduling), geocoding cache, and session data. Requires authentication.</p> <p>Port: <code>6379</code> \u00b7 Container: <code>redis-changemaker</code></p> <p> Redis Docs</p> </li> <li> <p> Nginx</p> <p>Reverse proxy handling all subdomain routing (<code>app.</code>, <code>api.</code>, <code>media.</code>, <code>docs.</code>, etc.). Includes security headers (HSTS, CSP, Permissions-Policy) and WebSocket support.</p> <p>Port: <code>80</code> / <code>443</code> \u00b7 Container: <code>changemaker-v2-nginx</code></p> <p> Nginx Docs</p> </li> </ul>"},{"location":"docs/services/#communication-email","title":"Communication &amp; Email","text":"<ul> <li> <p> Listmonk</p> <p>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.</p> <p>Port: <code>9001</code> \u00b7 Container: <code>listmonk-app</code> \u00b7 Subdomain: <code>listmonk.DOMAIN</code></p> <p> Listmonk Docs</p> </li> <li> <p> MailHog</p> <p>Email capture for development. All outgoing email is intercepted and displayed in a web UI when <code>EMAIL_TEST_MODE=true</code>. No real emails are sent.</p> <p>Port: <code>8025</code> (web) / <code>1025</code> (SMTP) \u00b7 Container: <code>mailhog-changemaker</code> \u00b7 Subdomain: <code>mail.DOMAIN</code></p> <p> MailHog GitHub</p> </li> </ul>"},{"location":"docs/services/#content-editing","title":"Content &amp; Editing","text":"<ul> <li> <p> MkDocs</p> <p>Material-themed documentation site with full-text search, blog, social cards, and Jinja2 template overrides. Two containers: live preview (dev) and static site (production).</p> <p>Port: <code>4003</code> (dev) / <code>4001</code> (static) \u00b7 Container: <code>mkdocs-changemaker</code> \u00b7 Subdomain: <code>docs.DOMAIN</code></p> <p> MkDocs Material</p> </li> <li> <p> Code Server</p> <p>Full VS Code in the browser. Edit configuration files, templates, and documentation from anywhere without SSH. Supports extensions.</p> <p>Port: <code>8888</code> \u00b7 Container: <code>code-server-changemaker</code> \u00b7 Subdomain: <code>code.DOMAIN</code></p> <p> Code Server Docs</p> </li> </ul>"},{"location":"docs/services/#data-automation","title":"Data &amp; Automation","text":"<ul> <li> <p> NocoDB</p> <p>Airtable-alternative database browser. Provides a spreadsheet-like interface to browse, filter, sort, and export campaign data. Read-only access to the main database.</p> <p>Port: <code>8091</code> \u00b7 Container: <code>changemaker-v2-nocodb</code> \u00b7 Subdomain: <code>db.DOMAIN</code></p> <p> NocoDB Docs</p> </li> <li> <p> n8n</p> <p>Visual workflow automation platform. Connect APIs, trigger actions on events, schedule tasks, and build custom integrations \u2014 all without code. 400+ built-in integrations.</p> <p>Port: <code>5678</code> \u00b7 Container: <code>n8n-changemaker</code> \u00b7 Subdomain: <code>n8n.DOMAIN</code></p> <p> n8n Docs</p> </li> <li> <p> Gitea</p> <p>Self-hosted Git repository hosting. Version control for campaign code, configuration, templates, and documentation. Includes issues, pull requests, and CI/CD.</p> <p>Port: <code>3030</code> (web) / <code>2222</code> (SSH) \u00b7 Container: <code>gitea-changemaker</code> \u00b7 Subdomain: <code>git.DOMAIN</code></p> <p> Gitea Docs</p> </li> </ul>"},{"location":"docs/services/#utilities","title":"Utilities","text":"<ul> <li> <p> Mini QR</p> <p>Lightweight QR code generator. Produces PNG images for walk sheets, campaign materials, and event signage. Embedded in the admin dashboard via iframe.</p> <p>Port: <code>8089</code> \u00b7 Container: <code>mini-qr</code> \u00b7 Subdomain: <code>qr.DOMAIN</code></p> </li> <li> <p> Homepage</p> <p>Service dashboard showing the status of all containers at a glance. Auto-generated <code>services.yaml</code> from <code>config.sh</code> provides both production and local links.</p> <p>Port: <code>3010</code> \u00b7 Container: <code>homepage-changemaker</code> \u00b7 Subdomain: <code>home.DOMAIN</code></p> <p> Homepage Docs</p> </li> <li> <p> Excalidraw</p> <p>Collaborative whiteboard for brainstorming, diagramming, and visual planning. Real-time collaboration via WebSocket.</p> <p>Port: <code>8090</code> \u00b7 Container: <code>excalidraw-changemaker</code> \u00b7 Subdomain: <code>draw.DOMAIN</code></p> <p> Excalidraw</p> </li> </ul>"},{"location":"docs/services/#networking-tunneling","title":"Networking &amp; Tunneling","text":"<ul> <li> <p> Pangolin + Newt</p> <p>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.</p> <p>Container: <code>newt-changemaker</code> \u00b7 Managed from Admin \u2192 Settings \u2192 Tunnel</p> <p> Deployment Guide \u00b7 Pangolin GitHub</p> </li> </ul>"},{"location":"docs/services/#monitoring-stack","title":"Monitoring Stack","text":"<p>These services run behind the <code>monitoring</code> Docker Compose profile. Start them with:</p> <pre><code>docker compose --profile monitoring up -d\n</code></pre> <ul> <li> <p> Prometheus</p> <p>Metrics collection and time-series database. Scrapes 12 custom <code>cm_*</code> application metrics plus container, host, and Redis metrics. Pre-configured alert rules.</p> <p>Port: <code>9090</code> \u00b7 Container: <code>prometheus-changemaker</code></p> <p> Prometheus Docs</p> </li> <li> <p> Grafana</p> <p>Metrics visualization with 3 auto-provisioned dashboards: API Overview, Infrastructure, and Campaign Activity. Supports custom dashboards and alerting.</p> <p>Port: <code>3001</code> \u00b7 Container: <code>grafana-changemaker</code> \u00b7 Subdomain: <code>grafana.DOMAIN</code></p> <p> Grafana Docs</p> </li> <li> <p> Alertmanager</p> <p>Alert routing and notification delivery. Receives alerts from Prometheus and dispatches to Gotify, email, or webhooks based on configurable rules.</p> <p>Port: <code>9093</code> \u00b7 Container: <code>alertmanager-changemaker</code></p> <p> Alertmanager Docs</p> </li> <li> <p> cAdvisor</p> <p>Container resource metrics. Exposes CPU, memory, network, and filesystem usage per container for Prometheus to scrape.</p> <p>Port: <code>8080</code> \u00b7 Container: <code>cadvisor-changemaker</code></p> <p> cAdvisor GitHub</p> </li> <li> <p> Node Exporter</p> <p>Host system metrics. Reports CPU, memory, disk, and network stats for the underlying server.</p> <p>Port: <code>9100</code> \u00b7 Container: <code>node-exporter-changemaker</code></p> <p> Node Exporter</p> </li> <li> <p> Redis Exporter</p> <p>Redis metrics for Prometheus. Exposes connection counts, memory usage, command stats, and keyspace info.</p> <p>Port: <code>9121</code> \u00b7 Container: <code>redis-exporter-changemaker</code></p> <p> Redis Exporter GitHub</p> </li> <li> <p> Gotify</p> <p>Self-hosted push notification server. Receives alerts from Alertmanager and delivers them to mobile/desktop clients.</p> <p>Port: <code>8889</code> \u00b7 Container: <code>gotify-changemaker</code></p> <p> Gotify Docs</p> </li> </ul>"},{"location":"docs/services/#quick-reference","title":"Quick Reference","text":"<p>All services at a glance with their default ports and subdomains.</p> Service Port Subdomain Docker Profile Express API 4000 <code>api.</code> default Media API 4100 <code>media.</code> default Admin GUI 3000 <code>app.</code> default PostgreSQL 5433 \u2014 default Redis 6379 \u2014 default Nginx 80/443 (all) default Listmonk 9001 <code>listmonk.</code> default MailHog 8025 <code>mail.</code> default MkDocs (dev) 4003 <code>docs.</code> default MkDocs (static) 4001 (root) default Code Server 8888 <code>code.</code> default NocoDB 8091 <code>db.</code> default n8n 5678 <code>n8n.</code> default Gitea 3030 <code>git.</code> default Mini QR 8089 <code>qr.</code> default Homepage 3010 <code>home.</code> default Excalidraw 8090 <code>draw.</code> default Newt (tunnel) \u2014 \u2014 default Prometheus 9090 \u2014 <code>monitoring</code> Grafana 3001 <code>grafana.</code> <code>monitoring</code> Alertmanager 9093 \u2014 <code>monitoring</code> cAdvisor 8080 \u2014 <code>monitoring</code> Node Exporter 9100 \u2014 <code>monitoring</code> Redis Exporter 9121 \u2014 <code>monitoring</code> Gotify 8889 \u2014 <code>monitoring</code> <p>Starting services selectively</p> <p>You don't need to run everything. Start only what you need:</p> <pre><code># 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</code></pre> <p>See Getting Started for the recommended startup order.</p>"},{"location":"docs/troubleshooting/","title":"Troubleshooting","text":"<p>Common issues and their solutions when running Changemaker Lite.</p> <p>Under Construction</p> <p>This troubleshooting guide is being expanded. Check back soon for more solutions.</p>"},{"location":"docs/troubleshooting/#cors-errors-in-production","title":"CORS Errors in Production","text":"<p>Symptom: Browser console shows CORS errors when accessing production domain.</p> <p>Fix: Add your production domain to <code>CORS_ORIGINS</code> in <code>.env</code>:</p> <pre><code>CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000\n</code></pre> <p>Then restart the API: <code>docker compose restart api</code></p>"},{"location":"docs/troubleshooting/#pangolin-tunnel-403302-errors","title":"Pangolin Tunnel 403/302 Errors","text":"<p>Symptom: All API endpoints return 302 redirects to Pangolin auth page.</p> <p>Fix: In the Pangolin dashboard, set each resource to \"Not Protected\" (public access).</p>"},{"location":"docs/troubleshooting/#database-connection-failures","title":"Database Connection Failures","text":"<ol> <li>Check PostgreSQL: <code>docker compose ps v2-postgres</code></li> <li>Verify <code>DATABASE_URL</code> in <code>.env</code></li> <li>View logs: <code>docker compose logs v2-postgres --tail 50</code></li> </ol>"},{"location":"docs/troubleshooting/#redis-connection-failures","title":"Redis Connection Failures","text":"<ol> <li>Check Redis: <code>docker compose ps redis-changemaker</code></li> <li>Verify <code>REDIS_PASSWORD</code> and <code>REDIS_URL</code> format in <code>.env</code></li> <li>Test: <code>docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping</code></li> </ol>"},{"location":"docs/troubleshooting/#api-not-starting","title":"API Not Starting","text":"<ol> <li>Check logs: <code>docker compose logs api --tail 100</code></li> <li>Verify all required env vars are set (see <code>.env.example</code>)</li> <li>Run migrations: <code>docker compose exec api npx prisma migrate deploy</code></li> </ol>"},{"location":"docs/volunteer/","title":"Volunteer Guide","text":"<p>Welcome! 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.</p>"},{"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":"<p>Visit the Shifts page (your organizer will share the link, or find it at <code>/shifts</code>). 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.</p>"},{"location":"docs/volunteer/#2-log-in","title":"2. Log In","text":"<p>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.</p>"},{"location":"docs/volunteer/#3-open-the-volunteer-portal","title":"3. Open the Volunteer Portal","text":"<p>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.</p>"},{"location":"docs/volunteer/#the-volunteer-map","title":"The Volunteer Map","text":"<p>The volunteer map is your main tool for canvassing. It shows all the addresses in your assigned area and tracks your position with GPS.</p>"},{"location":"docs/volunteer/#what-you-see","title":"What You See","text":"<ul> <li>Colored markers \u2014 each marker is an address. Colors indicate the outcome of the last visit (green = supportive, red = opposed, grey = not yet visited, etc.)</li> <li>Clusters \u2014 when zoomed out, markers group together and show the number of addresses in that area. Tap a cluster to zoom in.</li> <li>Blue dot \u2014 your current GPS position</li> <li>Walking route \u2014 a suggested path through the addresses (dotted line)</li> </ul>"},{"location":"docs/volunteer/#recording-a-visit","title":"Recording a Visit","text":"<ol> <li>Tap a marker to select an address</li> <li>A bottom panel slides up showing the address details</li> <li>Tap Record Visit to log what happened:<ul> <li>Not Home \u2014 nobody answered</li> <li>Supportive \u2014 positive interaction</li> <li>Opposed \u2014 not supportive</li> <li>Undecided \u2014 hasn't made up their mind</li> <li>Moved \u2014 no longer lives there</li> <li>Refused \u2014 declined to talk</li> </ul> </li> <li>Optionally add a note about the visit</li> <li>Tap Save \u2014 the marker color updates immediately</li> </ol>"},{"location":"docs/volunteer/#tips-for-canvassing","title":"Tips for Canvassing","text":"<ul> <li>Start a session before you begin knocking on doors \u2014 this tracks your route and time</li> <li>End your session when you're done for the day</li> <li>The map works offline for basic viewing, but you need a connection to save visits</li> <li>If GPS is inaccurate, you can manually tap the correct marker on the map</li> </ul>"},{"location":"docs/volunteer/#your-shifts","title":"Your Shifts","text":"<p>Visit Shifts in the bottom navigation to see your upcoming and past shifts. Each shift shows:</p> <ul> <li>Date and time</li> <li>Assigned area (if linked)</li> <li>A button to open the canvass map for that area</li> </ul>"},{"location":"docs/volunteer/#activity-log","title":"Activity Log","text":"<p>The Activity tab shows your complete visit history:</p> <ul> <li>Outcome breakdown \u2014 pie chart of your visit outcomes</li> <li>Visit list \u2014 each visit with address, outcome, time, and any notes</li> <li>Stats \u2014 total visits, addresses covered, and sessions completed</li> </ul>"},{"location":"docs/volunteer/#routes","title":"Routes","text":"<p>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.</p>"},{"location":"docs/volunteer/#browsing-public-pages","title":"Browsing Public Pages","text":"<p>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.</p>"},{"location":"docs/volunteer/#faq","title":"FAQ","text":"<p>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.</p> <p>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.</p> <p>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.</p> <p>Q: How do I sign up for more shifts? A: Visit the public shifts page (ask your organizer for the link, or go to <code>/shifts</code>).</p>"}]}