changemaker.lite/mkdocs/site/search/search_index.json

1 line
390 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-page/","title":"Test Page","text":"<p>Testing</p> <p>testing testing one two</p> <p>hello is this content going to show? </p>"},{"location":"test/","title":"test","text":"<p>Hello! </p> <p>Testing this editing system. </p>"},{"location":"blog/","title":"Blog","text":""},{"location":"blog/2026/03/27/test-blog-post---version-7/","title":"Test Blog Post - Version 7","text":"<p>This version uses the auto-setup token.</p>"},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/","title":"Introducing Changemaker Lite v2","text":"<p>Changemaker Lite v2 is a ground-up rebuild of the platform \u2014 same mission, entirely new architecture. After 14 phases of development, the platform is ready for production use.</p>","tags":["v2","release","self-hosted","FOSS"]},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/#what-changed","title":"What Changed","text":"<p>V1 was two independent Express apps stitched together with NocoDB as a data layer. It worked, but scaling features meant fighting the architecture at every turn.</p> <p>V2 is a unified TypeScript stack:</p> <ul> <li>Dual API architecture \u2014 Express.js for the main platform, Fastify for the media library, sharing a single PostgreSQL 16 database via Prisma ORM</li> <li>React admin GUI \u2014 Vite + Ant Design + Zustand, serving admin, public, and volunteer interfaces from one build</li> <li>30+ Docker services \u2014 from core infrastructure to monitoring, communication, and developer tools</li> <li>JWT authentication with refresh token rotation, role-based access control (11 roles), and a comprehensive security audit</li> </ul>","tags":["v2","release","self-hosted","FOSS"]},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/#whats-new","title":"What's New","text":"<p>The feature set has grown substantially:</p> <ul> <li>Advocacy campaigns with postal code \u2192 representative lookup, email sending, response walls, and moderation</li> <li>Map &amp; canvassing with multi-provider geocoding, polygon territories, GPS-tracked volunteer sessions, and walking route generation</li> <li>Media manager with video upload, FFprobe metadata extraction, scheduled publishing, analytics, and a public gallery</li> <li>Landing page builder powered by GrapesJS with drag-and-drop editing</li> <li>Payments via encrypted Stripe integration \u2014 products, donations, and subscription plans</li> <li>SMS campaigns via a Termux Android bridge</li> <li>Team communication with self-hosted Rocket.Chat and Jitsi Meet</li> <li>People CRM aggregating contacts across all modules with duplicate detection and merge</li> <li>Volunteer social features \u2014 friend system, achievements, leaderboards, and a personal calendar</li> <li>One-command install \u2014 <code>curl | bash</code> pulls a release tarball and runs the config wizard</li> </ul>","tags":["v2","release","self-hosted","FOSS"]},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/#why-self-hosted","title":"Why Self-Hosted","text":"<p>Every subscription to corporate campaign software funds infrastructure you don't control. Your voter lists, canvassing outcomes, and communication patterns become assets on someone else's balance sheet.</p> <p>Changemaker Lite costs roughly the price of a VPS \u2014 often under $50/month for the full stack. But the real value isn't cost savings. It's control. No vendor can cut off your access. No acquisition can change your terms.</p> <p>Read more in our Philosophy page.</p>","tags":["v2","release","self-hosted","FOSS"]},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/#get-started","title":"Get Started","text":"<pre><code>curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash\n</code></pre> <p>Or follow the Getting Started guide for a walkthrough.</p>","tags":["v2","release","self-hosted","FOSS"]},{"location":"blog/2026/03/22/introducing-changemaker-lite-v2/#whats-next","title":"What's Next","text":"<p>Phase 15 (Testing &amp; Polish) is underway. We're also working on:</p> <ul> <li>Social Calendar Phase B (shared views, availability finder)</li> <li>Expanded test coverage</li> <li>Performance optimization for large location datasets</li> </ul> <p>Follow this blog for updates, or subscribe to the newsletter.</p>","tags":["v2","release","self-hosted","FOSS"]},{"location":"comments/callback/","title":"Signing in...","text":"<p>Completing sign in...</p> <p>You will be redirected back to the page you were on.</p>"},{"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>","tags":["guide","getting-started"],"boost":2},{"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>","tags":["guide","getting-started"],"boost":2},{"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</p> </li> </ul>","tags":["guide","getting-started"],"boost":2},{"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</p> </li> </ul>","tags":["guide","getting-started"],"boost":2},{"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>","tags":["guide","getting-started"],"boost":2},{"location":"docs/phil/","title":"Philosophy","text":"","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#software-is-political","title":"Software Is Political","text":"<p>Every tool your movement adopts shapes how you organize. Proprietary platforms reinforce hierarchy \u2014 the vendor decides what features you get, what data you can export, and what happens when you stop paying. Community-controlled tools support democratic autonomy because the people using them decide how they work.</p> <p>If you do politics, who is reading your secrets? Corporate platforms harvest political intelligence systematically. Facebook chat data has been used in criminal prosecutions. Social media platforms are leveraged for political coordination and surveillance. When you organize on corporate infrastructure, you hand your strategies, your voter data, and your movement's internal conversations to entities that may have every reason to work against you.</p> <p>Changemaker Lite exists because we believe organizational independence requires technological independence.</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#the-extractive-model","title":"The Extractive Model","text":"<p>Most campaign and political software is extractive by design. The pattern is familiar:</p> <ol> <li>Free trial hooks you in</li> <li>Paid features gate the tools you actually need</li> <li>Data export becomes difficult or impossible</li> <li>Pricing escalates as you grow and become dependent</li> <li>Your usage patterns are monetized through data partnerships, behavioral analytics, and enterprise contracts</li> </ol> <p>This isn't a side effect \u2014 it's the business model. You pay with money and with data. Your voter lists, canvassing outcomes, donor records, and communication patterns become assets on someone else's balance sheet.</p> <p>Every subscription to corporate software funds the machine you're fighting.</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#the-alternative-grow-power-dont-rent-it","title":"The Alternative: Grow Power, Don't Rent It","text":"<p>Changemaker asks a different question than most political tech: instead of \"how do we extract more data from a community?\" we ask \"what tools are needed to grow change in a community?\"</p> <p>Growing change means:</p> <ul> <li>Making real connections between organizers, volunteers, and community members \u2014 not just collecting their contact info</li> <li>Providing access to the same caliber of tools that well-funded campaigns use \u2014 without the price tag or the surveillance</li> <li>Deeply understanding the wants and needs of your movement \u2014 on infrastructure you control, with data you own</li> </ul>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#distributed-organizing-is-the-way-out","title":"Distributed Organizing Is The Way Out","text":"<p>Socialist movements will never outspend capital. Progressive organizations cannot compete financially with well-funded conservative movements, and chasing big-donor dollars leads to mission drift and organizational capture \u2014 what some call the Political Industrial Complex.</p> <p>A thousand neighborhood mailing lists has more potential impact than any single organization. When organizing knowledge and digital tools are widely distributed \u2014 not gatekept by leadership or locked behind vendor paywalls \u2014 movements become genuinely resilient.</p> <p>The historical pattern is clear: worker victories occurred when organizing knowledge was widely distributed, not concentrated at the top. Changemaker Lite is built on this premise \u2014 provide the tools freely, train people to use them, and get out of the way.</p> <p>Workers, with the right tools, will build the future.</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#de-corp-your-stack","title":"De-Corp Your Stack","text":"<p>The practical work of digital sovereignty starts with replacing corporate services one at a time:</p> Corporate Tool Changemaker Alternative What You Gain Mailchimp Listmonk Unlimited subscribers, no per-send charges, your data stays local NationBuilder Changemaker Lite Full campaign platform without the $50-500/month ransom Google Docs Gitea + Code Server Version control, collaboration, no algorithmic scanning Slack Rocket.Chat Team chat with SSO, no message limits, no corporate eavesdropping SurveyMonkey Response Wall Supporter voices on your terms, with moderation you control Google Maps Self-hosted Leaflet No API fees, no tracking, offline-capable canvassing <p>The cost reduction is dramatic. Organizations spending thousands monthly on SaaS tools can replace them with a single self-hosted server running Changemaker Lite for roughly the cost of hosting \u2014 often under $50/month.</p> <p>But the real value isn't cost savings. It's control. No vendor can cut off your access. No acquisition can change your terms. No government can compel a foreign company to hand over your data. Your movement's digital infrastructure belongs to your movement.</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#security-culture-starts-with-infrastructure","title":"Security Culture Starts With Infrastructure","text":"<p>Security culture isn't just about who knows what \u2014 it's about who can know what. When your communications run through corporate servers, you've made a structural decision about who has access before you've even thought about operational security.</p> <p>Key principles:</p> <ul> <li>Compartmentalization by design \u2014 Self-hosted systems let you control exactly who has access to what, at the infrastructure level</li> <li>No third-party access \u2014 No corporate subpoenas for your data, no partnership agreements sharing your information</li> <li>Audit everything \u2014 When you run the servers, you can verify that your security promises are real, not just marketing</li> <li>Consent and autonomy \u2014 Your community sets its own security boundaries rather than accepting whatever a vendor's privacy policy allows</li> </ul> <p>You wouldn't hold a sensitive strategy meeting in a room wired by someone else. Why would you plan your campaign on someone else's servers?</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#our-principles","title":"Our Principles","text":"<p>Liberation First Technology should center marginalized voices. The tools we build reflect the values we hold, and they shape the movements that use them.</p> <p>Community Over Profit Changemaker Lite is free and open source software, built by a cooperative \u2014 not a startup looking for an exit. There are no shareholders to satisfy, no venture capitalists to answer to. The software serves the community because that's the only thing it's designed to do.</p> <p>Data Sovereignty Communities should own their complete digital infrastructure. Not just the content \u2014 the servers, the databases, the encryption keys, and the ability to pack up and leave at any time.</p> <p>Radical Accessibility Self-hosted doesn't have to mean self-excluding. Changemaker Lite is designed for organizers, not sysadmins. If you can follow a guide to set up a WordPress site, you can run this platform.</p>","tags":["concept","philosophy","FOSS"]},{"location":"docs/phil/#further-reading","title":"Further Reading","text":"<p>These articles explore the ideas behind Changemaker Lite in depth:</p> <ul> <li>If You Do Politics, Who Is Reading Your Secrets? \u2014 Why you should de-corp your software stack</li> <li>Distributed Digital Organizing Is The Way Out \u2014 Why decentralized power structures outperform centralized ones</li> <li>How Not To Get Got Making Content \u2014 Platform independence for political content creators</li> <li>What Is Security Culture? \u2014 The foundations of security culture for movements</li> </ul>","tags":["concept","philosophy","FOSS"]},{"location":"docs/admin/","title":"Admin Guide","text":"<p>The admin panel at <code>/app</code> is your command center for managing the entire platform. Use the sidebar to navigate between modules, or press Ctrl+K to open the command palette for quick access to any page, setting, or action.</p>","tags":["guide","admin"]},{"location":"docs/admin/#sections","title":"Sections","text":"<ul> <li> <p> Dashboard</p> <p>Live overview of platform activity, upcoming shifts, email stats, and service health.</p> </li> <li> <p> People &amp; Access</p> <p>User management, roles, the People CRM, and contact merging.</p> </li> <li> <p> Advocacy</p> <p>Campaigns, response moderation, representative lookup, and email queue monitoring.</p> </li> <li> <p> Broadcast</p> <p>Newsletter sync, email templates, and SMS campaigns.</p> </li> <li> <p> Web Content</p> <p>Landing pages, homepage, navigation menu, and documentation management.</p> </li> <li> <p> Map &amp; Canvassing</p> <p>Locations, areas, shifts, canvassing dashboard, data quality, and map settings.</p> </li> <li> <p> Media</p> <p>Video/photo library, analytics, playlists, comment moderation, and gallery ads.</p> </li> <li> <p> Payments</p> <p>Products, donations, subscription plans, and Stripe configuration.</p> </li> <li> <p> Services</p> <p>Tunnel management, monitoring, and third-party integrations.</p> </li> <li> <p> Settings</p> <p>Organization branding, theme colors, email config, feature toggles, and notifications.</p> </li> </ul>","tags":["guide","admin"]},{"location":"docs/admin/#roles-reference","title":"Roles Reference","text":"Role Access Level <code>SUPER_ADMIN</code> Full platform access \u2014 implicitly bypasses all role checks <code>INFLUENCE_ADMIN</code> Campaigns, responses, representatives, email queue <code>MAP_ADMIN</code> Locations, areas, shifts, canvassing, data quality <code>BROADCAST_ADMIN</code> Newsletter sync, email templates <code>CONTENT_ADMIN</code> Landing pages, homepage, navigation, documentation <code>MEDIA_ADMIN</code> Video library, analytics, gallery, moderation, ads <code>PAYMENTS_ADMIN</code> Products, donations, plans, Stripe configuration <code>EVENTS_ADMIN</code> Gancio event sync and calendar management <code>SOCIAL_ADMIN</code> Social connections, achievements, calendar layers <code>USER</code> Volunteer portal only <code>TEMP</code> Limited volunteer access (auto-created on shift signup)","tags":["guide","admin"]},{"location":"docs/admin/dashboard/","title":"Dashboard","text":"<p>The admin dashboard (<code>/app</code>) provides a real-time overview of platform activity.</p> <p></p>","tags":["guide","admin"]},{"location":"docs/admin/dashboard/#dashboard-cards","title":"Dashboard Cards","text":"<ul> <li>Platform Stats \u2014 active campaigns, total emails sent, registered users, and location count</li> <li>Activity Feed \u2014 recent events across all modules (signups, emails, visits, responses)</li> <li>Upcoming Shifts \u2014 next scheduled volunteer shifts with signup counts</li> <li>Newsletter Stats \u2014 Listmonk subscriber counts and recent campaign performance</li> <li>Chat Activity \u2014 Rocket.Chat channel activity and online users (when chat is enabled)</li> <li>Service Health \u2014 connectivity status for integrated services (Gitea, Gancio, Vaultwarden, etc.)</li> </ul> <p>All cards auto-refresh and gracefully degrade when their associated module is disabled.</p>","tags":["guide","admin"]},{"location":"docs/admin/people-access/","title":"People &amp; Access","text":"<p>Manage platform user accounts and roles, and use the People CRM to get a unified view of every supporter, donor, and volunteer across all modules.</p> <p></p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#user-management","title":"User Management","text":"","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#creating-users","title":"Creating Users","text":"<p>Navigate to Users (<code>/app/users</code>) and click Add User. Fill in name, email, and role. The user will receive a welcome email with login instructions.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#roles","title":"Roles","text":"Role Access Use Case <code>SUPER_ADMIN</code> Full platform access Campaign managers <code>INFLUENCE_ADMIN</code> Campaigns, responses, email queue Advocacy coordinators <code>MAP_ADMIN</code> Locations, areas, shifts, canvassing Field organizers <code>USER</code> Volunteer portal only Active volunteers <code>TEMP</code> Limited volunteer access Shift signups (auto-created)","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#password-policy","title":"Password Policy","text":"<p>Passwords must be at least 12 characters with uppercase, lowercase, and a digit. This is enforced at the API schema level.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#deactivating-users","title":"Deactivating Users","text":"<p>Edit a user from the Users page and toggle their active status. Deactivated users cannot log in but their data is preserved. Banned users have their sessions invalidated immediately.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#service-accounts-panel","title":"Service Accounts Panel","text":"<p>When editing a user, the Service Accounts panel shows provisioning status for each integrated service (Rocket.Chat, Gitea, Vaultwarden, Listmonk). You can provision, deprovision, or re-sync individual services per user.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#people-crm","title":"People CRM","text":"<p>Enable with <code>enablePeople</code> in Settings. The People module serves as the platform's CRM, aggregating data from all other modules into a unified view.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#virtual-aggregation","title":"Virtual Aggregation","text":"<p>The People page does not store a separate \"people\" table. Instead, it aggregates records in real time from seven data sources:</p> Source Data Users Platform accounts (name, email, phone, last login) Address Occupants Named residents from the map/canvassing module Campaign Senders People who sent advocacy emails Shift Signups Volunteer shift registrants SMS Contacts Contacts from SMS campaign lists Donations/Orders Buyers from the payments module Manual Contacts created directly in the CRM <p>Records are deduplicated by normalized email or phone number, with Users taking highest priority.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#managed-contacts","title":"Managed Contacts","text":"<p>Any virtual person can be \"promoted\" to a managed Contact record. This creates a persistent Contact entity in the database with:</p> <ul> <li>Display name, first/last name \u2014 editable independently of the source</li> <li>Tags \u2014 custom CRM tags for segmentation and filtering</li> <li>Notes \u2014 free-text notes field</li> <li>Support level \u2014 LEVEL_1 (Strong) through LEVEL_4 (Opposition)</li> <li>Opt-out flags \u2014 email opt-out, SMS opt-out, and do-not-contact</li> <li>Sign requested \u2014 track yard sign status</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#contact-details","title":"Contact Details","text":"<p>Each managed contact supports multiple structured data entries:</p> <ul> <li>Addresses \u2014 link to map locations with optional unit numbers and primary flag; new addresses can be auto-added to the map for geocoding</li> <li>Emails \u2014 multiple email addresses with labels (e.g., Personal, Work) and primary designation</li> <li>Phones \u2014 multiple phone numbers with labels and primary designation</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#activity-timeline","title":"Activity Timeline","text":"<p>View a chronological timeline of all interactions for a person, across every module:</p> <ul> <li>Advocacy emails sent and responses submitted</li> <li>Shift signups and canvass visits</li> <li>Donations and product purchases</li> <li>SMS messages sent and received</li> <li>Video views</li> <li>Notes added and contact merges</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#duplicate-detection-and-merge","title":"Duplicate Detection and Merge","text":"<p>The platform identifies potential duplicates by matching normalized email addresses and phone numbers across sources. The merge workflow lets you:</p> <ul> <li>Select which fields to keep from the source vs. target contact</li> <li>Merge tags, addresses, emails, and phones</li> <li>Preserve the full audit trail (merged contacts are soft-linked, not deleted)</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#connection-graph","title":"Connection Graph","text":"<p>Build a relationship graph between contacts using typed connections:</p> <ul> <li>Connection types \u2014 Household, Family, Colleague, Referred By, and Custom</li> <li>Bidirectional \u2014 connections can be one-way or mutual</li> <li>Visual graph \u2014 interactive force-directed graph visualization showing contacts as nodes and connections as edges</li> <li>Configurable depth \u2014 explore up to 3 degrees of separation</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#profile-links","title":"Profile Links","text":"<p>Generate shareable public profile pages for contacts:</p> <ul> <li>Unique token URLs at <code>/profile/:token</code></li> <li>Configurable expiration \u2014 24 hours, 7 days, 30 days, 90 days, 1 year, or never</li> <li>Optional password protection \u2014 require a PIN or password to view</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#household-detection","title":"Household Detection","text":"<p>The Household panel groups contacts who share the same physical address, making it easy to see all members of a household and their combined engagement.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#create-user-from-contact","title":"Create User from Contact","text":"<p>Promote a CRM contact to a full platform user account directly from the People interface, with role assignment and optional welcome email.</p>","tags":["guide","admin","CRM"]},{"location":"docs/admin/people-access/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/users</code> \u2014 user CRUD, role assignment, service accounts</li> <li><code>/app/people</code> \u2014 contact list with search, filters, source/tag filtering, and bulk actions</li> </ul>","tags":["guide","admin","CRM"]},{"location":"docs/admin/settings/","title":"Platform Settings","text":"<p>Centralized configuration for organization identity, theming, email delivery, feature modules, and automated notifications.</p> <p></p>","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#settings-tabs","title":"Settings Tabs","text":"","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#organization","title":"Organization","text":"<p>Configure your organization's public identity:</p> <ul> <li>Organization Name \u2014 displayed in the admin sidebar, public pages, and emails</li> <li>Short Name \u2014 shown when the admin sidebar is collapsed (max 10 characters)</li> <li>Logo URL \u2014 displayed on the login page, homepage hero, and public navigation</li> <li>Favicon URL \u2014 browser tab icon</li> <li>Footer Text \u2014 shown in public page footers</li> <li>Login Subtitle \u2014 displayed below the organization name on the login page</li> </ul>","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#theme-colors","title":"Theme Colors","text":"<p>Customize the look of admin and public interfaces:</p> <p>Admin theme:</p> <ul> <li>Primary color (accent color for buttons, links, active states)</li> <li>Background color (page background)</li> </ul> <p>Public theme:</p> <ul> <li>Primary color</li> <li>Background color</li> <li>Container color (card and section backgrounds)</li> <li>Header gradient (CSS gradient string for the public navigation bar)</li> </ul> <p>A live preview panel shows color swatches and a gradient preview as you configure.</p>","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#email","title":"Email","text":"<p>Configure how the platform sends emails:</p> <ul> <li>Sender \u2014 from name and from address for all outgoing emails</li> <li>Active SMTP provider \u2014 toggle between MailHog (testing) and Production SMTP with a single click</li> <li>Production SMTP \u2014 host, port, username, and password (collapsible panel, disabled when MailHog is active)</li> <li>Test mode \u2014 when enabled, all emails redirect to a single test recipient address</li> <li>Test actions \u2014 \"Test Connection\" verifies SMTP connectivity; \"Send Test Email\" delivers a test message through the active provider</li> </ul> <p>A configuration summary card at the top displays the current provider, server, authentication status, and test mode state.</p>","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#feature-toggles","title":"Feature Toggles","text":"<p>Enable or disable platform modules. Disabling a module hides it from navigation but does not delete data.</p> Category Flag Description Core Platform <code>enableInfluence</code> Advocacy campaigns, email sending, response wall <code>enableMap</code> Map, locations, canvassing, volunteer shifts <code>enableNewsletter</code> Listmonk newsletter sync <code>enableLandingPages</code> GrapesJS landing page builder Media &amp; Content <code>enableMediaFeatures</code> Video library, public gallery, analytics <code>enableGalleryAds</code> Promotional cards in the video gallery <code>enableEvents</code> Gancio event calendar integration Communication <code>enableChat</code> Rocket.Chat team coordination <code>enableMeet</code> Jitsi video meetings (integrates with Rocket.Chat) <code>enableSms</code> Termux Android SMS campaigns People &amp; Engagement <code>enablePeople</code> Unified contacts CRM <code>enableSocial</code> Volunteer social connections and activity feeds <code>autoSyncPeopleToMap</code> Auto-create map locations from contact addresses Commerce <code>enablePayments</code> Stripe subscriptions, products, and donations","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#notifications","title":"Notifications","text":"<p>Control which automated email notifications the platform sends. Disabling a notification stops future emails but does not affect already-queued jobs.</p> <p>Admin alerts:</p> <ul> <li>New shift signup</li> <li>Response wall submission</li> <li>Yard sign request (from canvassing)</li> <li>Shift cancellation</li> </ul> <p>Volunteer emails:</p> <ul> <li>Canvass session summary (sent after completing a session)</li> <li>Signup cancellation confirmation</li> <li>24-hour pre-shift reminder</li> <li>Post-shift thank-you (sent 2 hours after shift ends)</li> </ul> <p>Re-engagement:</p> <ul> <li>Re-engagement emails for inactive volunteers</li> <li>Configurable inactivity threshold (days without activity)</li> <li>Configurable cooldown period (minimum days between re-engagement emails)</li> </ul>","tags":["guide","admin","configuration"]},{"location":"docs/admin/settings/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/settings</code> \u2014 multi-tab settings page (supports deep-linking to a specific tab via router state)</li> </ul>","tags":["guide","admin","configuration"]},{"location":"docs/admin/advocacy/","title":"Advocacy","text":"<p>The advocacy module helps supporters contact their elected representatives through email campaigns.</p>","tags":["guide","admin","influence"]},{"location":"docs/admin/advocacy/#in-this-section","title":"In This Section","text":"<ul> <li>Campaigns \u2014 create and publish advocacy campaigns with postal code lookup and response tracking</li> <li>Responses \u2014 moderate the public response wall where supporters share representative replies</li> <li>Representatives \u2014 manage the representative lookup cache powered by the Represent API</li> <li>Email Queue \u2014 monitor outgoing advocacy emails, retry failures, and view delivery stats</li> </ul>","tags":["guide","admin","influence"]},{"location":"docs/admin/advocacy/campaigns/","title":"Advocacy Campaigns","text":"<p>Help supporters contact their elected representatives through email campaigns.</p> <p></p>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#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>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#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>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#user-submitted-campaigns","title":"User-Submitted Campaigns","text":"<p>Registered (non-temporary) users can create their own advocacy campaigns and submit them for admin review.</p> <ul> <li>Public submission route \u2014 users visit <code>/campaigns/create</code> to draft a campaign through a guided wizard</li> <li>3-step wizard \u2014 the submission flow walks users through campaign details (title, description, government levels), email template (subject and body), and a final review step before submitting</li> <li>My campaigns dashboard \u2014 users can view and manage their submitted campaigns at <code>/campaigns/mine</code>, including checking moderation status and editing campaigns that have been sent back for changes</li> <li>Restricted fields \u2014 user-submitted campaigns have limited options compared to admin-created ones (no SMTP sending, no highlight, no custom recipients); only the mailto link fallback is enabled by default</li> <li>Auto-moderation status \u2014 newly submitted campaigns start in <code>PENDING_REVIEW</code> status and remain in <code>DRAFT</code> until an admin approves them</li> <li>Edit restrictions \u2014 users can only edit their own campaigns, and only when the moderation status is <code>PENDING_REVIEW</code> or <code>CHANGES_REQUESTED</code>; editing automatically resets the status back to <code>PENDING_REVIEW</code></li> <li>Rate limiting \u2014 campaign submissions are rate-limited to 5 per hour per IP to prevent abuse</li> <li>XSS protection \u2014 all user-supplied text (title, description, email subject, email body) is HTML-escaped before storage</li> </ul>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#campaign-moderation","title":"Campaign Moderation","text":"<p>Admins review user-submitted campaigns before they go live.</p> <ul> <li>Moderation queue \u2014 accessible at <code>/app/campaign-moderation</code>, showing all user-generated campaigns filtered by moderation status (pending, approved, rejected, changes requested)</li> <li>Moderation actions \u2014 for each campaign in the queue, admins can:<ul> <li>Approve \u2014 sets the moderation status to <code>APPROVED</code> and the campaign status to <code>ACTIVE</code>, making it publicly visible</li> <li>Reject \u2014 marks the campaign as <code>REJECTED</code> with an optional reason visible to the submitter</li> <li>Request changes \u2014 sets the status to <code>CHANGES_REQUESTED</code> with feedback, allowing the user to revise and resubmit</li> </ul> </li> <li>Moderation stats \u2014 the queue page displays counters for total user-generated campaigns, pending reviews, approved, rejected, and changes-requested counts</li> <li>Reviewer tracking \u2014 each moderation action records the reviewer's user ID and timestamp</li> <li>Search and filter \u2014 the moderation queue supports searching by campaign title, submitter name, or email, and filtering by moderation status</li> </ul>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#campaign-analytics","title":"Campaign Analytics","text":"<p>The Campaign Effectiveness dashboard provides cross-campaign performance analytics at <code>/app/influence/effectiveness</code>.</p> <ul> <li>Performance tab \u2014 per-campaign KPIs including total emails sent, email delivery status breakdown, response counts, response rates, and call counts; top campaigns visualized as a horizontal bar chart</li> <li>Representatives tab \u2014 tracks individual representative responsiveness across all campaigns; shows emails received, responses given, verified response count, and response rate per representative; sortable by response count, response rate, or name; includes government level distribution</li> <li>Geography tab \u2014 engagement breakdown by geographic area; group results by postal code, city, or province; enriched with city/province data from the postal code cache</li> <li>Funnel tab \u2014 conversion funnel visualization showing progression from emails sent to unique participants to responses received to verified responses, plus calls made; includes percentage-of-first and stage-to-stage dropoff rates</li> <li>Trends tab \u2014 time-series activity chart showing daily or weekly email and response volumes; default view covers the last 30 days; merged email and response series for side-by-side comparison</li> <li>Global filters \u2014 all tabs share campaign and date range filters; select a specific campaign or view aggregate data across all campaigns</li> </ul>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/campaigns</code> \u2014 create, edit, and manage campaigns</li> <li><code>/app/campaign-moderation</code> \u2014 review and moderate user-submitted campaigns</li> <li><code>/app/influence/effectiveness</code> \u2014 campaign effectiveness analytics dashboard</li> <li><code>/app/responses</code> \u2014 moderate submitted responses</li> <li><code>/app/email-queue</code> \u2014 monitor outgoing email delivery</li> </ul>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/campaigns/#public-routes","title":"Public Routes","text":"<ul> <li><code>/campaigns</code> \u2014 browse active campaigns</li> <li><code>/campaigns/create</code> \u2014 submit a new user-generated campaign (requires login)</li> <li><code>/campaigns/mine</code> \u2014 view and manage your submitted campaigns (requires login)</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>","tags":["guide","admin","influence","campaigns"]},{"location":"docs/admin/advocacy/email-queue/","title":"Email Queue","text":"<p>Monitor outgoing advocacy emails processed through the BullMQ queue.</p> <p></p>","tags":["guide","admin","influence","email"]},{"location":"docs/admin/advocacy/email-queue/#key-features","title":"Key Features","text":"<ul> <li>Queue dashboard \u2014 view pending, active, completed, and failed jobs at <code>/app/email-queue</code></li> <li>Job details \u2014 inspect individual email jobs with recipient, subject, status, and timestamps</li> <li>Retry failed jobs \u2014 re-queue emails that failed due to SMTP errors or timeouts</li> <li>Clear completed \u2014 bulk-remove completed jobs to keep the queue clean</li> <li>Stats \u2014 total sent, delivery rate, and average processing time</li> </ul>","tags":["guide","admin","influence","email"]},{"location":"docs/admin/advocacy/email-queue/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/email-queue</code> \u2014 email queue monitoring and management</li> </ul>","tags":["guide","admin","influence","email"]},{"location":"docs/admin/advocacy/representatives/","title":"Representatives","text":"<p>The platform uses the Represent API to look up elected representatives by postal code across all government levels.</p> <p></p>","tags":["guide","admin","influence"]},{"location":"docs/admin/advocacy/representatives/#how-it-works","title":"How It Works","text":"<ul> <li>Postal code lookup \u2014 enter a Canadian postal code to retrieve federal, provincial, municipal, and school board representatives</li> <li>Redis cache \u2014 lookup results are cached to reduce API calls and improve response times</li> <li>Cache management \u2014 view cache status and clear entries from <code>/app/representatives</code></li> <li>Government levels \u2014 campaigns can target specific levels (federal, provincial, municipal, school board)</li> </ul>","tags":["guide","admin","influence"]},{"location":"docs/admin/advocacy/representatives/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/representatives</code> \u2014 representative cache management and lookup testing</li> </ul>","tags":["guide","admin","influence"]},{"location":"docs/admin/advocacy/responses/","title":"Response Moderation","text":"<p>Review and moderate representative responses submitted by supporters on the public response wall.</p> <p></p>","tags":["guide","admin","influence","moderation"]},{"location":"docs/admin/advocacy/responses/#key-features","title":"Key Features","text":"<ul> <li>Moderation queue \u2014 review submissions at <code>/app/responses</code> with filtering by campaign and status</li> <li>Verification \u2014 mark responses as verified to display a trust badge on the public wall</li> <li>Upvoting \u2014 supporters can upvote responses; counts are visible on the public wall</li> <li>Approve / reject \u2014 control which responses appear publicly</li> <li>Response stats \u2014 track response counts per campaign and per representative</li> </ul>","tags":["guide","admin","influence","moderation"]},{"location":"docs/admin/advocacy/responses/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/responses</code> \u2014 response moderation dashboard</li> </ul>","tags":["guide","admin","influence","moderation"]},{"location":"docs/admin/broadcast/","title":"Broadcast","text":"<p>Reach supporters through multiple channels \u2014 email newsletters, templated campaigns, and SMS text messages.</p>","tags":["guide","admin","broadcast"]},{"location":"docs/admin/broadcast/#in-this-section","title":"In This Section","text":"<ul> <li>Newsletter \u2014 Listmonk integration with automatic subscriber sync from shifts, campaigns, and contacts</li> <li>Email Templates \u2014 reusable templates with variable substitution, version history, and test sending</li> <li>SMS \u2014 text message campaigns via a Termux Android bridge with contact lists and response tracking</li> </ul>","tags":["guide","admin","broadcast"]},{"location":"docs/admin/broadcast/email-templates/","title":"Email Templates","text":"<p>Create reusable email templates with variable substitution for campaign communications. Templates are used by advocacy campaigns, shift confirmations, volunteer re-engagement emails, and other automated communications.</p> <p></p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#template-categories","title":"Template Categories","text":"<p>Each template belongs to a category that determines where it can be used:</p> <ul> <li>INFLUENCE -- advocacy campaign emails sent to representatives</li> <li>MAP -- shift confirmation, volunteer thank-you, and canvassing-related emails</li> <li>SYSTEM -- account verification, password reset, and platform notifications</li> <li>PAYMENT -- donation receipts, subscription confirmations, and purchase orders</li> </ul>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#variable-system","title":"Variable System","text":"<p>Templates use Handlebars-style <code>{{VARIABLE_NAME}}</code> placeholders that are replaced at send time. Variables must use uppercase letters and underscores (e.g., <code>{{RECIPIENT_NAME}}</code>, <code>{{CAMPAIGN_TITLE}}</code>).</p> <ul> <li>Text variables -- simple string substitution for names, dates, URLs, and other text</li> <li>Video variables -- embed a media library video by referencing its ID</li> <li>Conditional blocks -- show or hide content with <code>{{#if VARIABLE}}...{{/if}}</code> syntax</li> <li>Required vs optional -- each variable can be marked as required, with sample values for test emails</li> </ul> <p>The template validator automatically extracts all variables from the HTML, text, and subject line content and checks for unmatched conditional blocks.</p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#version-history","title":"Version History","text":"<p>Every change to a template's subject line, HTML content, or text content creates a new version. The full version history is preserved, and any previous version can be restored:</p> <ul> <li>Version browsing -- view the subject, HTML, and text content of any past version</li> <li>Rollback -- restore a previous version (creates a new version entry, preserving the audit trail)</li> <li>Change notes -- each version includes a description of what changed</li> <li>Author tracking -- versions record which admin made each change</li> </ul>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#test-emails","title":"Test Emails","text":"<p>Before activating a template, send a test email to verify rendering:</p> <ul> <li>Variable substitution -- provide test data for each variable to preview the final output</li> <li>Recipient selection -- send the test to any email address</li> <li>Test log -- all test sends are logged with success/failure status and message IDs</li> <li>Rate limited -- 10 test emails per 15 minutes per user to prevent abuse</li> </ul>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#template-caching","title":"Template Caching","text":"<p>Rendered templates are cached in memory for performance. The cache is automatically cleared when a template is created, updated, or deleted. Admins can also manually clear the cache from the admin interface.</p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/email-templates/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/email-templates</code> -- create and manage email templates with a visual editor</li> </ul>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/newsletter/","title":"Newsletter (Listmonk)","text":"<p>Integrated with Listmonk for opt-in mailing lists and newsletter campaigns. Enable with <code>LISTMONK_SYNC_ENABLED=true</code>.</p> <p></p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/newsletter/#managed-lists","title":"Managed Lists","text":"<p>The platform automatically creates and maintains 13 subscriber lists in Listmonk:</p> List Name Source Tags All Contacts All synced records <code>v2</code> Campaign Participants Users who sent advocacy emails <code>v2</code>, <code>influence</code> Locations - All Address occupants with email <code>v2</code>, <code>map</code> Support Level 1-4 Addresses by canvass support level <code>v2</code>, <code>map</code>, <code>support</code> Has Campaign Sign Addresses with a yard sign <code>v2</code>, <code>map</code>, <code>signs</code> Users Active non-temp platform accounts <code>v2</code>, <code>users</code> Volunteers Shift signups <code>v2</code>, <code>map</code>, <code>shifts</code> Canvassers Users who completed canvass sessions <code>v2</code>, <code>map</code>, <code>canvass</code> Subscribers Active paid subscribers <code>v2</code>, <code>payments</code> Donors Users who completed a donation <code>v2</code>, <code>payments</code>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/newsletter/#bulk-sync","title":"Bulk Sync","text":"<p>The admin panel provides a manual \"Sync All\" action that synchronizes four data sources to Listmonk:</p> <ol> <li>Campaign participants -- distinct email senders from advocacy campaigns</li> <li>Location contacts -- address occupants with email, mapped to support level and sign lists</li> <li>Users -- active platform accounts (excludes TEMP users)</li> <li>CRM tags -- contacts tagged in the People module, synced to tag-linked Listmonk lists</li> </ol> <p>Each source upserts subscribers (creates new or merges into existing), preserving existing list memberships and merging metadata attributes.</p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/newsletter/#event-driven-sync","title":"Event-Driven Sync","text":"<p>In addition to bulk sync, the platform fires real-time subscriber upserts on application events:</p> <ul> <li>Shift signup -- adds to Volunteers list</li> <li>Canvass session completed -- adds to Canvassers list</li> <li>Campaign email sent -- adds to Campaign Participants list</li> <li>Subscription activated -- adds to Subscribers list</li> <li>Donation completed -- adds to Donors list</li> <li>Product purchased -- adds to Donors list</li> <li>Address updated (canvass visit) -- updates support level list membership</li> <li>Re-engagement email sent -- updates Volunteers list metadata</li> <li>CRM tag changed -- adds/removes from tag-linked Listmonk lists</li> </ul> <p>All event-driven syncs are fire-and-forget and silently fail if Listmonk is unreachable.</p>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/newsletter/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/listmonk</code> (sidebar: \"Newsletter\") -- sync status, subscriber counts, campaign stats, and manual sync trigger</li> </ul>","tags":["guide","admin","broadcast","email"]},{"location":"docs/admin/broadcast/sms/","title":"SMS Campaigns","text":"<p>Text message outreach via a Termux Android bridge. Uses a real Android phone to send and receive SMS \u2014 no third-party SMS gateway or Twilio account needed.</p> <p></p> <p>Enable with <code>ENABLE_SMS=true</code> or via the setup wizard.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#architecture-overview","title":"Architecture Overview","text":"<p>The SMS system uses a three-tier architecture where your server communicates with a lightweight Python Flask API running on an Android phone:</p> <pre><code>graph LR\n A[Admin Dashboard&lt;br/&gt;Campaign UI] --&gt;|API calls| B[Express API&lt;br/&gt;BullMQ Queue]\n B --&gt;|HTTP + API Key| C[Android Phone&lt;br/&gt;Flask on Termux]\n C --&gt;|termux-sms-send| D[Android SMS]\n D --&gt;|Carrier Network| E[Recipients]\n E --&gt;|Reply SMS| D\n D --&gt;|termux-sms-list| C\n C --&gt;|HTTP response| B</code></pre> <p>Why this approach?</p> <ul> <li>No SaaS dependency \u2014 your phone is the SMS gateway, no Twilio/MessageBird/etc.</li> <li>Real phone number \u2014 recipients see a real number, not a short code</li> <li>Two-way messaging \u2014 incoming replies sync automatically</li> <li>Low cost \u2014 just your phone plan's SMS allowance</li> <li>Full control \u2014 FOSS stack end-to-end</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#prerequisites","title":"Prerequisites","text":"<p>Before starting setup, you'll need:</p> Item Details Android phone Any Android 7+ device with an active SIM card and SMS plan Termux Terminal emulator \u2014 install from F-Droid (not Play Store) Termux:API Termux plugin for SMS/contacts/battery \u2014 install from F-Droid Tailscale (recommended) VPN mesh for stable IP \u2014 install from Play Store Network access Phone must be reachable from the server (Tailscale, LAN, or port forwarding) <p>Both Apps MUST Come from F-Droid</p> <p>The Play Store version of Termux is abandoned and incompatible with the API plugin. If you install Termux from the Play Store and Termux:API from F-Droid (or vice versa), SMS commands will fail with:</p> <p>Termux:API is not yet available on Google Play</p> <p>Fix: Uninstall both apps, then reinstall both from F-Droid. They must come from the same source because Android verifies matching app signatures for inter-process communication.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#phone-setup","title":"Phone Setup","text":"","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-1-install-apps-from-f-droid","title":"Step 1: Install Apps from F-Droid","text":"<p>On your Android phone:</p> <ol> <li>Install F-Droid \u2014 download from f-droid.org if you don't have it</li> <li>Install Termux \u2014 search in F-Droid and install</li> <li>Install Termux:API \u2014 search in F-Droid and install</li> <li>Install Termux:Boot (optional) \u2014 for auto-start on phone reboot. Open once after install to register.</li> <li>Install Tailscale (recommended) \u2014 from Play Store, connect to your tailnet for a stable IP</li> </ol> <p>termux-api package</p> <p>You need two things called \"termux-api\": the F-Droid app (Termux:API) and the Termux package (<code>pkg install termux-api</code>). The setup script installs the package automatically, but the F-Droid app must be installed manually.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-2-generate-api-key","title":"Step 2: Generate API Key","text":"<p>Go to the admin dashboard SMS Setup page (<code>/app/sms/setup</code>) and click Generate API Key. Copy the key \u2014 you'll paste it into the setup command.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-3-run-the-setup-script","title":"Step 3: Run the Setup Script","text":"<p>Open Termux on the phone and run:</p> <pre><code># Clone the SMS server (first time only)\npkg install -y git &amp;&amp; git clone --depth 1 --filter=blob:none --sparse \\\n https://gitea.bnkops.com/admin/changemaker.lite.git ~/sms-server \\\n &amp;&amp; cd ~/sms-server &amp;&amp; git sparse-checkout set termux-sms\n\n# Run the setup script \u2014 paste your API key at the end\nbash ~/sms-server/termux-sms/setup.sh YOUR_API_KEY_HERE\n</code></pre> <p>The setup script automatically:</p> <ul> <li>Installs Python, Flask, termux-api, and openssh</li> <li>Saves the API key to <code>~/.bashrc</code></li> <li>Requests SMS and Contacts permissions (tap Allow when prompted)</li> <li>Creates a Termux:Boot auto-start script (if Termux:Boot is installed)</li> <li>Starts the SMS server</li> </ul> <p>When done, note the Phone URL displayed (e.g. <code>http://100.64.0.5:5001</code>).</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#recommended-install-service-supervisor","title":"Recommended: Install Service Supervisor","text":"<p>After initial setup, install <code>termux-services</code> for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:</p> <pre><code>cd ~/sms-server &amp;&amp; bash termux-sms/setup-services.sh\n</code></pre> <p>This registers two supervised services:</p> <ul> <li>sms-api \u2014 Flask SMS API server (port 5001)</li> <li>sshd-custom \u2014 SSH daemon for remote management (port 8022)</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-4-prevent-android-from-killing-termux","title":"Step 4: Prevent Android from Killing Termux","text":"<p>This is required for the server to run reliably in the background:</p> <ol> <li>Open Android Settings \u2192 Apps \u2192 Termux \u2192 Battery \u2192 set to Unrestricted</li> <li>Lock Termux in the recent apps view (long-press the app card \u2192 Lock/Pin)</li> <li>Samsung phones: also add Termux to Settings \u2192 Device Care \u2192 Battery \u2192 Never Sleeping Apps</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#updating","title":"Updating","text":"<p>To pull the latest server code and re-run setup:</p> <pre><code>cd ~/sms-server &amp;&amp; git pull &amp;&amp; bash android/setup.sh YOUR_API_KEY_HERE\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#service-management","title":"Service Management","text":"<p>If you installed <code>termux-services</code> (recommended):</p> <pre><code># Check status\nsv status sms-api\n\n# Restart\nsv restart sms-api\n\n# Stop\nsv down sms-api\n\n# Start\nsv up sms-api\n\n# View logs\ntail -f ~/logs/sms-api.log\n\n# Health check\ncurl http://127.0.0.1:5001/health\n</code></pre> <p>Without <code>termux-services</code> (legacy watchdog):</p> <pre><code># Check if the server is running\ncurl http://127.0.0.1:5001/health\n\n# Restart manually\ncd ~/sms-server/android &amp;&amp; bash sms-watchdog.sh\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#accessing-the-phone","title":"Accessing the Phone","text":"<p>There are several ways to run commands on the phone:</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#direct-on-phone","title":"Direct (on phone)","text":"<p>Simply open the Termux app and type commands. Best for initial setup.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#ssh-remote-access","title":"SSH (remote access)","text":"<p>Start the SSH server in Termux, then connect from your computer:</p> <pre><code># On the phone (first time only):\npkg install openssh\npasswd # Set a password\nsshd # Start SSH server on port 8022\n\n# From your computer:\nssh -p 8022 your-phone-ip\n# Or with Tailscale:\nssh -p 8022 100.x.x.x\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#scrcpy-screen-mirror","title":"scrcpy (screen mirror)","text":"<p>Mirror the phone screen to your computer \u2014 great for setup:</p> <pre><code># Install scrcpy on your computer (Ubuntu)\nsudo apt install scrcpy\n\n# Connect via USB\nscrcpy\n\n# Or wireless (phone must be on same network)\nscrcpy --tcpip=phone-ip:5555\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#setup-wizard","title":"Setup Wizard","text":"<p>The admin panel provides a guided three-step wizard at <code>/app/sms/setup</code>:</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-1-prepare-phone","title":"Step 1: Prepare Phone","text":"<p>Walks you through installing apps, cloning the server, setting the API key, and starting the Flask server. Generates a shared API key that both the server and phone use for authentication.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-2-connect","title":"Step 2: Connect","text":"<p>Choose how to find your phone's IP address:</p> Tailscale Auto-Discovery (Recommended)Manual URL Entry <ol> <li>Enter your Tailscale API key (<code>tskey-api-...</code>)</li> <li>Click Discover Devices</li> <li>The wizard queries the Tailscale API and lists all devices on your tailnet</li> <li>Select your Android phone \u2014 the URL auto-fills with its stable <code>100.x.x.x</code> IP</li> </ol> <p>Getting a Tailscale API Key</p> <p>Go to Tailscale Admin Console \u2192 Settings \u2192 Keys \u2192 Generate auth key or API access token.</p> <p>Enter the phone's URL directly:</p> <ul> <li>With Tailscale: <code>http://100.x.x.x:5001</code> (stable IP, works across networks)</li> <li>On same LAN: <code>http://192.168.x.x:5001</code> (changes if phone reconnects)</li> <li>Via port forward: <code>http://your-public-ip:5001</code> (requires router config)</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#step-3-test-save","title":"Step 3: Test &amp; Save","text":"<ol> <li>Click Test Connection \u2014 the wizard calls the phone's <code>/health</code> endpoint</li> <li>On success, you'll see device uptime and message count</li> <li>Click Save Configuration \u2014 stores the URL and key encrypted in the database</li> <li>The <code>enableSms</code> feature flag is automatically enabled</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#how-it-works","title":"How It Works","text":"","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#sending-messages","title":"Sending Messages","text":"<ol> <li>Admin creates an SMS campaign with a message template and contact list</li> <li>Campaign is started \u2192 messages are queued in BullMQ (one at a time, serial delivery)</li> <li>For each message, the Express API calls <code>POST /api/sms/send</code> on the phone</li> <li>The Flask server on the phone executes <code>termux-sms-send</code> to send via Android's native SMS</li> <li>A notification appears on the phone for each sent message</li> <li>Results are tracked in the database (success/failure per recipient)</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#receiving-responses","title":"Receiving Responses","text":"<p>A background service (<code>sms-response-sync.service.ts</code>) polls the phone's inbox at a configurable interval:</p> <ol> <li>Calls <code>GET /api/sms/inbox?since=&lt;last_sync_timestamp&gt;</code> on the phone</li> <li>The Flask server runs <code>termux-sms-list</code> to get new messages</li> <li>Incoming messages are matched to contacts and classified by keyword</li> <li>Threaded conversations are maintained per contact</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#device-monitoring","title":"Device Monitoring","text":"<p>A background service (<code>sms-device-monitor.service.ts</code>) checks phone health periodically:</p> <ul> <li>Battery level, charging status, temperature</li> <li>Server uptime and total messages sent</li> <li>Connection status (available/unreachable)</li> <li>Results displayed on the SMS Dashboard</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#key-features","title":"Key Features","text":"<ul> <li>Contact lists \u2014 import, tag, and segment contacts for targeted outreach</li> <li>Message templates \u2014 reusable templates with <code>{name}</code> variable placeholders</li> <li>BullMQ queue \u2014 serial delivery with configurable delays between messages</li> <li>Response sync \u2014 incoming SMS replies synced and classified automatically</li> <li>Device monitoring \u2014 battery, uptime, and connectivity reported in real-time</li> <li>Conversation view \u2014 threaded message history per contact</li> <li>Retry logic \u2014 configurable retry attempts for failed deliveries</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#admin-routes","title":"Admin Routes","text":"Route Description <code>/app/sms/setup</code> Guided setup wizard with Tailscale auto-discovery <code>/app/sms</code> SMS dashboard \u2014 campaign overview and device status <code>/app/sms/contacts</code> Manage contact lists and entries <code>/app/sms/campaigns</code> Create and monitor SMS campaigns <code>/app/sms/conversations</code> View threaded conversations with contacts","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#configuration","title":"Configuration","text":"","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#environment-variables","title":"Environment Variables","text":"Variable Default Description <code>ENABLE_SMS</code> <code>false</code> Feature flag (also set via setup wizard) <code>TERMUX_API_URL</code> \u2014 Phone URL, e.g. <code>http://100.x.x.x:5001</code> <code>TERMUX_API_KEY</code> \u2014 Shared API key for authentication <code>SMS_DELAY_BETWEEN_MS</code> <code>1000</code> Delay between messages in a campaign (ms) <code>SMS_MAX_RETRIES</code> <code>3</code> Retry attempts for failed sends <code>SMS_RESPONSE_SYNC_INTERVAL_MS</code> <code>10000</code> How often to check for incoming replies (ms) <code>SMS_DEVICE_MONITOR_INTERVAL_MS</code> <code>30000</code> How often to check device health (ms) <code>TAILSCALE_API_KEY</code> \u2014 Tailscale API key for auto-discovery <code>TAILSCALE_TAILNET</code> \u2014 Tailscale tailnet name (optional) <p>Note</p> <p>When you use the setup wizard, configuration is stored in the database and takes priority over environment variables. You don't need to set env vars if you use the wizard.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#phone-side-configuration","title":"Phone-Side Configuration","text":"<p>On the phone, only one environment variable is needed:</p> <pre><code>export SMS_API_SECRET='your-64-char-hex-key'\n</code></pre> <p>The Flask server also accepts <code>TERMUX_API_KEY</code> as an alias for backwards compatibility.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#phone-api-endpoints","title":"Phone API Endpoints","text":"<p>The Flask server running on the phone exposes these endpoints on port 5001:</p> Method Endpoint Auth Description <code>GET</code> <code>/health</code> No Server status, uptime, messages sent <code>GET</code> <code>/</code> No Web dashboard with endpoint documentation <code>POST</code> <code>/api/sms/send</code> Yes Send an SMS message <code>POST</code> <code>/api/sms/send-reply</code> Yes Send a reply with conversation tracking <code>GET</code> <code>/api/sms/inbox</code> No Get incoming messages (with <code>since</code> filter) <code>GET</code> <code>/api/sms/list</code> No List messages with pagination <code>GET</code> <code>/api/sms/history</code> No Get SMS history for a phone number <code>GET</code> <code>/api/device/battery</code> No Battery level, health, temperature <code>GET</code> <code>/api/device/location</code> No GPS coordinates (requires permission) <code>GET</code> <code>/api/device/info</code> No Device info + battery + uptime <code>GET</code> <code>/api/contacts/list</code> No Phone address book (with search) <code>POST</code> <code>/api/campaign/notify</code> No Push notification to device <p>Authentication uses the <code>X-API-Key</code> header with the shared secret.</p>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#troubleshooting","title":"Troubleshooting","text":"","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#phone-cant-be-reached","title":"Phone can't be reached","text":"<p>Symptoms: Test connection fails, \"Connection refused\" or timeout.</p> <p>Checks:</p> <ol> <li>Is the Flask server running? Check Termux \u2014 you should see the startup banner</li> <li>Is the IP correct? Run <code>ifconfig</code> in Termux to find the current IP</li> <li>Are they on the same network? If not using Tailscale, both must be on the same LAN</li> <li>Is Tailscale connected? Check the Tailscale app on the phone \u2014 it should show \"Connected\"</li> <li>Firewall? Android rarely blocks incoming connections on Termux, but check if any firewall app is installed</li> </ol> <pre><code># Quick test from your server\ncurl http://PHONE_IP:5001/health\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#authentication-required-errors","title":"\"Authentication required\" errors","text":"<p>Symptoms: API calls return 401 with \"Authentication required\".</p> <p>Fix: The API key on the phone doesn't match the one in the admin panel.</p> <pre><code># On the phone, check the current key\necho $SMS_API_SECRET\n\n# If it doesn't match, update it\nexport SMS_API_SECRET='correct-key-from-admin-panel'\necho 'export SMS_API_SECRET=\"correct-key-from-admin-panel\"' &gt;&gt; ~/.bashrc\n\n# Restart the server\nsv restart sms-api\n# Or without termux-services: pkill -f termux-sms-api-server.py &amp;&amp; cd ~/sms-server/android &amp;&amp; python termux-sms-api-server.py\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#sms-not-sending","title":"SMS not sending","text":"<p>Symptoms: Server responds successfully but messages don't arrive.</p> <p>Checks:</p> <ol> <li>SMS permissions granted? Go to Android Settings \u2192 Apps \u2192 Termux:API \u2192 Permissions \u2192 SMS</li> <li>Active SIM card? The phone needs a working SIM with SMS capability</li> <li>Message too long? Maximum 1600 characters per message</li> <li>Rate limited? Minimum 1 second between messages (carrier may enforce longer delays)</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#termux-keeps-getting-killed","title":"Termux keeps getting killed","text":"<p>Symptoms: Server stops after some time, especially when phone screen is off.</p> <p>Fix:</p> <ol> <li>Install <code>termux-services</code> (if not already): <code>bash ~/sms-server/android/setup-services.sh</code> \u2014 this uses runit, a proper service supervisor that auto-restarts the server immediately if it crashes</li> <li>Disable battery optimization: Android Settings \u2192 Apps \u2192 Termux \u2192 Battery \u2192 Unrestricted</li> <li>Lock Termux in recent apps \u2014 long-press the app card \u2192 Lock/Pin</li> <li>Samsung: also add Termux, Termux:API, and Termux:Boot to Settings \u2192 Device Care \u2192 Battery \u2192 Never Sleeping Apps</li> <li>Acquire wake lock: Run <code>termux-wake-lock</code> in Termux (included in boot script)</li> </ol>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#server-wont-start-missing-sms_api_secret","title":"Server won't start \u2014 \"Missing SMS_API_SECRET\"","text":"<p>Symptoms: Server exits immediately with a security error.</p> <p>Fix: Set the API key environment variable:</p> <pre><code># Generate a new key if you don't have one\npython -c \"import secrets; print(secrets.token_hex(32))\"\n\n# Set it\nexport SMS_API_SECRET='your-generated-key'\necho 'export SMS_API_SECRET=\"your-key\"' &gt;&gt; ~/.bashrc\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#rcs-chat-features-interfering-with-replies","title":"RCS / Chat Features interfering with replies","text":"<p>Symptoms: You send SMS messages successfully but some or all replies never appear in the system. Recipients say they replied, but the conversation shows no inbound messages.</p> <p>Cause: Google Messages enables RCS (Rich Communication Services) by default. When RCS is active, replies from recipients who also have RCS may be sent over data/Wi-Fi instead of the carrier SMS channel. The Termux server only reads the SMS inbox via <code>termux-sms-list</code>, so RCS messages are invisible to it.</p> <p>Fix: Disable RCS on the SMS phone:</p> <ol> <li>Open Google Messages on the phone</li> <li>Tap the profile icon (top right) \u2192 Messages settings</li> <li>Tap RCS chats (or \"Chat features\")</li> <li>Turn off \"Turn on RCS chats\"</li> </ol> <p>This must be done on the phone running the SMS server</p> <p>Disabling RCS on the server phone forces all outgoing messages to use plain SMS, and ensures replies also come back as SMS. You do not need recipients to change anything on their end \u2014 when the server phone sends a plain SMS, the reply will be plain SMS as well (unless the recipient's carrier forces RCS-only, which is rare).</p> <p>Additional checks:</p> <ul> <li>Some carriers (e.g. Google Fi, Jio) enable RCS at the carrier level. If disabling in the app doesn't help, contact the carrier to disable RCS on the SIM.</li> <li>If the phone has Samsung Messages instead of Google Messages, go to Samsung Messages \u2192 Settings \u2192 Chat settings \u2192 turn off.</li> <li>After disabling RCS, restart the phone and verify by sending a test message \u2014 the send button should show an SMS label, not \"Chat\".</li> </ul>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/broadcast/sms/#updating-the-sms-server","title":"Updating the SMS server","text":"<p>To pull the latest version of the server code:</p> <pre><code>cd ~/sms-server\ngit pull\n\n# Restart the server\nsv restart sms-api\n# Or without termux-services: pkill -f termux-sms-api-server.py &amp;&amp; cd android &amp;&amp; python termux-sms-api-server.py\n</code></pre>","tags":["guide","admin","broadcast","sms"]},{"location":"docs/admin/map/","title":"Map &amp; Canvassing","text":"<p>Manage locations, organize canvassing territories, schedule volunteer shifts, and coordinate door-to-door outreach.</p>","tags":["guide","admin","map"]},{"location":"docs/admin/map/#in-this-section","title":"In This Section","text":"<ul> <li>Locations \u2014 import addresses via CSV or NAR, geocode with multiple providers, and manage the location database</li> <li>Areas \u2014 draw polygon territories on the map to organize canvassing regions</li> <li>Shifts \u2014 schedule volunteer time slots with recurring patterns and calendar views</li> <li>Canvassing \u2014 canvass dashboard, walk sheets, contact export, and session management</li> <li>Data Quality \u2014 geocoding quality metrics, provider distribution, and confidence analysis</li> <li>Map Settings \u2014 configure map center, zoom level, and QR code links</li> </ul>","tags":["guide","admin","map"]},{"location":"docs/admin/map/areas/","title":"Areas (Cuts)","text":"<p>Draw polygon regions on the map to define canvassing territories. Areas organize locations into manageable chunks for volunteers.</p> <p></p>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/areas/#key-features","title":"Key Features","text":"<ul> <li>Polygon drawing \u2014 use the map editor at <code>/app/map/cuts</code> to draw, edit, and delete area boundaries</li> <li>Automatic association \u2014 locations within an area's polygon boundary are automatically linked</li> <li>Area stats \u2014 total addresses, visited count, coverage percentage per area</li> <li>Color coding \u2014 assign colors to visually distinguish areas on the map</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/areas/#area-import-wizard","title":"Area Import Wizard","text":"<p>Bulk-import addresses into an area from multiple data sources:</p> <ul> <li>OpenStreetMap (OSM) \u2014 pull building addresses from Nominatim within the area</li> <li>NAR (National Address Register) \u2014 import from the Canadian federal address dataset</li> <li>Reverse geocode grid \u2014 generate a grid of points and reverse-geocode to discover addresses</li> <li>Deduplication \u2014 imported addresses are checked against existing locations to avoid duplicates</li> <li>Progress tracking \u2014 real-time status per source during import</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/areas/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map/cuts</code> \u2014 draw and manage canvassing areas</li> <li><code>/app/map/cuts/:id/export</code> \u2014 printable location report for a cut</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/canvassing/","title":"Canvassing","text":"<p>Coordinate and monitor volunteer door-to-door outreach with the canvass dashboard, walk sheets, and contact export tools.</p> <p></p>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/canvassing/#canvass-dashboard","title":"Canvass Dashboard","text":"<p>From <code>/app/map/canvass</code>:</p> <ul> <li>Active sessions \u2014 see which volunteers are currently canvassing and their real-time positions</li> <li>Leaderboard \u2014 volunteer rankings by visit count</li> <li>Activity feed \u2014 recent visit outcomes across all areas</li> <li>Stats \u2014 total sessions, visits recorded, and outcome breakdowns</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/canvassing/#walk-sheets-exports","title":"Walk Sheets &amp; Exports","text":"<ul> <li>Walk sheet \u2014 printable form at <code>/app/map/walk-sheet</code> with space for recording visit outcomes; includes up to 3 configurable QR codes</li> <li>Cut export \u2014 printable location report at <code>/app/map/cuts/:id/export</code> for a specific canvassing area</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/canvassing/#canvass-contact-export","title":"Canvass Contact Export","text":"<p>Bridge canvassing data with advocacy campaigns:</p> <ul> <li>Filter by outcome \u2014 include specific visit outcomes (spoke with, left literature, come back later)</li> <li>Support level range \u2014 filter by recorded support level</li> <li>Area selection \u2014 limit export to specific areas</li> <li>Campaign targeting \u2014 export contacts as recipients for an advocacy campaign</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/canvassing/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map/canvass</code> \u2014 canvass dashboard</li> <li><code>/app/map/walk-sheet</code> \u2014 printable walk sheet</li> </ul>","tags":["guide","admin","map","canvassing"]},{"location":"docs/admin/map/data-quality/","title":"Data Quality","text":"<p>Monitor geocoding accuracy and coverage from <code>/app/map/data-quality</code>.</p> <p></p>","tags":["guide","admin","map","analytics"]},{"location":"docs/admin/map/data-quality/#key-metrics","title":"Key Metrics","text":"<ul> <li>Geocoding success rate \u2014 percentage of locations with valid coordinates</li> <li>Provider distribution \u2014 breakdown of which geocoding provider was used per location</li> <li>Confidence scores \u2014 distribution of geocoding confidence levels across the dataset</li> <li>Missing data \u2014 locations without coordinates, postal codes, or province assignments</li> <li>Bulk re-geocode \u2014 re-process failed or low-confidence locations with a different provider</li> </ul>","tags":["guide","admin","map","analytics"]},{"location":"docs/admin/map/data-quality/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map/data-quality</code> \u2014 geocoding quality dashboard</li> </ul>","tags":["guide","admin","map","analytics"]},{"location":"docs/admin/map/locations/","title":"Locations","text":"<p>Import addresses via CSV or the Canadian National Address Register (NAR), geocode them with multiple providers, and manage the location database.</p> <p></p>","tags":["guide","admin","map","locations"]},{"location":"docs/admin/map/locations/#adding-locations","title":"Adding Locations","text":"<ul> <li>Click-to-add \u2014 click on the admin map to drop a new location marker</li> <li>Form entry \u2014 manually enter address details</li> <li>CSV import \u2014 upload a CSV with address columns; the system geocodes each row</li> <li>NAR import \u2014 import Canadian National Address Register data with province, city, postal code, and residential-only filters</li> </ul>","tags":["guide","admin","map","locations"]},{"location":"docs/admin/map/locations/#geocoding","title":"Geocoding","text":"<p>Locations are geocoded automatically using a multi-provider system supporting Nominatim, ArcGIS, Photon, Mapbox, Google, and LocationIQ. Failed entries can be re-geocoded individually or in bulk.</p>","tags":["guide","admin","map","locations"]},{"location":"docs/admin/map/locations/#bulk-operations","title":"Bulk Operations","text":"<p>Select multiple locations for:</p> <ul> <li>Re-geocoding with a different provider</li> <li>Tagging or re-tagging</li> <li>Deletion</li> <li>CSV export</li> </ul>","tags":["guide","admin","map","locations"]},{"location":"docs/admin/map/locations/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map</code> \u2014 location CRUD, CSV import/export, geocoding, area import wizard</li> </ul>","tags":["guide","admin","map","locations"]},{"location":"docs/admin/map/settings/","title":"Map Settings","text":"<p>Configure the default map view and QR code links from <code>/app/map/settings</code>.</p> <p></p>","tags":["guide","admin","map","configuration"]},{"location":"docs/admin/map/settings/#settings","title":"Settings","text":"<ul> <li>Map center \u2014 latitude and longitude for the default map center point</li> <li>Default zoom \u2014 initial zoom level when maps load (1-18)</li> <li>QR code links \u2014 up to 3 configurable URLs that appear as QR codes on printed walk sheets (e.g., campaign page, shift signup, volunteer portal)</li> </ul>","tags":["guide","admin","map","configuration"]},{"location":"docs/admin/map/settings/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map/settings</code> \u2014 map configuration</li> </ul>","tags":["guide","admin","map","configuration"]},{"location":"docs/admin/map/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> <p></p>","tags":["guide","admin","map","shifts"]},{"location":"docs/admin/map/shifts/#creating-shifts","title":"Creating Shifts","text":"<ul> <li>Single shifts \u2014 set date, time, location description, and optional area assignment</li> <li>Recurring shifts \u2014 create series with daily, weekly, or monthly frequency; weekly allows specific day selection</li> <li>Calendar view \u2014 dedicated calendar tab showing shifts by date; click any date to create a new shift pre-filled</li> </ul>","tags":["guide","admin","map","shifts"]},{"location":"docs/admin/map/shifts/#series-management","title":"Series Management","text":"<ul> <li>Edit modes \u2014 when editing a recurring shift, choose: this shift only, this and future, or all in series</li> <li>Date range \u2014 define start and optional end date; generates up to 12 weeks (capped at 100 shifts)</li> <li>Detach \u2014 remove a shift from its series to edit independently</li> </ul>","tags":["guide","admin","map","shifts"]},{"location":"docs/admin/map/shifts/#signups","title":"Signups","text":"<ul> <li>Signup drawer \u2014 view all signups for a shift in the admin panel</li> <li>Capacity \u2014 optionally set maximum volunteer count per shift</li> <li>Confirmation emails \u2014 automatic email sent when a volunteer signs up or cancels</li> </ul>","tags":["guide","admin","map","shifts"]},{"location":"docs/admin/map/shifts/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/map/shifts</code> \u2014 shift CRUD, calendar view, signup management</li> </ul>","tags":["guide","admin","map","shifts"]},{"location":"docs/admin/media/","title":"Media","text":"<p>Upload, organize, and share campaign videos and photos with built-in analytics and engagement features. Enable with <code>enableMediaFeatures</code> in Settings.</p>","tags":["guide","admin","media"]},{"location":"docs/admin/media/#in-this-section","title":"In This Section","text":"<ul> <li>Library \u2014 upload videos and photos, manage metadata, schedule publishing, and generate preview links</li> <li>Analytics \u2014 view counts, watch time, completion rates, traffic sources, and viewer activity</li> <li>Curated Gallery \u2014 playlists, shorts feed, and featured content for the public gallery</li> <li>Moderation \u2014 comment review, word filters, and content moderation tools</li> <li>Gallery Ads \u2014 promotional cards with audience targeting, scheduling, and click-through analytics</li> </ul>","tags":["guide","admin","media"]},{"location":"docs/admin/media/ads/","title":"Gallery Ads","text":"<p>Create promotional cards that appear in the public media gallery and documentation site. Manage from <code>/app/media/ads</code>.</p>","tags":["guide","admin","media"]},{"location":"docs/admin/media/ads/#key-features","title":"Key Features","text":"<ul> <li>Ad CRUD \u2014 create ads with title, description, image, and click-through URL</li> <li>Placement targeting \u2014 assign ads to specific placements (gallery sidebar, gallery feed, docs sidebar)</li> <li>Scheduling \u2014 set start and end dates for time-limited promotions</li> <li>Click tracking \u2014 view impressions and click-through rates per ad</li> <li>Priority ordering \u2014 control which ads appear first when multiple are active</li> </ul>","tags":["guide","admin","media"]},{"location":"docs/admin/media/ads/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/media/ads</code> \u2014 gallery ad management and analytics</li> </ul>","tags":["guide","admin","media"]},{"location":"docs/admin/media/analytics/","title":"Analytics","text":"<p>Track video engagement with GDPR-compliant analytics (IP hashing, 90-day retention).</p> <p></p>","tags":["guide","admin","media","analytics"]},{"location":"docs/admin/media/analytics/#per-video-metrics","title":"Per-Video Metrics","text":"<p>Each video tracks:</p> <ul> <li>View count and unique viewers</li> <li>Average watch time and completion rate</li> <li>Traffic sources \u2014 direct, embedded, shared</li> <li>Registered viewer activity (when logged in)</li> </ul>","tags":["guide","admin","media","analytics"]},{"location":"docs/admin/media/analytics/#global-dashboard","title":"Global Dashboard","text":"<p>The analytics dashboard at <code>/app/media/analytics</code> provides:</p> <ul> <li>Aggregate view counts across all videos</li> <li>Top-performing content by views and completion rate</li> <li>Viewer trends over time</li> <li>Traffic source breakdown</li> </ul>","tags":["guide","admin","media","analytics"]},{"location":"docs/admin/media/analytics/#tracking","title":"Tracking","text":"<p>Public endpoints record engagement:</p> <ul> <li>View initiation</li> <li>10-second heartbeat intervals</li> <li><code>navigator.sendBeacon</code> for reliable end-of-session reporting</li> </ul>","tags":["guide","admin","media","analytics"]},{"location":"docs/admin/media/analytics/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/media/analytics</code> \u2014 global analytics dashboard</li> </ul>","tags":["guide","admin","media","analytics"]},{"location":"docs/admin/media/curated/","title":"Curated Gallery","text":"<p>Curate the public gallery experience with playlists, a shorts feed, and featured content.</p> <p></p>","tags":["guide","admin","media","gallery"]},{"location":"docs/admin/media/curated/#playlists","title":"Playlists","text":"<p>From <code>/app/media/curated</code>:</p> <ul> <li>Three types \u2014 admin playlists (managed), user playlists (personal), and public playlists (community)</li> <li>Drag-reorder \u2014 arrange videos within a playlist</li> <li>Featured carousel \u2014 feature playlists on the gallery homepage</li> <li>Dedicated viewer \u2014 full playlist playback page with up-next queue</li> </ul>","tags":["guide","admin","media","gallery"]},{"location":"docs/admin/media/curated/#shorts-feed","title":"Shorts Feed","text":"<p>TikTok-style vertical video feed for clips under 60 seconds:</p> <ul> <li>Automatic classification \u2014 videos under 60 seconds are flagged as shorts</li> <li>Vertical feed \u2014 mobile-optimized swipeable interface at <code>/gallery/shorts</code></li> <li>Autoplay \u2014 continuous playback as viewers scroll</li> </ul>","tags":["guide","admin","media","gallery"]},{"location":"docs/admin/media/curated/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/media/curated</code> \u2014 playlist management</li> </ul>","tags":["guide","admin","media","gallery"]},{"location":"docs/admin/media/library/","title":"Library","text":"<p>The media library at <code>/app/media/library</code> is where you upload, organize, and publish video and photo content.</p> <p></p>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/library/#video-upload","title":"Video Upload","text":"<ul> <li>Drag-and-drop \u2014 single or batch upload (up to 10GB per file)</li> <li>Supported formats \u2014 MP4, MOV, AVI, MKV, WebM, M4V, FLV</li> <li>Automatic metadata \u2014 FFprobe extracts duration, dimensions, orientation, quality, and audio info</li> <li>Quick actions \u2014 hover a video card for Edit (E), Preview (P), Analytics (A), Schedule (S) keyboard shortcuts</li> </ul>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/library/#photo-management","title":"Photo Management","text":"<ul> <li>Albums \u2014 organize photos into named collections with cover images</li> <li>Bulk uploads \u2014 drag-and-drop multiple photos with automatic metadata extraction</li> <li>Photo picker \u2014 insert photos into landing pages and email templates via a modal picker</li> </ul>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/library/#scheduled-publishing","title":"Scheduled Publishing","text":"<ul> <li>Publish/unpublish dates \u2014 set future dates for automatic state changes</li> <li>Timezone support \u2014 11 supported timezones</li> <li>Calendar view \u2014 visualize scheduled items on the Calendar tab</li> <li>BullMQ automation \u2014 jobs fire at scheduled times</li> </ul>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/library/#preview-links","title":"Preview Links","text":"<p>Generate 24-hour JWT-authenticated preview links for unpublished videos \u2014 useful for stakeholder review before publishing.</p>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/library/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/media/library</code> \u2014 video and photo management</li> <li><code>/app/media/jobs</code> \u2014 processing job queue monitoring</li> </ul>","tags":["guide","admin","media","videos"]},{"location":"docs/admin/media/moderation/","title":"Moderation","text":"<p>Admin tools for reviewing and managing comments across all media content at <code>/app/media/moderation</code>.</p>","tags":["guide","admin","media","moderation"]},{"location":"docs/admin/media/moderation/#moderation-dashboard","title":"Moderation Dashboard","text":"<ul> <li>Filter by status \u2014 pending, safe, flagged, hidden</li> <li>Status counts \u2014 summary stats showing total, pending, flagged, hidden, and safe comments</li> <li>Search \u2014 filter by video, date range, or text content</li> </ul>","tags":["guide","admin","media","moderation"]},{"location":"docs/admin/media/moderation/#moderation-actions","title":"Moderation Actions","text":"<p>For each comment, admins can:</p> <ul> <li>Approve \u2014 mark as safe and unhide if previously hidden</li> <li>Hide \u2014 remove from public view with a reason (manual, word filter, spam, or link)</li> <li>Unhide \u2014 restore a previously hidden comment</li> <li>Delete \u2014 permanently remove</li> <li>Add notes \u2014 internal moderation notes (not visible to users)</li> </ul>","tags":["guide","admin","media","moderation"]},{"location":"docs/admin/media/moderation/#word-filter","title":"Word Filter","text":"<p>A configurable list of words with severity levels:</p> Severity Action High Auto-blocks the comment Medium Auto-hides for review Low Flags for moderator attention Custom User-defined severity <p>The filter list is cached with a 1-minute TTL and invalidated on changes.</p>","tags":["guide","admin","media","moderation"]},{"location":"docs/admin/media/moderation/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/media/moderation</code> \u2014 comment moderation and word filter management</li> </ul>","tags":["guide","admin","media","moderation"]},{"location":"docs/admin/payments/","title":"Payments","text":"<p>Accept memberships, product sales, and donations through Stripe. Enable with <code>enablePayments</code> in Settings.</p>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/#in-this-section","title":"In This Section","text":"<ul> <li>Products \u2014 manage merchandise and one-time purchase items with inventory tracking</li> <li>Donations \u2014 donation pages with goals, suggested amounts, and branded thank-you messages</li> <li>Plans \u2014 recurring subscription plans with monthly and yearly billing</li> <li>Settings \u2014 Stripe API key configuration with encrypted storage</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/#how-it-works","title":"How It Works","text":"<ol> <li>Enable payments in Settings or <code>.env</code> (<code>ENABLE_PAYMENTS=true</code>)</li> <li>Configure Stripe API keys in Settings &gt; Payments (stored encrypted with <code>ENCRYPTION_KEY</code>)</li> <li>Payment widgets become available on landing pages and MkDocs pages</li> </ol>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/donations/","title":"Donations","text":"<p>Create custom branded donation landing pages with independent branding and goals.</p>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/donations/#donation-pages","title":"Donation Pages","text":"<p>From <code>/app/donation-pages</code>:</p> <ul> <li>Custom branding \u2014 each page has its own title, description, and cover image</li> <li>Configurable amounts \u2014 set suggested donation amounts per page</li> <li>Thank-you messages \u2014 customizable post-donation confirmation</li> <li>Public slugs \u2014 shareable URL at <code>/donate/:slug</code></li> <li>Goal tracking \u2014 fundraising goals with progress indicators</li> <li>Multiple campaigns \u2014 run several pages simultaneously with independent tracking</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/donations/#donation-management","title":"Donation Management","text":"<p>From <code>/app/donations</code>:</p> <ul> <li>View all donations with date, amount, donor info, and status</li> <li>Filter by donation page, date range, or amount</li> <li>Export to CSV for accounting and tax receipts</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/donations/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/donations</code> \u2014 donation management</li> <li><code>/app/donation-pages</code> \u2014 donation page CRUD</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/plans/","title":"Plans","text":"<p>Create and manage recurring subscription plans for campaign supporters at <code>/app/plans</code>.</p>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/plans/#key-features","title":"Key Features","text":"<ul> <li>Tiered plans \u2014 multiple subscription tiers with different pricing and benefits</li> <li>Billing cycles \u2014 monthly and yearly billing options</li> <li>Stripe integration \u2014 subscriptions managed through Stripe for reliable recurring payments</li> <li>Subscriber tracking \u2014 view active subscribers, MRR, and churn metrics</li> <li>Public pricing page \u2014 plans displayed at <code>/pricing</code></li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/plans/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/plans</code> \u2014 subscription plan management</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/products/","title":"Products","text":"<p>Manage campaign merchandise and one-time purchase items at <code>/app/products</code>.</p>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/products/#key-features","title":"Key Features","text":"<ul> <li>Product CRUD \u2014 create products with title, description, price, and images</li> <li>Inventory management \u2014 track stock levels and set low-stock alerts</li> <li>Stripe checkout \u2014 seamless payment flow via Stripe</li> <li>Public shop \u2014 products displayed at <code>/shop</code> for public browsing and purchase</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/products/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/products</code> \u2014 product management</li> </ul>","tags":["guide","admin","payments"]},{"location":"docs/admin/payments/settings/","title":"Payment Settings","text":"<p>Configure Stripe integration from Settings &gt; Payments.</p>","tags":["guide","admin","payments","configuration"]},{"location":"docs/admin/payments/settings/#stripe-configuration","title":"Stripe Configuration","text":"<ul> <li>Publishable key \u2014 used by the frontend for Stripe Elements and Checkout</li> <li>Secret key \u2014 used by the API for creating charges and managing subscriptions</li> <li>Encrypted storage \u2014 both keys are stored encrypted in the database using the <code>ENCRYPTION_KEY</code> environment variable (AES encryption)</li> </ul>","tags":["guide","admin","payments","configuration"]},{"location":"docs/admin/payments/settings/#webhook","title":"Webhook","text":"<p>Stripe webhooks are automatically configured to handle:</p> <ul> <li>Successful payments and subscription renewals</li> <li>Failed payments and subscription cancellations</li> <li>Refunds and disputes</li> </ul>","tags":["guide","admin","payments","configuration"]},{"location":"docs/admin/payments/settings/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/settings</code> (Payments tab) \u2014 Stripe key configuration</li> </ul>","tags":["guide","admin","payments","configuration"]},{"location":"docs/admin/services/","title":"Services","text":"<p>Manage the platform's infrastructure services, monitoring stack, and third-party integrations.</p>","tags":["guide","admin","services"]},{"location":"docs/admin/services/#in-this-section","title":"In This Section","text":"<ul> <li>Tunnel \u2014 Pangolin tunnel management for public access without port forwarding</li> <li>CrowdSec &amp; Security \u2014 CrowdSec Manager, Tinyauth forward-auth, ISP whitelisting, and Turnstile captcha on the Pangolin server</li> <li>Monitoring \u2014 Prometheus metrics, Grafana dashboards, and Alertmanager</li> <li>Integrations \u2014 Chat, video conferencing, password manager, whiteboard, Git hosting, automation, and QR codes</li> <li>User Provisioning \u2014 automatic account sync across integrated services</li> </ul>","tags":["guide","admin","services"]},{"location":"docs/admin/services/crowdsec/","title":"CrowdSec Manager &amp; Security Configuration","text":"<p>This page covers the CrowdSec Manager web UI on the Pangolin server, protected behind Tinyauth authentication, along with tuning of CrowdSec security rules and enabling Cloudflare Turnstile captcha for the CrowdSec bouncer.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#architecture","title":"Architecture","text":"<pre><code>graph LR\n User --&gt;|HTTPS| Traefik\n Traefik --&gt;|forwardAuth| Tinyauth\n Tinyauth --&gt;|authenticated| Traefik\n Traefik --&gt;|proxy| CrowdSec-Manager\n CrowdSec-Manager --&gt;|API| CrowdSec\n Traefik --&gt;|bouncer plugin| CrowdSec\n CrowdSec --&gt;|captcha decision| Turnstile[Cloudflare Turnstile]</code></pre> <p>All services run on the same Docker Compose stack and share the <code>pangolin</code> network. Traefik reaches them through Gerbil's network namespace.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#components-added","title":"Components Added","text":"","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#crowdsec-manager","title":"CrowdSec Manager","text":"<p>Image: <code>hhftechnology/crowdsec-manager:1.1.0</code></p> <p>A web UI for managing CrowdSec operations \u2014 viewing alerts, decisions, managing bouncers, and configuring scenarios. It has read-only access to Traefik and CrowdSec configs and read-write access to its own data and backups.</p> <p>Accessible at: <code>https://crowdsec.bnkserve.org</code></p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#tinyauth","title":"Tinyauth","text":"<p>Image: <code>ghcr.io/steveiliop56/tinyauth:v4</code></p> <p>A lightweight forward-auth middleware that protects the CrowdSec Manager dashboard with a login screen. Traefik's <code>forwardAuth</code> middleware checks every request to the manager against Tinyauth before allowing access.</p> <p>Login page at: <code>https://auth.bnkserve.org</code></p> <p>User credentials are stored in a users file (<code>/data/users</code>) mounted from the host, using bcrypt-hashed passwords.</p> <p>Special Characters in Passwords</p> <p>Tinyauth v4 has a known issue where special characters (<code>@</code>, <code>!</code>, etc.) in passwords can cause login failures through the browser, even though the bcrypt hash verifies correctly via the CLI. Use alphanumeric passwords to avoid this.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#traefik-routing","title":"Traefik Routing","text":"","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#routers","title":"Routers","text":"Router Domain Middleware Purpose <code>crowdsec-manager-router</code> <code>crowdsec.bnkserve.org</code> <code>security-headers</code>, <code>tinyauth</code> Dashboard (HTTPS) <code>crowdsec-manager-redirect</code> <code>crowdsec.bnkserve.org</code> <code>redirect-to-https</code> HTTP \u2192 HTTPS redirect <code>tinyauth-router</code> <code>auth.bnkserve.org</code> <code>security-headers</code> Auth login page (HTTPS) <code>tinyauth-redirect</code> <code>auth.bnkserve.org</code> <code>redirect-to-https</code> HTTP \u2192 HTTPS redirect <p>No tinyauth middleware on the tinyauth router</p> <p>The <code>tinyauth-router</code> must not have the <code>tinyauth</code> forwardAuth middleware applied \u2014 this would create an infinite redirect loop.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#middleware","title":"Middleware","text":"<p>The <code>tinyauth</code> forwardAuth middleware forwards every request to <code>http://tinyauth:3000/api/auth/traefik</code>. If the user has a valid session cookie (scoped to <code>.bnkserve.org</code>), the request passes through. Otherwise, the user is redirected to the Tinyauth login page.</p> <pre><code>tinyauth:\n forwardAuth:\n address: http://tinyauth:3000/api/auth/traefik\n trustForwardHeader: true\n authResponseHeaders:\n - X-Forwarded-User\n</code></pre>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#crowdsec-tuning","title":"CrowdSec Tuning","text":"","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#relaxed-crawl-detection","title":"Relaxed Crawl Detection","text":"<p>The <code>crowdsecurity/http-crawl-non_statics</code> scenario was triggering on legitimate Canadian users browsing the site. The local override at <code>/etc/crowdsec/scenarios/http-crawl-non_statics.yaml</code> (replacing the hub symlink) has relaxed thresholds:</p> Parameter Before After Effect <code>capacity</code> 40 80 Twice as many distinct pages before triggering <code>leakspeed</code> 0.5s 0.25s Bucket drains twice as fast <p>Combined effect: 4x more lenient \u2014 a user must hit 80+ distinct non-static pages faster than 1 every 0.25 seconds to trigger a captcha.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#canadian-isp-whitelist","title":"Canadian ISP Whitelist","text":"<p>A whitelist expression in <code>/etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml</code> exempts traffic from major Canadian ISPs from all CrowdSec scenarios:</p> <pre><code>expression:\n - evt.Meta.ASNNumber in ['812', '852', '6327', '5645', '20365', '25668', '577']\n</code></pre> AS Number ISP 812 Rogers Communications 852 TELUS Communications 6327 Shaw Communications 5645 TekSavvy 20365 Freedom Mobile 25668 CipherKey 577 Bell Canada <p>Field name</p> <p>The GeoIP enricher populates <code>evt.Meta.ASNNumber</code> (not <code>ASNumber</code>). This can be verified by inspecting <code>/etc/crowdsec/parsers/s02-enrich/geoip-enrich.yaml</code>.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#cloudflare-turnstile-captcha","title":"Cloudflare Turnstile Captcha","text":"<p>Previously, CrowdSec captcha decisions resulted in a hard 403 block because no captcha provider was configured. Now, users with a captcha decision see a Cloudflare Turnstile challenge page and can proceed after solving it.</p> <p>Configuration added to the CrowdSec bouncer plugin in <code>dynamic_config.yml</code>:</p> <pre><code>captchaProvider: turnstile\ncaptchaSiteKey: &lt;site-key&gt;\ncaptchaSecretKey: &lt;secret-key&gt;\ncaptchaHTMLFilePath: /etc/traefik/captcha.html\n</code></pre> <p>Captcha HTML template path</p> <p>The <code>captcha.html</code> template is copied from the plugin source to <code>/etc/traefik/captcha.html</code> (the mounted config volume). Do not reference the <code>/plugins-storage/</code> path directly \u2014 the hash in that path changes on every Traefik restart.</p>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#dns-records","title":"DNS Records","text":"<p>Two A records pointing to <code>72.11.155.21</code>:</p> Record Purpose <code>crowdsec.bnkserve.org</code> CrowdSec Manager dashboard <code>auth.bnkserve.org</code> Tinyauth login page","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/crowdsec/#verification","title":"Verification","text":"<pre><code># Check containers are running and healthy\ndocker ps --filter name=crowdsec-manager --filter name=tinyauth\n\n# Check both are on the pangolin network\ndocker network inspect pangolin --format '{{range .Containers}}{{.Name}} {{end}}'\n\n# Verify no Canadian ISPs in active decisions\ndocker exec crowdsec cscli decisions list | grep \"CA\"\n\n# Check CrowdSec whitelist is loaded\ndocker exec crowdsec cscli parsers inspect mywhitelists\n\n# Check Traefik logs for captcha errors\ndocker logs traefik 2&gt;&amp;1 | grep -i captcha\n</code></pre>","tags":["guide","admin","services","security"]},{"location":"docs/admin/services/integrations/","title":"Integrations","text":"<p>Changemaker Lite integrates with several self-hosted services. Each runs as a Docker container and can be enabled independently.</p>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#team-chat-rocketchat","title":"Team Chat (Rocket.Chat)","text":"<p>Self-hosted team chat for volunteer coordination. Enable with <code>enableChat</code> in Settings.</p> <ul> <li>Channels &amp; DMs \u2014 organize conversations by topic, team, or campaign</li> <li>Iframe integration \u2014 embedded in the admin dashboard and volunteer portal</li> <li>Floating widget \u2014 minimizable chat FAB on admin pages (toggleable in Settings)</li> <li>SSO-ready \u2014 iframe authentication for seamless login</li> <li>Mobile apps \u2014 native Rocket.Chat apps work with your instance</li> <li>Routes: <code>/app/services/rocketchat</code>, <code>/volunteer/chat</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#video-conferencing-jitsi-meet","title":"Video Conferencing (Jitsi Meet)","text":"<p>Self-hosted video calls integrated with Rocket.Chat via JWT authentication. Enable with <code>enableMeet</code> in Settings.</p> <ul> <li>One-click calls \u2014 start a call from any Rocket.Chat channel or DM</li> <li>JWT auth \u2014 participants join automatically with no separate login</li> <li>4 containers \u2014 jitsi-web, jitsi-prosody (XMPP/JWT), jitsi-jicofo (conference focus), jitsi-jvb (video bridge)</li> <li>Setup: Generate secrets, start containers, configure the Jitsi marketplace app in Rocket.Chat, set token expiration to <code>now + 1hour</code></li> </ul> <p>Token Expiration</p> <p>Set the Jitsi app's Token Expiration to <code>now + 1hour</code>. A raw number like <code>120</code> is interpreted as Unix timestamp 120 (Jan 1970), causing all tokens to appear expired.</p>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#password-manager-vaultwarden","title":"Password Manager (Vaultwarden)","text":"<p>Bitwarden-compatible password vault for secure team credential sharing.</p> <ul> <li>Bitwarden client compatible \u2014 use official browser extensions, desktop apps, and mobile apps</li> <li>Auto-invite \u2014 initial admin user invited on first startup</li> <li>User provisioning \u2014 new platform users can be auto-invited when provisioning is enabled</li> <li>Client setup: Point Bitwarden clients to <code>https://vault.DOMAIN</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#whiteboard-excalidraw","title":"Whiteboard (Excalidraw)","text":"<p>Collaborative whiteboard for brainstorming and campaign planning.</p> <ul> <li>Real-time collaboration \u2014 multi-user drawing with WebSocket support</li> <li>Embedded in admin \u2014 full-screen iframe at <code>/app/services/excalidraw</code></li> <li>Desktop only \u2014 requires a desktop browser for the drawing experience</li> <li>Route: <code>/app/services/excalidraw</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#git-hosting-gitea","title":"Git Hosting (Gitea)","text":"<p>Self-hosted Git repository hosting for campaign code and configuration.</p> <ul> <li>Lightweight Git forge \u2014 repositories, issues, pull requests, and wikis</li> <li>User provisioning \u2014 platform users can be auto-provisioned as Gitea accounts</li> <li>Embedded \u2014 accessible at <code>git.DOMAIN</code> or embedded in admin at <code>/app/services/gitea</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#workflow-automation-n8n","title":"Workflow Automation (n8n)","text":"<p>Self-hosted workflow automation for connecting platform events to external services.</p> <ul> <li>Visual workflow editor \u2014 drag-and-drop automation builder</li> <li>Webhook triggers \u2014 respond to platform events</li> <li>Embedded \u2014 accessible at <code>n8n.DOMAIN</code> or embedded in admin at <code>/app/services/n8n</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/integrations/#qr-code-generator-mini-qr","title":"QR Code Generator (Mini QR)","text":"<p>Built-in QR code generation for walk sheets, volunteer invites, and campaign links.</p> <ul> <li>Public API \u2014 QR code PNG generation at <code>/api/qr</code></li> <li>Walk sheet integration \u2014 QR codes embedded in printable walk sheets</li> <li>Volunteer quick join \u2014 QR codes for instant volunteer onboarding</li> <li>Embedded \u2014 admin interface at <code>/app/services/qr</code></li> </ul>","tags":["guide","admin","services","integrations"]},{"location":"docs/admin/services/monitoring/","title":"Monitoring","text":"<p>The monitoring stack runs as a Docker Compose profile and provides metrics collection, visualization, and alerting.</p> <p></p>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#starting-the-stack","title":"Starting the Stack","text":"<pre><code>docker compose --profile monitoring up -d\n</code></pre> <p>This starts Prometheus, Grafana, Alertmanager, cAdvisor, Node Exporter, and Redis Exporter.</p>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#custom-metrics","title":"Custom Metrics","text":"<p>The platform exposes 12 custom <code>cm_*</code> Prometheus metrics:</p> <ul> <li>API request rates and latencies</li> <li>BullMQ queue sizes (email, SMS, video scheduling)</li> <li>Active canvass sessions</li> <li>External service health gauges</li> </ul>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#grafana-dashboards","title":"Grafana Dashboards","text":"<p>Three pre-configured dashboards auto-provisioned from <code>configs/grafana/</code>:</p> <ul> <li>API Performance \u2014 request rates, latencies, error rates</li> <li>Application Overview \u2014 queue sizes, active sessions, service health</li> <li>System Health \u2014 container resources, host metrics, Redis stats</li> </ul>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#alertmanager","title":"Alertmanager","text":"<p>Alert rules in <code>configs/prometheus/alerts.yml</code> cover:</p> <ul> <li>API downtime and high error rates</li> <li>Queue backlogs</li> <li>Service connectivity failures</li> <li>Resource utilization thresholds</li> </ul>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/observability</code> \u2014 embedded Grafana dashboards, alert status, and service health (3 tabs)</li> </ul>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/monitoring/#direct-access","title":"Direct Access","text":"<ul> <li>Grafana: <code>localhost:3001</code> or <code>grafana.DOMAIN</code></li> <li>Prometheus: <code>localhost:9090</code></li> <li>Alertmanager: <code>localhost:9093</code></li> </ul>","tags":["guide","admin","services","monitoring"]},{"location":"docs/admin/services/tunnel/","title":"Tunnel (Pangolin)","text":"<p>Pangolin provides secure tunneling to expose your self-hosted services to the internet without port forwarding or a static IP.</p> <p></p>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/tunnel/#setup","title":"Setup","text":"<p>From <code>/app/pangolin</code>:</p> <ul> <li>Automated setup \u2014 one-command deployment that creates the Pangolin site, updates <code>.env</code> with credentials, and restarts the Newt tunnel container</li> <li>Manual setup \u2014 step-by-step instructions for connecting to an existing Pangolin instance</li> </ul>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/tunnel/#resource-management","title":"Resource Management","text":"<p>The platform defines 12+ service resources in <code>configs/pangolin/resources.yml</code>:</p> <ul> <li>Each resource maps a subdomain (e.g., <code>api.DOMAIN</code>, <code>app.DOMAIN</code>) to an internal service</li> <li>Hourly sync \u2014 nginx cron job pushes resource definitions to Pangolin automatically</li> <li>Status dashboard \u2014 view tunnel connection status and resource health</li> </ul>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/tunnel/#newt-container","title":"Newt Container","text":"<p>The Newt container runs alongside nginx and tunnels traffic to your services:</p> <ul> <li>Configured via <code>PANGOLIN_NEWT_ID</code> and <code>PANGOLIN_NEWT_SECRET</code> environment variables</li> <li>Depends on nginx (all resources route through <code>nginx:80</code>)</li> <li>Auto-restarts on failure</li> </ul>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/tunnel/#security","title":"Security","text":"<p>The Pangolin server runs CrowdSec for intrusion detection with a web management UI protected by Tinyauth forward-auth. See CrowdSec &amp; Security for details on:</p> <ul> <li>CrowdSec Manager dashboard (<code>crowdsec.bnkserve.org</code>)</li> <li>Tinyauth authentication (<code>auth.bnkserve.org</code>)</li> <li>Canadian ISP whitelisting and crawl detection tuning</li> <li>Cloudflare Turnstile captcha integration</li> </ul>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/tunnel/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/pangolin</code> \u2014 tunnel status, setup wizard, and resource management</li> </ul>","tags":["guide","admin","services","networking"]},{"location":"docs/admin/services/user-provisioning/","title":"User Provisioning","text":"<p>Automatically create and sync user accounts across integrated services when new platform users are registered. Enable with <code>enableUserProvisioning</code> in Settings.</p>","tags":["guide","admin","services"]},{"location":"docs/admin/services/user-provisioning/#supported-services","title":"Supported Services","text":"Service Mode Notes Rocket.Chat Always lazy SSO on first access Gitea Eager or lazy Admin API provisioning Vaultwarden Eager or lazy Invite-based (no password management) Listmonk Eager or lazy Subscriber sync","tags":["guide","admin","services"]},{"location":"docs/admin/services/user-provisioning/#configuration","title":"Configuration","text":"<p>From Settings &gt; User Provisioning:</p> <ul> <li>Toggle provisioning per service</li> <li>Choose eager (create immediately on user registration) or lazy (create on first access)</li> <li>View provisioning status per user in the Service Accounts panel on the Users page</li> </ul>","tags":["guide","admin","services"]},{"location":"docs/admin/services/user-provisioning/#bulk-sync","title":"Bulk Sync","text":"<p>Trigger a bulk sync from <code>/api/users/provisioning/sync</code> to provision all existing users across enabled services. Useful after enabling a new service.</p>","tags":["guide","admin","services"]},{"location":"docs/admin/services/user-provisioning/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/users</code> (edit drawer) \u2014 per-user service account status and actions</li> <li><code>/app/settings</code> (User Provisioning tab) \u2014 per-service toggle and timing</li> </ul>","tags":["guide","admin","services"]},{"location":"docs/admin/web/","title":"Web Content","text":"<p>Manage the public-facing web presence \u2014 landing pages, the dynamic homepage, navigation menu, and documentation site.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/#in-this-section","title":"In This Section","text":"<ul> <li>Landing Pages \u2014 build campaign microsites with the GrapesJS drag-and-drop editor</li> <li>Homepage \u2014 dynamic public landing page aggregating campaigns, shifts, media, and events</li> <li>Navigation \u2014 customize the public navigation menu with toggles, custom links, and reordering</li> <li>Documentation \u2014 MkDocs site management, page analytics, and comment moderation</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/#social-sharing","title":"Social Sharing","text":"<p>All public content (campaigns, landing pages, gallery videos) automatically generates Open Graph and Twitter Card meta tags for rich link previews when shared on social media, messaging apps, and search engines. OG responses are cached in Redis for 10 minutes.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/documentation/","title":"Documentation","text":"<p>Manage the MkDocs documentation site, track page engagement, and moderate visitor comments.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/documentation/#mkdocs-management","title":"MkDocs Management","text":"<p>From Docs (<code>/app/docs</code>):</p> <ul> <li>View MkDocs build status and health</li> <li>Browse the documentation file tree</li> <li>Export landing pages to MkDocs as Jinja2 Material theme overrides</li> <li>Configure documentation settings from MkDocs Settings (<code>/app/docs/settings</code>)</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/documentation/#documentation-analytics","title":"Documentation Analytics","text":"<p>Track how visitors interact with documentation pages using the MkDocs Material theme's custom analytics provider and <code>navigation.tracking</code>.</p> <ul> <li>Navigation tracking \u2014 updates the browser URL as users scroll through sections, enabling section-level engagement tracking</li> <li>Custom provider \u2014 integrates with any third-party analytics tool (Plausible, Umami, Google Analytics) via template overrides in <code>docs/overrides/</code></li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/documentation/#comments","title":"Comments","text":"<p>Visitors can leave comments on documentation pages using a Gitea-backed comment system.</p> <ul> <li>Anonymous posting \u2014 visitors can comment without creating an account</li> <li>Gitea-backed \u2014 comments stored as Gitea issues (one issue per page) for version control and searchability</li> <li>Moderation \u2014 admin panel at <code>/app/docs-comments</code> for approving, hiding, or deleting comments</li> <li>OAuth login \u2014 optional Gitea OAuth for authenticated commenting</li> <li>Per-page threads \u2014 each documentation page gets its own comment thread</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/documentation/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/docs</code> \u2014 MkDocs management (file tree, config, build triggers)</li> <li><code>/app/docs/settings</code> \u2014 documentation configuration</li> <li><code>/app/docs-comments</code> \u2014 moderate documentation comments</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/homepage/","title":"Public Homepage","text":"<p>A dynamic public landing page that showcases your organization and aggregates content from across the platform.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/homepage/#sections","title":"Sections","text":"<p>The homepage assembles its content from enabled modules. Sections are only displayed when their corresponding module is active and has data to show.</p> <ul> <li>Hero banner \u2014 organization name, logo, tagline (configurable via Settings), and call-to-action buttons for campaigns and volunteer signups</li> <li>Stats counters \u2014 active campaigns, total emails sent, and volunteer signups (shown only when counts are greater than zero)</li> <li>Featured campaigns \u2014 up to 3 active campaigns, sorted by highlight status then creation date, with email counts and descriptions</li> <li>Upcoming shifts \u2014 up to 3 open shifts with date, time, location, and spots remaining</li> <li>Latest videos \u2014 up to 4 recently published videos from the media library, displayed in a horizontal scroll strip with thumbnails and durations</li> <li>Upcoming events \u2014 up to 3 future events from Gancio with date, location, and tags</li> <li>Recent activity \u2014 a compact activity feed showing the latest platform actions</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/homepage/#data-caching","title":"Data &amp; Caching","text":"<p>All homepage data is fetched from a single API endpoint (<code>/api/homepage</code>) and cached in Redis for 2 minutes. Individual section queries use <code>Promise.allSettled</code> so that a failure in one module (e.g., Gancio being offline) does not prevent the rest of the page from loading.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/homepage/#configuration","title":"Configuration","text":"<ul> <li>Organization name and logo \u2014 set via Settings &gt; Organization</li> <li>Homepage tagline \u2014 set via the <code>homepageTagline</code> field in site settings</li> <li>Module visibility \u2014 controlled by feature flags (enableInfluence, enableMap, enableMediaFeatures, enableEvents, enablePayments, enableLandingPages)</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/homepage/#public-routes","title":"Public Routes","text":"<ul> <li><code>/home</code> \u2014 public homepage</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/landing-pages/","title":"Landing Pages","text":"<p>Build campaign microsites with a drag-and-drop visual editor.</p> <p></p>","tags":["guide","admin","content","landing-pages"]},{"location":"docs/admin/web/landing-pages/#how-it-works","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>","tags":["guide","admin","content","landing-pages"]},{"location":"docs/admin/web/landing-pages/#admin-routes","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>","tags":["guide","admin","content","landing-pages"]},{"location":"docs/admin/web/landing-pages/#public-routes","title":"Public Routes","text":"<ul> <li><code>/p/:slug</code> \u2014 view a published landing page</li> </ul>","tags":["guide","admin","content","landing-pages"]},{"location":"docs/admin/web/navigation/","title":"Navigation Settings","text":"<p>Customize the public-facing navigation menu from the admin panel. The navigation bar appears on all public pages, the admin header, the Gancio events page, and the MkDocs documentation site.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/navigation/#key-features","title":"Key Features","text":"<ul> <li>Per-item toggle -- enable or disable each navigation item with a switch</li> <li>Custom links -- add external links or internal paths to the navigation</li> <li>Reorder -- move items up and down to arrange them in any order</li> <li>Editable labels and paths -- rename any item or change its destination</li> <li>Feature flag awareness -- builtin items tied to a feature flag (e.g., Campaigns requires <code>enableInfluence</code>) are automatically hidden when that feature is disabled</li> <li>Visitor control -- determine exactly what public visitors can access</li> </ul>","tags":["guide","admin","content"]},{"location":"docs/admin/web/navigation/#builtin-items","title":"Builtin Items","text":"<p>The platform ships with 11 builtin navigation items that cover the main public routes:</p> <p>Home, Campaigns, Map, Shifts, Events, Gallery, Pricing, Shop, Donate, Website (landing page), and Docs (documentation site).</p> <p>Each builtin item has a default icon and path. Some paths use special <code>$</code> tokens (e.g., <code>$landing</code>, <code>$docs</code>) that are automatically resolved to the correct external URL based on the deployment environment.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/navigation/#custom-links","title":"Custom Links","text":"<p>Add any number of custom links via the \"Add Custom Link\" button. Custom links support:</p> <ul> <li>Internal paths (e.g., <code>/blog</code>)</li> <li>External URLs (e.g., <code>https://example.com</code>) -- automatically detected and opened in a new tab</li> </ul> <p>Custom links can be deleted from the navigation; builtin items can only be toggled off.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/navigation/#mobile-handling","title":"Mobile Handling","text":"<p>On mobile devices, the navigation collapses into a hamburger menu that opens a full-height drawer. On desktop, the nav bar also supports a collapse mode that hides labels and shows only icons, toggled via a fold/unfold button. The collapse state is persisted in local storage.</p>","tags":["guide","admin","content"]},{"location":"docs/admin/web/navigation/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/navigation</code> -- navigation editor with per-item toggle, reorder, label editing, and custom link management</li> </ul>","tags":["guide","admin","content"]},{"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>","tags":["reference","developer","API"]},{"location":"docs/api/#authentication","title":"Authentication","text":"","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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.","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"location":"docs/api/#main-api-express-port-4000","title":"Main API (Express \u2014 Port 4000)","text":"","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>)","tags":["reference","developer","API"]},{"location":"docs/api/#campaigns","title":"Campaigns","text":"","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#responses","title":"Responses","text":"<p>Prefix: <code>/api/campaigns</code> (public) and <code>/api/responses</code> (admin + actions)</p>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#locations","title":"Locations","text":"<p>Prefix: <code>/api/map/locations</code></p>","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#shifts","title":"Shifts","text":"<p>Prefix: <code>/api/map/shifts</code></p>","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#canvassing","title":"Canvassing","text":"<p>Prefix: <code>/api/map/canvass</code></p>","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#gps-tracking","title":"GPS Tracking","text":"<p>Prefix: <code>/api/map/tracking</code></p>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>)","tags":["reference","developer","API"]},{"location":"docs/api/#landing-pages","title":"Landing Pages","text":"<p>Prefix: <code>/api/pages</code> and <code>/api/page-blocks</code></p>","tags":["reference","developer","API"]},{"location":"docs/api/#public_4","title":"Public","text":"Method Path Auth Description GET <code>/api/pages/:slug/view</code> Get published page by slug","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#payments","title":"Payments","text":"<p>Prefix: <code>/api/payments</code></p>","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"location":"docs/api/#health","title":"Health","text":"Method Path Auth Description GET <code>/health</code> Media API health check","tags":["reference","developer","API"]},{"location":"docs/api/#videos-admin","title":"Videos (Admin)","text":"<p>Prefix: <code>/api/videos</code> \u00b7 Auth: Admin roles</p>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"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>","tags":["reference","developer","API"]},{"location":"docs/api/#comments-chat","title":"Comments &amp; Chat","text":"","tags":["reference","developer","API"]},{"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)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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>)","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/api/#playlists","title":"Playlists","text":"","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"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","tags":["reference","developer","API"]},{"location":"docs/architecture/","title":"Architecture","text":"<p>Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database, a React single-page application, and Nginx for subdomain routing across 30+ services.</p>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#system-diagram","title":"System Diagram","text":"<pre><code>graph LR\n Browser[\"Browser\"] --&gt; Nginx[\"Nginx&lt;br/&gt;(reverse proxy)\"]\n Nginx --&gt; Admin[\"React Admin GUI&lt;br/&gt;port 3000\"]\n Nginx --&gt; API[\"Express API&lt;br/&gt;port 4000\"]\n Nginx --&gt; MediaAPI[\"Fastify Media API&lt;br/&gt;port 4100\"]\n Nginx --&gt; MkDocs[\"MkDocs&lt;br/&gt;port 4003/4004\"]\n Nginx --&gt; Services[\"Other Services&lt;br/&gt;(Gitea, NocoDB, etc.)\"]\n\n API --&gt; PostgreSQL[(\"PostgreSQL 16&lt;br/&gt;30+ tables\")]\n MediaAPI --&gt; PostgreSQL\n API --&gt; Redis[(\"Redis&lt;br/&gt;cache + queues\")]\n API --&gt; BullMQ[\"BullMQ&lt;br/&gt;(email, video jobs)\"]\n BullMQ --&gt; Redis\n\n subgraph Tunnel [\"Public Access\"]\n Newt[\"Newt Client\"] --&gt; Pangolin[\"Pangolin Server\"]\n end\n Newt --&gt; Nginx</code></pre>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#key-components","title":"Key Components","text":"Component Technology Role Main API Express.js + TypeScript + Prisma Auth, campaigns, map, shifts, pages, canvassing, email Media API Fastify + TypeScript + Prisma Video library, analytics, uploads, scheduling Admin GUI React 19 + Vite + Ant Design + Zustand Admin dashboard, public pages, volunteer portal, media gallery Database PostgreSQL 16 Shared by both APIs (30+ models via Prisma) Cache Redis 7 Rate limiting, BullMQ job queues, geocoding cache Proxy Nginx Subdomain routing, security headers, WebSocket upgrade Tunnel Pangolin + Newt Expose services without port forwarding Monitoring Prometheus + Grafana + Alertmanager Metrics collection, dashboards, alerting","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#dual-api-design","title":"Dual API Design","text":"<p>The platform runs two independent API servers sharing one PostgreSQL database:</p> Express API (port 4000)Fastify Media API (port 4100) <p>The main API handles all core platform logic:</p> <ul> <li>Authentication \u2014 JWT access/refresh tokens, RBAC middleware</li> <li>Modules \u2014 Influence (campaigns, responses), Map (locations, cuts, shifts, canvassing), Pages, Email Templates, Settings, Users, Payments, Social, Calendar</li> <li>Services \u2014 Email queue (BullMQ), geocoding queue, Listmonk sync, Pangolin client, user provisioning</li> <li>ORM \u2014 Prisma with 30+ models and migration history</li> </ul> <p>A separate server optimized for media handling:</p> <ul> <li>Video CRUD \u2014 Upload with FFprobe metadata extraction</li> <li>Scheduled Publishing \u2014 BullMQ queue with timezone support</li> <li>Analytics \u2014 View tracking, watch time, completion rates (GDPR-compliant)</li> <li>Public Gallery \u2014 Playlists, reactions, comments, SSE chat</li> <li>ORM \u2014 Prisma (migrated from Drizzle, Feb 2026)</li> </ul> <p>Both servers connect to the same database and share the same Prisma schema. This separation allows the media API to handle large file uploads and streaming independently from the main API's request/response cycle.</p>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#authentication-flow","title":"Authentication Flow","text":"<pre><code>sequenceDiagram\n participant Client\n participant API\n participant DB\n participant Redis\n\n Client-&gt;&gt;API: POST /api/auth/login {email, password}\n API-&gt;&gt;Redis: Check rate limit (10/min per IP)\n Redis--&gt;&gt;API: OK\n API-&gt;&gt;DB: Verify bcrypt password\n DB--&gt;&gt;API: User record\n API-&gt;&gt;DB: Create refresh token\n API--&gt;&gt;Client: {accessToken (15min), refreshToken (7d)}\n\n Note over Client: Authenticated requests\n Client-&gt;&gt;API: GET /api/campaigns&lt;br/&gt;Authorization: Bearer &lt;accessToken&gt;\n API-&gt;&gt;API: Verify JWT + check role (RBAC)\n API--&gt;&gt;Client: 200 OK\n\n Note over Client: Token expired\n Client-&gt;&gt;API: POST /api/auth/refresh {refreshToken}\n API-&gt;&gt;DB: Atomic rotation (delete old, create new)\n API--&gt;&gt;Client: {new accessToken, new refreshToken}</code></pre>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#security-features","title":"Security Features","text":"<ul> <li>Password policy \u2014 12+ characters, uppercase, lowercase, digit (schema-enforced)</li> <li>Refresh token rotation \u2014 Atomic Prisma transaction prevents race conditions</li> <li>User enumeration prevention \u2014 Returns 401 (not 404) for missing users</li> <li>Rate limiting \u2014 10 requests/minute on auth endpoints via Redis</li> <li>11 roles \u2014 <code>SUPER_ADMIN</code> (implicit bypass), 8 module-specific admin roles, <code>USER</code>, <code>TEMP</code></li> <li>Encryption \u2014 AES-256-GCM for sensitive DB fields (<code>ENCRYPTION_KEY</code> env var)</li> </ul>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#request-lifecycle","title":"Request Lifecycle","text":"<pre><code>graph TD\n A[\"Incoming Request\"] --&gt; B[\"Nginx\"]\n B --&gt;|\"Host: api.domain\"| C[\"Express API\"]\n B --&gt;|\"Host: media.domain\"| D[\"Fastify Media API\"]\n B --&gt;|\"Host: app.domain\"| E[\"React Admin GUI\"]\n C --&gt; F[\"Rate Limiter (Redis)\"]\n F --&gt; G[\"Auth Middleware (JWT)\"]\n G --&gt; H[\"Role Check (RBAC)\"]\n H --&gt; I[\"Validation (Zod)\"]\n I --&gt; J[\"Route Handler\"]\n J --&gt; K[\"Service Layer\"]\n K --&gt; L[\"Prisma ORM\"]\n L --&gt; M[(\"PostgreSQL\")]\n J --&gt; N[\"Response + Metrics\"]</code></pre>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#database-schema","title":"Database Schema","text":"<p>The database contains 30+ Prisma models organized by module:</p> Module Key Models Auth <code>User</code>, <code>RefreshToken</code> Influence <code>Campaign</code>, <code>CampaignEmail</code>, <code>CampaignResponse</code>, <code>Representative</code>, <code>PostalCode</code> Map <code>Location</code>, <code>Address</code>, <code>Cut</code>, <code>Shift</code>, <code>ShiftSignup</code> Canvass <code>CanvassSession</code>, <code>CanvassVisit</code>, <code>TrackingSession</code>, <code>TrackingPoint</code> Pages <code>Page</code>, <code>PageBlock</code>, <code>EmailTemplate</code> Media <code>Video</code>, <code>VideoReaction</code>, <code>VideoComment</code>, <code>VideoView</code>, <code>Playlist</code>, <code>PlaylistVideo</code> Payments <code>StripeProduct</code>, <code>StripePrice</code>, <code>StripeDonationPage</code>, <code>StripeOrder</code> Social <code>Friendship</code>, <code>SocialNotification</code>, <code>CalendarLayer</code>, <code>CalendarItem</code> SMS <code>SmsContactList</code>, <code>SmsCampaign</code>, <code>SmsMessage</code>, <code>SmsConversation</code> People <code>Contact</code>, <code>ContactAddress</code>, <code>ContactEmail</code>, <code>ContactPhone</code>, <code>ContactConnection</code> Settings <code>SiteSettings</code>, <code>MapSettings</code>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#docker-compose-architecture","title":"Docker Compose Architecture","text":"<p>Services are organized into categories with dependency management:</p> <pre><code>graph TD\n subgraph Core [\"Core (always started)\"]\n PG[\"PostgreSQL\"] --&gt; API[\"Express API\"]\n Redis --&gt; API\n PG --&gt; Media[\"Fastify Media API\"]\n API --&gt; Admin[\"React Admin\"]\n Admin --&gt; Nginx\n API --&gt; Nginx\n Media --&gt; Nginx\n end\n\n subgraph Communication [\"Communication (optional)\"]\n RC[\"Rocket.Chat\"] --&gt; MongoDB\n Jitsi[\"Jitsi Meet (4 containers)\"]\n Gancio[\"Gancio Events\"]\n end\n\n subgraph Monitoring [\"Monitoring (profile)\"]\n Prometheus --&gt; Grafana\n Prometheus --&gt; Alertmanager\n cAdvisor --&gt; Prometheus\n NodeExporter --&gt; Prometheus\n end\n\n subgraph Tunnel [\"Tunnel\"]\n Newt --&gt; Nginx\n end</code></pre> <p>Docker healthchecks ensure proper startup order: PostgreSQL and Redis must be healthy before the API starts. The API runs migrations and seeding automatically via its entrypoint script.</p>","tags":["reference","developer","architecture"]},{"location":"docs/architecture/#subdomain-routing","title":"Subdomain Routing","text":"<p>Nginx routes requests based on the <code>Host</code> header. All services run on the <code>changemaker-lite</code> Docker bridge network.</p> Pattern Target <code>app.DOMAIN</code> Admin GUI (admin + public + volunteer + gallery) <code>api.DOMAIN</code> Express API <code>media.DOMAIN</code> Fastify Media API <code>DOMAIN</code> (root) MkDocs static site <code>*.DOMAIN</code> 15+ additional service subdomains <p>See Services for the complete subdomain table.</p>","tags":["reference","developer","architecture"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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 4004 <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>vault.DOMAIN</code> Vaultwarden password manager 8445 <code>chat.DOMAIN</code> Rocket.Chat team chat \u2014 <code>events.DOMAIN</code> Gancio event management 8092 <code>grafana.DOMAIN</code> Monitoring dashboards 3005","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#exposure-methods","title":"Exposure Methods","text":"","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#production-checklist","title":"Production Checklist","text":"<p>Before going live, verify each item:</p>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#backups_1","title":"Backups","text":"<p>The included backup script dumps PostgreSQL databases, archives uploads, and optionally uploads to S3.</p>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#monitoring","title":"Monitoring","text":"<p>The monitoring stack runs behind a Docker Compose profile and is not started by default.</p>","tags":["guide","operator","deployment","docker"]},{"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 3005 Dashboards and visualization Alertmanager 9093 Alert routing and notifications cAdvisor 8086 Container resource metrics Node Exporter 9100 Host system metrics Redis Exporter 9121 Redis metrics Gotify 8889 Push notifications","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#upgrading","title":"Upgrading","text":"","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#pulling-updates","title":"Pulling Updates","text":"<pre><code># Pull latest code\ngit pull origin main\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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/deployment/#troubleshooting-production-issues","title":"Troubleshooting Production Issues","text":"","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"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>","tags":["guide","operator","deployment","docker"]},{"location":"docs/getting-started/","title":"Getting Started","text":"<p>Changemaker Lite is a self-hosted campaign platform that runs entirely on Docker. This guide takes you from zero to a working deployment \u2014 whether you're evaluating locally or launching for a live campaign.</p> <p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#what-youll-need","title":"What You'll Need","text":"<p>Before deploying, gather the essentials. The requirements differ depending on whether you're running a quick local test or a production deployment serving real users.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#development-local-testing","title":"Development (Local Testing)","text":"<p>All you need is Docker \u2014 no domain, tunnel, or SMTP required:</p> <ul> <li> <p> Docker 24+ &amp; Compose v2</p> <p>The only hard requirement. All 30+ services run as containers.</p> </li> <li> <p> OpenSSL</p> <p>Used by the config wizard to auto-generate 21 secrets (JWT, encryption keys, passwords).</p> </li> <li> <p> 2 GB RAM / 10 GB Disk</p> <p>Minimum for core services. 4 GB recommended if enabling media, chat, or monitoring.</p> </li> </ul> <p>MailHog captures all emails locally</p> <p>In dev mode, every outbound email is caught by MailHog at <code>http://localhost:8025</code> \u2014 no SMTP provider needed.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#production-deployment","title":"Production Deployment","text":"<p>For a deployment that serves real users, you'll also need:</p> <ul> <li> <p> A Domain Name</p> <p>Changemaker Lite uses subdomain routing \u2014 <code>app.</code>, <code>api.</code>, <code>docs.</code>, <code>git.</code>, and 10+ more subdomains are created automatically. Wildcard DNS (<code>*.yourdomain.org</code>) is the simplest approach.</p> <p>Budget ~$10\u201315/year from any registrar.</p> </li> <li> <p> A Reverse Tunnel or Public IP</p> <p>Your server needs to be reachable from the internet. Built-in support for Pangolin \u2014 a self-hosted tunnel with SSL, subdomain routing, and access control. The admin dashboard includes a one-click setup wizard.</p> <p>Alternatives: Cloudflare Tunnel, a VPS with public IP, or any reverse proxy with SSL.</p> </li> <li> <p> SMTP Email Provider</p> <p>Campaign messages, password resets, volunteer invitations, and newsletters all need real email delivery.</p> <p>Recommended: Proton Mail (privacy-focused), Mailgun (100/day free), Amazon SES (cheapest at scale), or Brevo (300/day free).</p> </li> <li> <p> A Linux Server</p> <p>Any Linux with Docker. Ubuntu 22.04+ LTS recommended. A VPS from DigitalOcean, Hetzner, or Linode works great \u2014 or a spare machine on your network if using a tunnel.</p> </li> </ul> Optional services that enhance your deployment <p>These aren't required but unlock additional features:</p> Service Purpose Free Tier Stripe Donations, merchandise, membership plans Free account, pay-as-you-go Mapbox or Google Maps Better geocoding accuracy for mapping 100K req/mo (Mapbox) or $200/mo credit (Google) MaxMind GeoLite2 Geographic analytics (visitor locations) Free account Android phone + Termux SMS campaigns via physical phone gateway Free \u2014 no third-party SMS costs Public IP + UDP 10000 Jitsi self-hosted video conferencing Free (requires open firewall port) <p>See Prerequisites &amp; External Services for setup details on each.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#quick-install","title":"Quick Install","text":"","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#pre-built-images-recommended","title":"Pre-built Images (Recommended)","text":"<p>The fastest path \u2014 no source code, no compilation:</p> <pre><code>curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash\n</code></pre> <p>This downloads a lightweight release package (~2 MB), runs the 14-step configuration wizard, and pulls pre-built Docker images. First startup takes ~2 minutes.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#from-source-development","title":"From Source (Development)","text":"<p>For development or customization:</p> <pre><code>git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\n</code></pre> <pre><code>bash config.sh\n</code></pre> <pre><code>docker compose up -d\n</code></pre> <p>Open http://localhost:3000 and sign in with the admin credentials you configured. The API container automatically runs database migrations and seeding on first startup.</p> <p>Change your password</p> <p>If you used the wizard's generated password, change it immediately from the admin dashboard.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#configuration-wizard","title":"Configuration Wizard","text":"<p>The <code>config.sh</code> wizard produces a fully populated <code>.env</code> file in 14 steps:</p> Step What It Does 1. Prerequisites Verifies Docker, Docker Compose, and OpenSSL 2. Environment file Creates <code>.env</code> from <code>.env.example</code> (backs up existing) 3. Domain Sets root domain + 14 derived variables, updates mkdocs.yml 4. Admin credentials Email + password (enforces 12+ chars, mixed case, digit) 5. Secrets Auto-generates 21 unique secrets (JWT, encryption, database, service passwords) 6. Email MailHog (dev) or production SMTP, optionally shared with Listmonk 7. Feature flags 9 toggles: Media, Listmonk, Payments, Chat, Events, Meet, SMS, Docs Comments, Bunker Ops 8. Tunnel Pangolin credentials for secure public access 9. CORS Auto-calculated allowed origins from domain 10. Nginx Renders <code>.conf.template</code> files with domain substitution 11. Homepage Generates <code>services.yaml</code> with 27 service entries 12. Permissions Creates 12 directories with container-friendly permissions 13. Upgrade watcher Installs systemd units for GUI-triggered upgrades (optional, requires sudo) 14. Summary Displays configuration summary + next steps <p>See Installation for detailed documentation of each step.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#pre-installation-checklist","title":"Pre-Installation Checklist","text":"<p>Use this to make sure you're ready before running the installer:</p> <ul> <li> Docker 24+ and Docker Compose v2 installed</li> <li> OpenSSL installed (for secret generation)</li> <li> Domain name registered and DNS accessible</li> <li> DNS configured \u2014 wildcard <code>*.yourdomain.org</code> or individual subdomains pointing to your tunnel/server</li> <li> Tunnel or public IP \u2014 Pangolin credentials (API key + Org ID), or server with public IP + SSL</li> <li> SMTP credentials \u2014 host, port, username, password from your email provider</li> <li> (Optional) Stripe account for payments</li> <li> (Optional) Mapbox or Google Maps API key for geocoding</li> <li> (Optional) MaxMind account for geographic analytics</li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#services-at-a-glance","title":"Services at a Glance","text":"<p>Changemaker Lite includes 30+ Docker services organized into 8 categories:</p> Category Services Startup Core API, Admin, PostgreSQL, Redis, Nginx <code>docker compose up -d v2-postgres redis api admin nginx</code> Media Fastify media API <code>docker compose up -d media-api</code> Communication Rocket.Chat, Gancio, Jitsi Meet Individual <code>docker compose up -d</code> commands Newsletter &amp; Email Listmonk, MailHog <code>docker compose up -d listmonk-app</code> Developer Tools Code Server, MkDocs, Gitea, NocoDB, n8n Individual <code>docker compose up -d</code> commands Utilities Mini QR, Excalidraw, Vaultwarden, Homepage <code>docker compose up -d mini-qr excalidraw vaultwarden homepage</code> Monitoring Prometheus, Grafana, Alertmanager, exporters <code>docker compose --profile monitoring up -d</code> Infrastructure Newt tunnel, Docker socket proxy Auto-starts with tunnel configuration <p>See Services Overview for the complete catalog with ports, feature flags, and detailed descriptions.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/#next-steps","title":"Next Steps","text":"<ul> <li> <p> Prerequisites</p> <p>Full details on every external service \u2014 domain setup, SMTP providers, tunnel options, and optional integrations.</p> </li> <li> <p> Installation</p> <p>Detailed setup walkthrough, manual configuration, and the full config wizard reference.</p> </li> <li> <p> Services Overview</p> <p>Complete catalog of 30+ containers with ports, feature flags, and descriptions.</p> </li> <li> <p> Environment Variables</p> <p>Complete <code>.env</code> reference for every setting.</p> </li> <li> <p> First Steps</p> <p>Create your first campaign, add locations to the map, and invite volunteers.</p> </li> <li> <p> Updates &amp; Upgrades</p> <p>Keep your installation current with zero-downtime upgrades.</p> </li> <li> <p> Control Panel</p> <p>Manage multiple Changemaker Lite instances from a single dashboard.</p> </li> <li> <p> Features at a Glance</p> <p>Visual overview of every module \u2014 advocacy, mapping, media, payments, and more.</p> </li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/control-panel/","title":"Changemaker Control Panel (CCP)","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>The Changemaker Control Panel is a multi-tenant management layer for operators who run multiple Changemaker Lite instances from a single server. It provides a web UI to provision, monitor, and maintain a fleet of instances without manual configuration.</p> <p>Single instance?</p> <p>If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with First Steps.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#when-to-use-ccp","title":"When to Use CCP","text":"<p>CCP is designed for:</p> <ul> <li>Campaign organizations managing instances for multiple chapters or regions</li> <li>Hosting providers offering Changemaker Lite as a managed service</li> <li>Development teams spinning up isolated test instances</li> </ul> <p>CCP handles the entire instance lifecycle: provisioning, configuration, health monitoring, backups, and upgrades \u2014 all from a single dashboard.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#architecture","title":"Architecture","text":"<p>CCP runs as 4 Docker containers alongside (but independent from) your CML instances:</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CCP Admin GUI (5100) \u2502 React + Vite + Ant Design\n\u2502 Dark theme, SPA \u2502 Zustand auth store\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CCP API (5000) \u2502 Express + TypeScript\n\u2502 JWT auth, RBAC \u2502 Prisma ORM \u2192 PostgreSQL\n\u2502 Docker socket access \u2502 Winston logger\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25bc \u25bc \u25bc\nccp-postgres ccp-redis Docker Socket\n(port 5480) (port 6399)\n</code></pre> Service Container Port Description CCP API <code>ccp-api</code> 5000 Express API with Docker CLI access CCP Admin <code>ccp-admin</code> 5100 React admin GUI CCP PostgreSQL <code>ccp-postgres</code> 5480 CCP metadata database CCP Redis <code>ccp-redis</code> 6399 Rate limiting, caching <p>Each managed CML instance gets its own isolated set of containers and PostgreSQL database, with ports allocated from non-overlapping ranges.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#setup","title":"Setup","text":"","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#1-run-the-setup-script","title":"1. Run the Setup Script","text":"<pre><code>cd changemaker-control-panel\nchmod +x setup.sh\n./setup.sh\n</code></pre> <p>The setup script:</p> <ul> <li>Detects the installation directory and resolves absolute paths</li> <li>Creates <code>instances/</code> and <code>backups/</code> directories</li> <li>Copies <code>.env.example</code> to <code>.env</code> if not present</li> <li>Sets <code>INSTANCES_BASE_PATH</code>, <code>BACKUP_STORAGE_PATH</code>, and <code>CML_SOURCE_PATH</code></li> <li>Generates random secrets for any placeholder values</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#2-review-environment","title":"2. Review Environment","text":"<p>Edit <code>.env</code> and verify the key settings:</p> Variable Default Description <code>JWT_ACCESS_SECRET</code> Auto-generated JWT signing key <code>JWT_REFRESH_SECRET</code> Auto-generated Refresh token signing key <code>ENCRYPTION_KEY</code> Auto-generated AES-256 key for instance secrets at rest <code>INITIAL_ADMIN_EMAIL</code> <code>admin@example.com</code> Bootstrap admin email <code>INITIAL_ADMIN_PASSWORD</code> <code>ChangeMe2025!!</code> Bootstrap admin password <code>INSTANCES_BASE_PATH</code> <code>./instances</code> Where instance directories are created <code>CML_SOURCE_PATH</code> Auto-detected Path to CML source repo for provisioning <code>BACKUP_STORAGE_PATH</code> <code>./backups</code> Backup archive storage <code>PANGOLIN_API_URL</code> \u2014 Pangolin API for tunnel management <code>PANGOLIN_API_KEY</code> \u2014 Pangolin authentication <code>PANGOLIN_ORG_ID</code> \u2014 Pangolin organization","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#3-start-ccp","title":"3. Start CCP","text":"<pre><code>docker compose up -d\n\n# Run database migrations and seed the admin user\ndocker compose exec ccp-api npx prisma migrate deploy\ndocker compose exec ccp-api npx prisma db seed\n</code></pre>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#4-log-in","title":"4. Log In","text":"<p>Open http://localhost:5100 and sign in with the admin credentials from <code>.env</code>.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#creating-an-instance","title":"Creating an Instance","text":"<p>The Create Instance wizard walks through 5 steps:</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#step-1-basic-information","title":"Step 1: Basic Information","text":"<ul> <li>Instance name \u2014 human-readable label (e.g., \"Edmonton Chapter\")</li> <li>Slug \u2014 URL-safe identifier (e.g., <code>edmonton</code>), used for directory names and compose project</li> <li>Domain \u2014 the domain this instance will serve (e.g., <code>edmonton.example.org</code>)</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#step-2-features","title":"Step 2: Features","text":"<p>Toggle which platform features to enable for this instance:</p> <ul> <li>Media Manager</li> <li>Listmonk newsletter sync</li> <li>Payments</li> <li>Rocket.Chat</li> <li>Gancio events</li> <li>Jitsi Meet</li> <li>SMS Campaigns</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#step-3-email","title":"Step 3: Email","text":"<p>Configure SMTP for the instance, or use MailHog for testing.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#step-4-tunnel","title":"Step 4: Tunnel","text":"<p>Optionally configure Pangolin tunnel credentials for public access.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#step-5-review","title":"Step 5: Review","text":"<p>Review all settings, then click Create to start provisioning.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#provisioning-flow","title":"Provisioning Flow","text":"<p>When you create an instance, CCP runs a 13-step async provisioning process:</p> Step What Happens 1 Validate uniqueness (slug + domain) 2 Allocate 4 ports from ranges 3 Generate 14 secrets (passwords, JWT keys, encryption keys) 4 Create Instance record (status: PROVISIONING) 5 Create instance directory 6 Copy CML source code (rsync, excluding node_modules/.git/.env) 7 Decrypt secrets and build template context 8 Render 7 config files from Handlebars templates (docker-compose.yml, .env, nginx configs, Pangolin, Prometheus) 9 Copy static files (nginx.conf) 10 <code>docker compose pull</code> (non-fatal if images are cached) 11 <code>docker compose build</code> 12 Start infrastructure (PostgreSQL + Redis), wait for healthy 13 Start API (runs migrations + seed), then start all remaining services <p>The admin GUI polls every 3 seconds during provisioning to show progress. When complete, the instance status changes to RUNNING.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#port-allocation","title":"Port Allocation","text":"<p>CCP allocates ports from 4 non-overlapping ranges to prevent conflicts between instances:</p> Range Start End Purpose API 14000 14999 Express API server Admin 13000 13999 React admin GUI PostgreSQL 15400 15499 Database Nginx 10000 10999 Reverse proxy <p>Each new instance receives one port from each range. Ports are tracked in the database and released when instances are deleted.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#pages-overview","title":"Pages Overview","text":"","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#dashboard","title":"Dashboard","text":"<p>At-a-glance fleet status:</p> <ul> <li>Total instances, running, healthy, degraded, stopped, error counts</li> <li>Instance cards with status indicators and quick actions</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#instance-list","title":"Instance List","text":"<p>Searchable, filterable table of all instances with status, domain, health, and creation date.</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#instance-detail","title":"Instance Detail","text":"<p>5-tab view for each instance:</p> Tab Content Overview Status, domain, ports, features, health summary Services Per-container status grid with restart and log-view actions Logs Real-time log viewer with service filter, tail count, and time range Backups Backup list with create, download, and delete actions Tunnel Pangolin tunnel status and configuration","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#backups","title":"Backups","text":"<p>Cross-instance backup management:</p> <ul> <li>All backups in one table with instance filter</li> <li>Stats: total count, total size, last backup time</li> <li>\"Backup All Running\" bulk action</li> <li>Download and delete individual archives</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#audit-log","title":"Audit Log","text":"<p>Filterable activity trail with 18 action types:</p> <ul> <li>Instance lifecycle: CREATE, UPDATE, DELETE, START, STOP, RESTART, UPGRADE</li> <li>Backups: CREATE, DELETE</li> <li>Tunnel: PANGOLIN_SETUP, PANGOLIN_SYNC</li> <li>Users: LOGIN, CREATE, UPDATE, DELETE</li> <li>Settings: UPDATE</li> </ul> <p>Each entry includes timestamp, user, action, instance, IP address, and details (expandable JSON).</p>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#settings","title":"Settings","text":"<p>CCP-level configuration:</p> <ul> <li>Port ranges</li> <li>Pangolin credentials</li> <li>Default feature flags for new instances</li> <li>Health check interval</li> <li>Backup retention period</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#roles","title":"Roles","text":"Role Capabilities SUPER_ADMIN Full access: create/delete instances, manage users, view secrets, delete backups OPERATOR Manage instances: create, start/stop/restart, backups, health checks VIEWER Read-only: view instances, logs, health, backups, audit log","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#security","title":"Security","text":"<ul> <li>JWT authentication with 15-minute access tokens and 7-day refresh tokens (atomic rotation)</li> <li>AES-256-GCM encryption for instance secrets stored in the database</li> <li>Audit logging on all operations with IP address capture</li> <li>Role-based access control on all API endpoints</li> <li>Docker socket access restricted to the CCP API container only</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/control-panel/#next-steps","title":"Next Steps","text":"<ul> <li>Services Overview \u2014 learn about the services CCP provisions for each instance</li> <li>Updates &amp; Upgrades \u2014 upgrading CML instances</li> <li>Deployment \u2014 production setup with tunneling and SSL</li> </ul>","tags":["guide","operator","multi-tenant"]},{"location":"docs/getting-started/environment-variables/","title":"Environment Variables","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <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>","tags":["reference","getting-started","operator","configuration"]},{"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","tags":["reference","getting-started","operator","configuration"]},{"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>.","tags":["reference","getting-started","operator","configuration"]},{"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>","tags":["reference","getting-started","operator","configuration"]},{"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_INVITE_SECRET</code> \u2014 Secret for signing volunteer invite tokens. Must differ from access and refresh secrets. Generate with <code>openssl rand -hex 32</code>. <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.","tags":["reference","getting-started","operator","configuration"]},{"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 all environments (no longer falls back to JWT secret in development).","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#security-extras","title":"Security Extras","text":"<p>Additional secrets for key separation. These fall back to <code>JWT_ACCESS_SECRET</code> if empty, but setting unique values is strongly recommended for production.</p> Variable Default Description <code>GITEA_SSO_SECRET</code> (empty) Cookie signing secret for Gitea SSO integration. Falls back to <code>JWT_ACCESS_SECRET</code> if empty. Generate with <code>openssl rand -hex 32</code>. <code>SERVICE_PASSWORD_SALT</code> (empty) Salt for deriving deterministic service passwords (Gitea, Rocket.Chat user provisioning). Falls back to <code>JWT_ACCESS_SECRET</code> if empty \u2014 rotating JWT_ACCESS_SECRET would then invalidate all provisioned service passwords. Generate with <code>openssl rand -hex 32</code>. <code>CSP_ENABLED</code> <code>false</code> Enable Content Security Policy headers in API responses. <p>Key separation</p> <p>If <code>GITEA_SSO_SECRET</code> and <code>SERVICE_PASSWORD_SALT</code> are left empty, the API logs security warnings on every startup. Set unique values to isolate secret rotation and prevent one compromised key from affecting other subsystems.</p>","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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>","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#rate-limiting","title":"Rate Limiting","text":"Variable Default Description <code>RATE_LIMIT_WINDOW_MS</code> <code>900000</code> Rate limit window in milliseconds (default: 15 minutes). <code>RATE_LIMIT_MAX</code> <code>500</code> Maximum requests per window per IP. Auth endpoints have a stricter limit (10/min).","tags":["reference","getting-started","operator","configuration"]},{"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).","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#payments-stripe","title":"Payments (Stripe)","text":"Variable Default Description <code>ENABLE_PAYMENTS</code> <code>false</code> Set to <code>true</code> to enable the payments feature (memberships, products, donations). Stripe API keys are stored encrypted in the database via the admin settings page.","tags":["reference","getting-started","operator","configuration"]},{"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>","tags":["reference","getting-started","operator","configuration"]},{"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>5434</code> Listmonk's own PostgreSQL port (separate from the main DB). Uses 5434 to avoid conflict with the main PostgreSQL (5432 internal / 5433 host). <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_WEBHOOK_SECRET</code> (empty) Shared secret for Listmonk webhook callbacks. <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.","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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>PUBLIC_MEDIA_PORT</code> <code>3100</code> Public media gallery server port. <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).","tags":["reference","getting-started","operator","configuration"]},{"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_URL</code> <code>http://gitea-changemaker:3000</code> Internal container URL for Gitea. <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. Gitea Docs Comments <p>Enable comments on MkDocs documentation pages, backed by Gitea Issues.</p> Variable Default Description <code>GITEA_COMMENTS_ENABLED</code> <code>false</code> Enable comments on MkDocs pages. <code>GITEA_API_TOKEN</code> (empty) Personal access token with repo write scope. Create in Gitea \u2192 Settings \u2192 Applications. <code>GITEA_COMMENTS_REPO_OWNER</code> (empty) Gitea username that owns the docs-comments repo. <code>GITEA_COMMENTS_REPO_NAME</code> <code>docs-comments</code> Repository name (auto-created via admin setup). <code>GITEA_OAUTH_CLIENT_ID</code> (empty) OAuth2 application client ID (create in Gitea \u2192 Settings \u2192 Applications \u2192 OAuth2). <code>GITEA_OAUTH_CLIENT_SECRET</code> (empty) OAuth2 application client secret.","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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>4004</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.","tags":["reference","getting-started","operator","configuration"]},{"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-changemaker:8443</code> Internal container URL.","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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).","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#vaultwarden-password-manager","title":"Vaultwarden (Password Manager)","text":"<p>Self-hosted Bitwarden-compatible password manager. Optional service.</p> Variable Default Description <code>VAULTWARDEN_PORT</code> <code>8445</code> Vaultwarden web UI port. <code>VAULTWARDEN_URL</code> <code>http://vaultwarden-changemaker:80</code> Internal container URL. <code>VAULTWARDEN_EMBED_PORT</code> <code>8890</code> Port for iframe embedding in admin. <code>VAULTWARDEN_ADMIN_TOKEN</code> (empty) Admin panel token (access at <code>/admin</code>). Generate with <code>openssl rand -hex 32</code>. <code>VAULTWARDEN_DOMAIN</code> <code>https://vault.cmlite.org</code> Public-facing URL. Must use HTTPS \u2014 Bitwarden web vault enforces HTTPS for account creation. Set to your Pangolin tunnel URL. <code>VAULTWARDEN_SIGNUPS_ALLOWED</code> <code>false</code> Allow new user self-registration. Keep <code>false</code> and use admin panel invites. <code>VAULTWARDEN_WEBSOCKET_ENABLED</code> <code>true</code> Enable WebSocket notifications for real-time sync. <code>VAULTWARDEN_SMTP_SECURITY</code> <code>off</code> SMTP security mode: <code>off</code> for MailHog, <code>starttls</code> or <code>force_tls</code> for production. Uses the main <code>SMTP_*</code> variables for host/credentials. <p>Initial setup</p> <p>The <code>vaultwarden-init</code> container automatically invites the <code>INITIAL_ADMIN_EMAIL</code> user when starting. Check MailHog (or your SMTP) for the invitation email.</p>","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#rocketchat-team-chat","title":"Rocket.Chat (Team Chat)","text":"<p>Self-hosted team chat for volunteer coordination. Requires MongoDB (auto-configured).</p> Variable Default Description <code>ENABLE_CHAT</code> <code>false</code> Set to <code>true</code> to enable the Rocket.Chat integration. The initial default; once saved in admin Settings, the DB value is authoritative. <code>ROCKETCHAT_ADMIN_USER</code> <code>rcadmin</code> Rocket.Chat admin username. <code>ROCKETCHAT_ADMIN_PASSWORD</code> \u2014 Rocket.Chat admin password. <code>ROCKETCHAT_URL</code> <code>http://rocketchat-changemaker:3000</code> Internal container URL. <code>ROCKETCHAT_EMBED_PORT</code> <code>8891</code> Port for iframe embedding in admin. <code>MONGO_ROOT_USER</code> <code>rocketchat</code> MongoDB admin username. <code>MONGO_ROOT_PASSWORD</code> \u2014 MongoDB admin password. MongoDB runs with <code>--auth</code> enabled.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#gancio-event-management","title":"Gancio (Event Management)","text":"<p>Self-hosted event management platform. Uses the shared PostgreSQL database (auto-created by <code>init-gancio-db.sh</code>).</p> Variable Default Description <code>GANCIO_PORT</code> <code>8092</code> Gancio web UI port. <code>GANCIO_URL</code> <code>http://gancio-changemaker:13120</code> Internal container URL. <code>GANCIO_EMBED_PORT</code> <code>8892</code> Port for iframe embedding in admin. <code>GANCIO_BASE_URL</code> <code>https://events.cmlite.org</code> Public-facing URL for Gancio. Used in event links. <code>GANCIO_ADMIN_USER</code> <code>admin</code> Gancio admin username for shift-to-event sync (OAuth login). <code>GANCIO_ADMIN_PASSWORD</code> \u2014 Gancio admin password. <code>GANCIO_SYNC_ENABLED</code> <code>false</code> Set to <code>true</code> to enable automatic shift \u2192 Gancio event synchronization.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#jitsi-meet-video-conferencing","title":"Jitsi Meet (Video Conferencing)","text":"<p>Self-hosted video conferencing with JWT authentication. Integrates with Rocket.Chat for in-channel video calls.</p> Variable Default Description <code>ENABLE_MEET</code> <code>false</code> Set to <code>true</code> to enable the Jitsi Meet integration. The initial default; once saved in admin Settings, the DB value is authoritative. <code>JITSI_APP_ID</code> <code>changemaker</code> JWT application ID. Must match across Jitsi Prosody, Rocket.Chat app settings, and <code>JWT_ACCEPTED_ISSUERS</code>/<code>JWT_ACCEPTED_AUDIENCES</code>. <code>JITSI_APP_SECRET</code> \u2014 JWT secret for signing Jitsi tokens. Generate with <code>openssl rand -hex 32</code>. Shared between Jitsi Prosody, Rocket.Chat, and the API. <code>JITSI_JICOFO_AUTH_PASSWORD</code> \u2014 Internal XMPP password for Jicofo (conference focus). Generate with <code>openssl rand -hex 16</code>. <code>JITSI_JVB_AUTH_PASSWORD</code> \u2014 Internal XMPP password for JVB (video bridge). Generate with <code>openssl rand -hex 16</code>. <code>JITSI_EMBED_PORT</code> <code>8893</code> Port for iframe embedding in admin. <code>JITSI_URL</code> <code>http://jitsi-web-changemaker:80</code> Internal container URL. <code>JVB_ADVERTISE_IP</code> (empty) Server's public IP address. Required in production for NAT traversal so remote participants can connect. <code>JVB_PORT</code> <code>10000</code> UDP port for media traffic. Must be open in your firewall. <p>Production requirements</p> <ul> <li><code>JVB_ADVERTISE_IP</code> must be set to your server's public IP for calls to work outside the local network.</li> <li>Port <code>10000/udp</code> must be open in your firewall for media traffic.</li> <li>Calls must go through the production domain (not localhost) for SSL/JWT to work.</li> </ul>","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#sms-campaigns-termux-android-bridge","title":"SMS Campaigns (Termux Android Bridge)","text":"<p>Send SMS messages via an Android phone running the Termux API server. The phone acts as an SMS gateway.</p> Variable Default Description <code>ENABLE_SMS</code> <code>false</code> Set to <code>true</code> to enable SMS campaigns. The initial default; once saved in admin Settings, the DB value is authoritative. <code>TERMUX_API_URL</code> <code>http://10.0.0.193:5001</code> URL of the Termux API server running on the Android phone. <code>TERMUX_API_KEY</code> (empty) API key for authenticating with the Termux server (HMAC auth via <code>X-API-Key</code> header). <code>SMS_DELAY_BETWEEN_MS</code> <code>3000</code> Delay between sending individual SMS messages (ms). Prevents carrier throttling. <code>SMS_MAX_RETRIES</code> <code>3</code> Maximum retry attempts for failed SMS sends. <code>SMS_RESPONSE_SYNC_INTERVAL_MS</code> <code>30000</code> How often to poll the phone's inbox for responses (ms). <code>SMS_DEVICE_MONITOR_INTERVAL_MS</code> <code>30000</code> How often to check device health \u2014 battery, connectivity (ms). <p>GUI configuration</p> <p>The Termux API URL and API key can also be configured from Admin \u2192 Settings \u2192 SMS. Database values override these env vars when set.</p>","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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>","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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.","tags":["reference","getting-started","operator","configuration"]},{"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>","tags":["reference","getting-started","operator","configuration"]},{"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>3005</code> Grafana dashboard port. <code>GRAFANA_ADMIN_PASSWORD</code> <code>admin</code> Change in production. <code>GRAFANA_ROOT_URL</code> <code>http://localhost:3005</code> Public URL for Grafana (used in links). <code>CADVISOR_PORT</code> <code>8086</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. <code>GRAFANA_EMBED_PORT</code> <code>8894</code> Port for iframe embedding Grafana in admin. <code>ALERTMANAGER_EMBED_PORT</code> <code>8895</code> Port for iframe embedding Alertmanager in admin.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#bunker-ops-fleet-management","title":"Bunker Ops (Fleet Management)","text":"<p>Remote metrics push for managing multiple Changemaker Lite instances from a central monitoring server.</p> Variable Default Description <code>INSTANCE_LABEL</code> (empty) Unique label for this instance (used as a Prometheus metric label). Falls back to <code>DOMAIN</code> if empty. <code>BUNKER_OPS_ENABLED</code> <code>false</code> Enable remote metrics push to a central VictoriaMetrics server. <code>BUNKER_OPS_REMOTE_WRITE_URL</code> (empty) VictoriaMetrics <code>remote_write</code> endpoint (e.g., <code>https://ops.example.com/api/v1/write</code>).","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#social-people-analytics","title":"Social, People &amp; Analytics","text":"<p>Feature flags for the social graph, CRM people module, and analytics dashboard.</p> Variable Default Description <code>ENABLE_SOCIAL</code> <code>false</code> Enable the social module (friendships, challenges, spotlights, referrals). The initial default; once saved in admin Settings, the DB value is authoritative. <code>ENABLE_PEOPLE</code> <code>false</code> Enable the CRM people module. The initial default; once saved in admin Settings, the DB value is authoritative. <code>ENABLE_ANALYTICS</code> <code>false</code> Enable the analytics dashboard with visitor tracking and geographic insights. The initial default; once saved in admin Settings, the DB value is authoritative.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#geoip-maxmind-geolite2","title":"GeoIP (MaxMind GeoLite2)","text":"<p>Geographic IP lookup for analytics visitor location tracking. Requires a free MaxMind account.</p> Variable Default Description <code>MAXMIND_ACCOUNT_ID</code> (empty) MaxMind account ID. Sign up free. <code>MAXMIND_LICENSE_KEY</code> (empty) MaxMind license key. When set, the GeoLite2-City database auto-downloads at startup. <code>GEOIP_DB_PATH</code> <code>/data/geoip/GeoLite2-City.mmdb</code> Path to the GeoLite2 database file inside the container.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#control-panel-agent-ccp","title":"Control Panel Agent (CCP)","text":"<p>Remote management agent for the Changemaker Control Panel \u2014 enables centralized multi-instance management.</p> Variable Default Description <code>ENABLE_CCP_AGENT</code> <code>false</code> Enable the CCP remote management agent. <code>CCP_URL</code> (empty) URL of the Changemaker Control Panel server. <code>CCP_INVITE_CODE</code> (empty) One-time invite code for agent registration with the control panel. <code>CCP_AGENT_URL</code> (empty) How the CCP can reach this agent (must be externally accessible). <code>CCP_AGENT_PORT</code> <code>7443</code> Agent listener port.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#container-registry","title":"Container Registry","text":"<p>Settings for pulling pre-built production images from the Gitea container registry.</p> Variable Default Description <code>GITEA_REGISTRY</code> <code>gitea.bnkops.com/admin</code> Registry hostname and namespace for pulling images. <code>IMAGE_TAG</code> (empty) Image tag to pull. Set to a commit SHA or <code>latest</code> for pre-built images. Leave empty (defaults to <code>local</code>) to build from source. <code>COMPOSE_PROFILES</code> (empty) Docker Compose profiles to activate. Set to <code>monitoring</code> to include Prometheus/Grafana/Alertmanager in every <code>docker compose up -d</code>. <code>GITEA_REGISTRY_USER</code> <code>admin</code> Registry username for <code>docker login</code> and the registry status API endpoint. <code>GITEA_REGISTRY_PASS</code> (empty) Registry password for the status API endpoint. For <code>docker push/pull</code>, use <code>docker login gitea.bnkops.com</code>. <code>GITEA_REGISTRY_API_TOKEN</code> (empty) API token for the remote registry (gitea.bnkops.com). Used by <code>build-release.sh --upload</code> to publish release tarballs. Create at Gitea \u2192 User Settings \u2192 Applications. Not the same as <code>GITEA_API_TOKEN</code>.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#docker-container-management","title":"Docker / Container Management","text":"<p>Internal settings for the admin dashboard's service status panel and container management.</p> Variable Default Description <code>DOCKER_NETWORK_NAME</code> <code>changemaker-lite</code> Docker bridge network name. Used by the dashboard to auto-discover containers. <code>DOCKER_PROXY_URL</code> <code>http://docker-socket-proxy:2375</code> Read-only Docker socket proxy URL for container inspection. <code>NEWT_CONTAINER_NAME</code> <code>newt-changemaker</code> Newt tunnel container name (for restart/status checks). <code>NEWT_COMPOSE_SERVICE</code> <code>newt</code> Docker Compose service name for the Newt container.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#embed-proxy-ports","title":"Embed Proxy Ports","text":"<p>Dedicated nginx ports for iframe embedding services in the admin dashboard without requiring DNS/subdomains. Change these to avoid port conflicts when running multiple instances on one host.</p> Variable Default Description <code>NOCODB_EMBED_PORT</code> <code>8881</code> NocoDB iframe port. <code>N8N_EMBED_PORT</code> <code>8882</code> n8n iframe port. <code>GITEA_EMBED_PORT</code> <code>8883</code> Gitea iframe port. <code>MAILHOG_EMBED_PORT</code> <code>8884</code> MailHog iframe port. <code>MINI_QR_EMBED_PORT</code> <code>8885</code> Mini QR iframe port. <code>EXCALIDRAW_EMBED_PORT</code> <code>8886</code> Excalidraw iframe port. <code>HOMEPAGE_EMBED_PORT</code> <code>8887</code> Homepage iframe port. <code>VAULTWARDEN_EMBED_PORT</code> <code>8890</code> Vaultwarden iframe port. <code>ROCKETCHAT_EMBED_PORT</code> <code>8891</code> Rocket.Chat iframe port. <code>GANCIO_EMBED_PORT</code> <code>8892</code> Gancio iframe port. <code>JITSI_EMBED_PORT</code> <code>8893</code> Jitsi iframe port. <code>GRAFANA_EMBED_PORT</code> <code>8894</code> Grafana iframe port. <code>ALERTMANAGER_EMBED_PORT</code> <code>8895</code> Alertmanager iframe port.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#gitea-docs-version-history","title":"Gitea Docs Version History","text":"<p>Settings for the documentation version history feature (backed by Gitea repository commits).</p> Variable Default Description <code>GITEA_DOCS_REPO</code> <code>admin/changemaker.lite</code> Gitea repository path for docs version history. <code>GITEA_DOCS_PREFIX</code> <code>mkdocs/docs</code> Path prefix within the repository where documentation files live. <code>GITEA_DOCS_BRANCH</code> <code>v2</code> Git branch to query for version history. <code>GITEA_ADMIN_PASSWORD</code> (empty) Gitea admin password. Used once during initial setup to create an API token, then can be cleared.","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/environment-variables/#prisma-cli-host-side","title":"Prisma CLI (Host-Side)","text":"Variable Default Description <code>DATABASE_URL</code> <code>postgresql://changemaker:YOUR_POSTGRES_PASSWORD@localhost:5433/changemaker_v2</code> Full PostgreSQL connection string. Only used when running Prisma CLI on the host (<code>npx prisma migrate dev</code>). Docker containers resolve the database hostname internally via Docker Compose environment variables.","tags":["reference","getting-started","operator","configuration"]},{"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 (three separate values)\necho \"JWT_ACCESS_SECRET=$(openssl rand -hex 32)\"\necho \"JWT_REFRESH_SECRET=$(openssl rand -hex 32)\"\necho \"JWT_INVITE_SECRET=$(openssl rand -hex 32)\"\n\n# Encryption key (must differ from JWT secrets)\necho \"ENCRYPTION_KEY=$(openssl rand -hex 32)\"\n\n# Security extras (key separation)\necho \"GITEA_SSO_SECRET=$(openssl rand -hex 32)\"\necho \"SERVICE_PASSWORD_SALT=$(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\n# Vaultwarden\necho \"VAULTWARDEN_ADMIN_TOKEN=$(openssl rand -hex 32)\"\n\n# Rocket.Chat + MongoDB\necho \"ROCKETCHAT_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\necho \"MONGO_ROOT_PASSWORD=$(openssl rand -hex 24)\"\n\n# Gancio\necho \"GANCIO_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\n\n# Jitsi Meet\necho \"JITSI_APP_SECRET=$(openssl rand -hex 32)\"\necho \"JITSI_JICOFO_AUTH_PASSWORD=$(openssl rand -hex 16)\"\necho \"JITSI_JVB_AUTH_PASSWORD=$(openssl rand -hex 16)\"\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>","tags":["reference","getting-started","operator","configuration"]},{"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=...\nJWT_INVITE_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\nENABLE_PAYMENTS=true\nENABLE_CHAT=true\nENABLE_MEET=true\nENABLE_SMS=true\nLISTMONK_SYNC_ENABLED=true\nGANCIO_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=...\nVAULTWARDEN_ADMIN_TOKEN=...\nROCKETCHAT_ADMIN_PASSWORD=...\nMONGO_ROOT_PASSWORD=...\nGANCIO_ADMIN_PASSWORD=...\nJITSI_APP_SECRET=...\nJITSI_JICOFO_AUTH_PASSWORD=...\nJITSI_JVB_AUTH_PASSWORD=...\nJVB_ADVERTISE_IP=your.public.ip.here\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>","tags":["reference","getting-started","operator","configuration"]},{"location":"docs/getting-started/features/","title":"Features at a Glance","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <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>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#core-features","title":"Core Features","text":"<ul> <li> <p> Advocacy Campaigns</p> <p>Help supporters contact elected representatives through email campaigns with postal code lookup and a public response wall.</p> <p> Campaign guide</p> </li> <li> <p> Map &amp; Canvassing</p> <p>Manage locations, draw canvassing territories, schedule volunteer shifts, and run GPS-tracked door-to-door outreach.</p> <p> Map guide</p> </li> <li> <p> Media Manager</p> <p>Upload videos and photos, curate playlists, publish a shorts feed, and track engagement with built-in analytics.</p> <p> Media guide</p> </li> <li> <p> Landing Pages</p> <p>Build campaign microsites with a drag-and-drop GrapesJS visual editor and publish at custom slugs.</p> <p> Landing pages guide</p> </li> <li> <p> Payments (Stripe)</p> <p>Accept memberships, product sales, and donations with encrypted Stripe integration and branded donation pages.</p> <p> Payments guide</p> </li> <li> <p> SMS Campaigns</p> <p>Text message outreach via a Termux Android bridge with contact lists, templates, and response tracking.</p> <p> SMS guide</p> </li> <li> <p> Public Homepage</p> <p>Customizable landing page with hero section, live stats, featured campaigns, upcoming shifts, and activity feed.</p> <p> Homepage guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#communication-collaboration","title":"Communication &amp; Collaboration","text":"<ul> <li> <p> Newsletter (Listmonk)</p> <p>Opt-in mailing lists and newsletter campaigns with automatic subscriber sync from shifts and contacts.</p> <p> Newsletter guide</p> </li> <li> <p> Email Templates</p> <p>Reusable email templates with variable substitution for campaign communications.</p> <p> Email templates guide</p> </li> <li> <p> Team Chat (Rocket.Chat)</p> <p>Self-hosted team chat with iframe integration, floating widget, and native mobile app support.</p> <p> Chat guide</p> </li> <li> <p> Video Conferencing (Jitsi)</p> <p>Self-hosted video calls integrated with Rocket.Chat via JWT authentication \u2014 no separate login required.</p> <p> Video conferencing guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#integrations-services","title":"Integrations &amp; Services","text":"<ul> <li> <p> Events (Gancio)</p> <p>Self-hosted event management with automatic shift-to-event sync and an embeddable calendar widget.</p> <p> Events guide</p> </li> <li> <p> Password Manager (Vaultwarden)</p> <p>Bitwarden-compatible password vault for secure team credential sharing.</p> <p> Password manager guide</p> </li> <li> <p> User Provisioning</p> <p>Automatic account creation and sync across Rocket.Chat, Gitea, Vaultwarden, and Listmonk.</p> <p> User provisioning guide</p> </li> <li> <p> People / Contacts</p> <p>Centralized contact management for supporters, donors, and community members with cross-module linking.</p> <p> People guide</p> </li> <li> <p> Whiteboard (Excalidraw)</p> <p>Self-hosted collaborative whiteboard for brainstorming, planning, and visual collaboration.</p> <p> Whiteboard guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#volunteer-portal","title":"Volunteer Portal","text":"<ul> <li> <p> Social Connections</p> <p>Friend system, activity feed, groups, profiles, pokes, and privacy controls for volunteer community building.</p> <p> Social guide</p> </li> <li> <p> Achievements &amp; Leaderboard</p> <p>Badge system with 11 achievements across 4 categories, progress tracking, and competitive leaderboards.</p> <p> Achievements guide</p> </li> <li> <p> Volunteer Quick Join</p> <p>QR code invite links for instant volunteer onboarding \u2014 scan, fill a short form, and start canvassing.</p> <p> Quick join guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#automation-analytics","title":"Automation &amp; Analytics","text":"<ul> <li> <p> Email Automation</p> <p>Automated volunteer lifecycle emails \u2014 thank-you notes, shift reminders, weekly summaries, and re-engagement campaigns.</p> <p> Automation guide</p> </li> <li> <p> Data Quality Dashboard</p> <p>Geocoding quality metrics with per-provider stats, confidence tiers, and coverage analysis.</p> <p> Data quality guide</p> </li> <li> <p> Documentation Analytics</p> <p>Page view tracking and engagement metrics for MkDocs documentation pages.</p> <p> Docs analytics guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/features/#admin-tools","title":"Admin Tools","text":"<ul> <li> <p> Docs Comments</p> <p>Gitea-backed comment system for documentation pages with anonymous posting and moderation.</p> <p> Docs comments guide</p> </li> <li> <p> Command Palette</p> <p>Global Ctrl+K search across pages, campaigns, locations, users, settings, and media.</p> <p> Command palette guide</p> </li> <li> <p> Navigation Settings</p> <p>Customize the public navigation menu with feature toggles, custom links, and drag-and-drop reordering.</p> <p> Navigation guide</p> </li> <li> <p> Platform Settings</p> <p>Five-tab settings page covering organization details, theme colors, email configuration, feature flags, and notifications.</p> <p> Settings guide</p> </li> <li> <p> Social Sharing (OG Tags)</p> <p>Open Graph meta tags for campaigns, landing pages, and gallery videos \u2014 rich link previews on social media.</p> <p> OG sharing guide</p> </li> <li> <p> Gallery Ads</p> <p>Internal ad system with 5 ad types, audience targeting, scheduling, frequency caps, and CTR analytics.</p> <p> Gallery ads guide</p> </li> <li> <p> Self-Service Contact Profile</p> <p>Token-based public profile pages where contacts can view and update their information and preferences.</p> <p> Contact profile guide</p> </li> </ul>","tags":["reference","getting-started"],"boost":2},{"location":"docs/getting-started/first-steps/","title":"First Steps","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>You've installed Changemaker Lite \u2014 here's what to do next.</p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#1-log-in","title":"1. Log In","text":"<p>Open the admin panel at <code>http://localhost:3000</code> (or <code>app.DOMAIN</code> in production) and sign in with the admin email and password you configured during setup.</p> <p></p> <p>Change your password</p> <p>If you used the wizard's generated password, change it immediately from Settings &gt; Organization.</p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#2-explore-the-dashboard","title":"2. Explore the Dashboard","text":"<p>The dashboard gives you an at-a-glance view of platform activity. Initially it will be empty \u2014 that's normal.</p> <p></p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#3-configure-settings","title":"3. Configure Settings","text":"<p>Visit Settings (<code>/app/settings</code>) to:</p> <ul> <li>Set your organization name, logo, and tagline</li> <li>Choose theme colors for admin and public interfaces</li> <li>Enable feature modules (campaigns, map, media, payments, etc.)</li> <li>Configure email delivery (MailHog for testing, production SMTP for live use)</li> <li>Check the System tab to verify your installation and check for updates</li> </ul> <p></p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#4-create-your-first-campaign","title":"4. Create Your First Campaign","text":"<p>Go to Campaigns (<code>/app/campaigns</code>) and click Create Campaign:</p> <ol> <li>Write a title and description</li> <li>Compose the email template supporters will send</li> <li>Select government levels to target</li> <li>Publish \u2014 the campaign appears at <code>/campaigns</code></li> </ol> <p></p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#5-add-locations","title":"5. Add Locations","text":"<p>Go to Locations (<code>/app/map</code>) and add addresses:</p> <ul> <li>Click on the map to drop a marker</li> <li>Import a CSV of addresses</li> <li>Use the NAR (National Address Register) import for Canadian data</li> </ul> <p></p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#6-schedule-a-shift","title":"6. Schedule a Shift","text":"<p>Go to Shifts (<code>/app/map/shifts</code>) and create your first volunteer shift:</p> <ol> <li>Set a date, time, and location description</li> <li>Optionally link it to a canvassing area</li> <li>Share the public shifts page (<code>/shifts</code>) with volunteers</li> </ol> <p></p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#7-invite-volunteers","title":"7. Invite Volunteers","text":"<p>Share the shifts page link or generate QR codes for in-person events. Volunteers sign up with just an email address.</p>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/first-steps/#next-steps","title":"Next Steps","text":"<ul> <li>Services Overview \u2014 complete catalog of all 30+ Docker services</li> <li>Updates &amp; Upgrades \u2014 keep your installation current</li> <li>Features at a Glance \u2014 visual overview of every module</li> <li>Admin Guide \u2014 full administration reference</li> <li>Deployment \u2014 production setup with tunneling and SSL</li> </ul>","tags":["tutorial","getting-started","admin"],"boost":2},{"location":"docs/getting-started/installation/","title":"Installation","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>Changemaker Lite runs as a set of Docker containers orchestrated by Docker Compose. The <code>config.sh</code> wizard handles all configuration \u2014 or you can set things up manually.</p> <p>Have your external services ready?</p> <p>For a production deployment, you'll need a domain name, SMTP email provider, and a reverse tunnel (like Pangolin) or public IP with SSL. Gather these before running the wizard \u2014 it makes the process much smoother.</p> <p> Prerequisites &amp; External Services \u2014 full checklist with provider recommendations</p> <p>For local development/evaluation, you can skip this \u2014 Docker and MailHog handle everything out of the box.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#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 for core services, 4 GB for the full stack</li> <li>A domain name (optional for development, recommended for production)</li> <li>An SMTP provider for production email delivery (see Prerequisites)</li> <li>A reverse tunnel or public IP for internet access (see Prerequisites)</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#quick-start","title":"Quick Start","text":"<p>Clone the repository:</p> <pre><code>git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\n</code></pre> <p>Run the configuration wizard:</p> <pre><code>bash config.sh\n</code></pre> <p>Start all services:</p> <pre><code>docker compose up -d\n</code></pre> <p>Open http://localhost:3000 and sign in with the admin credentials you configured. Database migrations and seeding run automatically on first startup.</p> <p>Change your password</p> <p>If you used the wizard's generated password, change it immediately from the admin dashboard.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#pre-built-image-installation","title":"Pre-built Image Installation","text":"<p>For production deployments, you can skip cloning the source repository entirely. Pre-built Docker images are pulled from the Gitea container registry.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#one-line-install","title":"One-Line Install","text":"<pre><code>curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash\n</code></pre> <p>This script:</p> <ol> <li>Checks prerequisites (Docker, Docker Compose, OpenSSL)</li> <li>Downloads the latest release package from Gitea</li> <li>Extracts to <code>~/changemaker.lite/</code></li> <li>Launches the configuration wizard (<code>config.sh</code>)</li> </ol> <p>After the wizard completes, start everything with <code>docker compose up -d</code>.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#manual-download","title":"Manual Download","text":"<p>If you prefer not to pipe to bash:</p> <pre><code># Download latest release\ncurl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz\ntar xzf changemaker-lite-latest.tar.gz\ncd changemaker-lite\nbash config.sh\ndocker compose up -d\n</code></pre>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#whats-different-from-source-install","title":"What's Different from Source Install","text":"Source Install Pre-built Install Download size ~200 MB (full repo) ~2 MB (config + scripts) First startup 10+ min (TypeScript compile + Docker build) ~2 min (image pull only) Requires Git, full repo Docker only Upgrades <code>git pull</code> + rebuild Download new release tarball Development Edit source, hot-reload Not for development <p>When to use which</p> <p>Use pre-built install for production deployments and quick evaluation. Use source install when you want to modify the platform code or contribute to development.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#configuration-wizard-configsh","title":"Configuration Wizard (<code>config.sh</code>)","text":"<p>The wizard performs 14 steps to produce a fully configured <code>.env</code> file and prepare the system for startup. Each step is interactive with sensible defaults.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-1-prerequisites-check","title":"Step 1: Prerequisites Check","text":"<p>Verifies that Docker, Docker Compose v2, and OpenSSL are installed. Exits immediately if any are missing, with links to installation guides.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-2-environment-file-setup","title":"Step 2: Environment File Setup","text":"<ul> <li>If no <code>.env</code> exists, copies <code>.env.example</code> as the starting point</li> <li>If <code>.env</code> already exists, offers to back it up (timestamped copy) and create a fresh one, or update values in place</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-3-domain-configuration","title":"Step 3: Domain Configuration","text":"<p>Prompts for your root domain (default: <code>cmlite.org</code>) and derives 14 environment variables from it:</p> Variable Example Value <code>DOMAIN</code> <code>example.org</code> <code>BASE_DOMAIN</code> <code>https://example.org</code> <code>GITEA_ROOT_URL</code> <code>https://git.example.org</code> <code>GITEA_DOMAIN</code> <code>git.example.org</code> <code>N8N_HOST</code> <code>n8n.example.org</code> <code>SMTP_FROM</code> <code>noreply@example.org</code> <code>INITIAL_ADMIN_EMAIL</code> <code>admin@example.org</code> <code>NC_ADMIN_EMAIL</code> <code>admin@example.org</code> <code>EXCALIDRAW_WS_URL</code> <code>wss://draw.example.org</code> <code>LISTMONK_SMTP_FROM</code> <code>Changemaker Lite &lt;noreply@example.org&gt;</code> <code>HOMEPAGE_VAR_BASE_URL</code> <code>https://example.org</code> <code>VAULTWARDEN_DOMAIN</code> <code>https://vault.example.org</code> <code>GANCIO_BASE_URL</code> <code>https://events.example.org</code> <code>TEST_EMAIL_RECIPIENT</code> <code>admin@example.org</code> <p>Also updates <code>mkdocs/mkdocs.yml</code> with the new <code>site_url</code> and <code>repo_url</code>, and asks whether this is a production deployment (sets <code>NODE_ENV=production</code>).</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-4-admin-credentials","title":"Step 4: Admin Credentials","text":"<p>Prompts for the initial super-admin email and password. The password is validated against the security policy:</p> <ul> <li>Minimum 12 characters</li> <li>At least one uppercase letter</li> <li>At least one lowercase letter</li> <li>At least one digit</li> <li>Requires password confirmation</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-5-secret-generation","title":"Step 5: Secret Generation","text":"<p>Auto-generates 21 unique secrets \u2014 no placeholder passwords remain after this step:</p> Category Count Secrets JWT &amp; Encryption 4 <code>JWT_ACCESS_SECRET</code>, <code>JWT_REFRESH_SECRET</code>, <code>JWT_INVITE_SECRET</code> (each 64-char hex), <code>ENCRYPTION_KEY</code> (64-char hex, must differ from JWT secrets) Database 2 <code>V2_POSTGRES_PASSWORD</code>, <code>REDIS_PASSWORD</code> (24-char alphanumeric) Listmonk 3 <code>LISTMONK_DB_PASSWORD</code>, <code>LISTMONK_WEB_ADMIN_PASSWORD</code>, <code>LISTMONK_API_TOKEN</code> NocoDB 1 <code>NC_ADMIN_PASSWORD</code> Gitea 2 <code>GITEA_DB_PASSWD</code>, <code>GITEA_DB_ROOT_PASSWORD</code> n8n 2 <code>N8N_ENCRYPTION_KEY</code>, <code>N8N_USER_PASSWORD</code> Monitoring 2 <code>GRAFANA_ADMIN_PASSWORD</code>, <code>GOTIFY_ADMIN_PASSWORD</code> Vaultwarden 1 <code>VAULTWARDEN_ADMIN_TOKEN</code> (64-char hex) Rocket.Chat 1 <code>ROCKETCHAT_ADMIN_PASSWORD</code> Gancio 1 <code>GANCIO_ADMIN_PASSWORD</code> Jitsi Meet 3 <code>JITSI_APP_SECRET</code> (64-char hex), <code>JITSI_JICOFO_AUTH_PASSWORD</code>, <code>JITSI_JVB_AUTH_PASSWORD</code>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-6-email-configuration","title":"Step 6: Email Configuration","text":"<p>Choose between:</p> <ul> <li>MailHog (default) \u2014 captures all outgoing emails at <code>http://localhost:8025</code> for development</li> <li>Production SMTP \u2014 configures host, port, user, and password. Optionally shares credentials with Listmonk for newsletter delivery</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-7-feature-flags","title":"Step 7: Feature Flags","text":"<p>Enable or disable 9 optional platform features:</p> Flag Environment Variable What It Enables Media Manager <code>ENABLE_MEDIA_FEATURES=true</code> Video library, analytics, scheduled publishing Listmonk Sync <code>LISTMONK_SYNC_ENABLED=true</code> Newsletter subscriber sync from platform participants Payments <code>ENABLE_PAYMENTS=true</code> Stripe-based products, donations, and plans Rocket.Chat <code>ENABLE_CHAT=true</code> Team communication platform Gancio Events <code>GANCIO_SYNC_ENABLED=true</code> Shift-to-event sync with Gancio Jitsi Meet <code>ENABLE_MEET=true</code> Video conferencing (also prompts for server public IP) SMS Campaigns <code>ENABLE_SMS=true</code> Termux Android bridge for SMS (also prompts for API URL) Docs Comments <code>GITEA_COMMENTS_ENABLED=true</code> Gitea-backed page comments on documentation Bunker Ops <code>BUNKER_OPS_ENABLED=true</code> Fleet metrics push to central server (also prompts for remote write URL)","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-8-tunnel-configuration-pangolin","title":"Step 8: Tunnel Configuration (Pangolin)","text":"<p>Optionally configures Pangolin tunnel credentials for secure public access:</p> <ul> <li><code>PANGOLIN_API_URL</code> \u2014 API endpoint (default: <code>https://api.bnkserve.org/v1</code>)</li> <li><code>PANGOLIN_API_KEY</code> \u2014 Authentication key</li> <li><code>PANGOLIN_ORG_ID</code> \u2014 Organization identifier</li> </ul> <p>Complete tunnel setup is done from the admin GUI at Settings &gt; Tunnel after services are running.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-9-cors-origins","title":"Step 9: CORS Origins","text":"<p>Automatically calculates allowed origins from your domain:</p> <pre><code>http://app.DOMAIN,https://app.DOMAIN,http://DOMAIN,https://DOMAIN,http://localhost:3000,http://localhost,http://localhost:4003\n</code></pre>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-10-nginx-config-generation","title":"Step 10: Nginx Config Generation","text":"<p>Renders all <code>.conf.template</code> files in <code>nginx/conf.d/</code> by substituting <code>${DOMAIN}</code> with your configured domain. This produces the nginx configuration files that handle subdomain routing.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-11-homepage-services-yaml","title":"Step 11: Homepage Services YAML","text":"<p>Generates <code>configs/homepage/services.yaml</code> with 27 service entries (both production and local development URLs) for the Homepage service dashboard.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-12-container-directory-permissions","title":"Step 12: Container Directory Permissions","text":"<p>Creates and sets permissions (775) on 12 directories needed by containers:</p> Directory Purpose <code>configs/code-server/.config</code> Code Server configuration <code>configs/code-server/.local</code> Code Server local data <code>mkdocs/.cache</code> MkDocs build cache <code>mkdocs/site</code> MkDocs built site output <code>assets/uploads</code> Listmonk uploads <code>assets/images</code> Shared images <code>assets/icons</code> Homepage icons <code>media/local/inbox</code> Media upload inbox <code>media/local/thumbnails</code> Video thumbnails <code>media/public</code> Public media files <code>local-files</code> n8n local files <code>data</code> NAR import data","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-13-upgrade-watcher-optional","title":"Step 13: Upgrade Watcher (Optional)","text":"<p>Installs a systemd path watcher that enables the admin GUI's \"Check for Updates\" and \"Start Upgrade\" buttons. This step requires <code>sudo</code> and is optional \u2014 you can install it later or use the CLI upgrade script directly.</p> <p>The watcher installs two systemd units:</p> <ul> <li><code>changemaker-upgrade.path</code> \u2014 watches for <code>data/upgrade/trigger.json</code></li> <li><code>changemaker-upgrade.service</code> \u2014 runs <code>scripts/upgrade-watcher.sh</code> when triggered</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#step-14-summary-next-steps","title":"Step 14: Summary &amp; Next Steps","text":"<p>Displays a configuration summary showing all choices made, then prints startup commands.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#what-gets-modified","title":"What Gets Modified","text":"<p>After the wizard completes, the following files have been created or modified:</p> File Action <code>.env</code> Created (or updated) with all configuration values <code>mkdocs/mkdocs.yml</code> Updated <code>site_url</code> and <code>repo_url</code> with domain <code>nginx/conf.d/*.conf</code> Generated from <code>.conf.template</code> files <code>configs/homepage/services.yaml</code> Generated with all service URLs 12 directories Created with container-friendly permissions systemd units (optional) Installed to <code>/etc/systemd/system/</code>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#manual-setup-alternative","title":"Manual Setup (Alternative)","text":"<p>If you prefer to configure by hand instead of using the wizard:</p> <pre><code>cp .env.example .env\n</code></pre> <p>At minimum, set these required secrets:</p> <pre><code># Generate cryptographic secrets\nV2_POSTGRES_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24)\nREDIS_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24)\nJWT_ACCESS_SECRET=$(openssl rand -hex 32)\nJWT_REFRESH_SECRET=$(openssl rand -hex 32)\nJWT_INVITE_SECRET=$(openssl rand -hex 32)\nENCRYPTION_KEY=$(openssl rand -hex 32) # must differ from all JWT secrets\n</code></pre> <p>Set your admin credentials (password must meet the 12+ char complexity requirement):</p> <pre><code>INITIAL_ADMIN_EMAIL=admin@yourdomain.org\nINITIAL_ADMIN_PASSWORD=YourStrongPassword1\n</code></pre> <p>Then configure optional sections:</p> <ul> <li>Domain: Set <code>DOMAIN</code> and all derived variables (see Step 3 table above)</li> <li>SMTP: Set <code>SMTP_HOST</code>, <code>SMTP_PORT</code>, <code>SMTP_USER</code>, <code>SMTP_PASS</code>, <code>EMAIL_TEST_MODE=false</code></li> <li>Feature flags: Enable features as needed (see Step 7 table above)</li> <li>Tunnel: Set <code>PANGOLIN_API_URL</code>, <code>PANGOLIN_API_KEY</code>, <code>PANGOLIN_ORG_ID</code></li> </ul> <p>See Environment Variables for every available option.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#full-stack-startup","title":"Full Stack Startup","text":"<p>After configuration, start the entire platform:</p> <pre><code>docker compose up -d\n</code></pre> <p>That's it. Docker handles the startup order automatically:</p> <ol> <li>PostgreSQL and Redis start first (with healthchecks)</li> <li>API waits for both to be healthy, then auto-runs database migrations and seeding</li> <li>Admin GUI waits for the API</li> <li>Nginx, media, communication, and all other services start in parallel</li> <li>Init containers (nocodb-init, listmonk-init, etc.) run once and exit</li> </ol> <p>Watch the startup progress:</p> <pre><code>docker compose logs -f api --tail 20\n</code></pre> <p>Once you see <code>Starting server on port 4000</code>, open http://localhost:3000 and log in.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#include-monitoring","title":"Include Monitoring","text":"<p>The monitoring stack (Prometheus, Grafana, Alertmanager) uses a Docker Compose profile and isn't included by default:</p> <pre><code>docker compose --profile monitoring up -d\n</code></pre>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#start-only-core-services","title":"Start Only Core Services","text":"<p>If you prefer a minimal startup (lower resource usage):</p> <pre><code>docker compose up -d v2-postgres redis api admin nginx\n</code></pre> <p>Manual migrations</p> <p>The API container runs migrations and seeding automatically on startup via its entrypoint script. You only need to run them manually if you're developing locally without Docker:</p> <pre><code>cd api &amp;&amp; npx prisma migrate deploy &amp;&amp; npx prisma db seed\n</code></pre> <p>See Services Overview for the complete service catalog.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#verifying-installation","title":"Verifying Installation","text":"<p>After starting services, verify everything is healthy:</p> <pre><code># Check running containers\ndocker compose ps\n\n# API health check\ncurl -s http://localhost:4000/api/health | python3 -m json.tool\n\n# View API logs\ndocker compose logs api --tail 20\n\n# Check for containers in restart loops\ndocker compose ps | grep -i restarting\n</code></pre> <p>You should see the API return <code>{\"status\":\"ok\"}</code> and all started containers in a \"running\" state.</p>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/installation/#next-steps","title":"Next Steps","text":"<ul> <li>Services Overview \u2014 complete service catalog with ports and startup commands</li> <li>Environment Variables \u2014 complete <code>.env</code> reference</li> <li>First Steps \u2014 create your first campaign and add locations</li> <li>Updates &amp; Upgrades \u2014 keep your installation up to date</li> </ul>","tags":["guide","getting-started","operator","docker"],"boost":2},{"location":"docs/getting-started/prerequisites/","title":"Prerequisites &amp; External Services","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>Before running the installer, gather the external services and accounts listed below. Having these ready makes the configuration wizard a smooth, uninterrupted process.</p> <p>Don't have these yet?</p> <p>You can still install Changemaker Lite in development mode with just Docker \u2014 no domain, tunnel, or SMTP required. MailHog captures all emails locally. But for a production deployment serving real users, you'll need the items on this page.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#required-for-production","title":"Required for Production","text":"","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#1-a-domain-name","title":"1. A Domain Name","text":"<p>You need a domain (e.g., <code>betteredmonton.org</code>) that you control. Changemaker Lite uses subdomain routing \u2014 the platform creates subdomains like:</p> Subdomain Purpose <code>app.yourdomain.org</code> Admin dashboard + all public pages <code>api.yourdomain.org</code> Backend API <code>docs.yourdomain.org</code> Documentation site <code>git.yourdomain.org</code> Git hosting (Gitea) <code>events.yourdomain.org</code> Event calendar (Gancio) ... and 10+ more See Services Overview <p>You'll point your domain's DNS to wherever your tunnel or server is hosted. Wildcard DNS (<code>*.yourdomain.org</code>) is the simplest approach.</p> <p>Where to get one: Any registrar \u2014 Namecheap, Cloudflare Registrar, Porkbun, etc. Budget ~$10\u201315/year.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#2-a-reverse-tunnel-or-public-ip","title":"2. A Reverse Tunnel or Public IP","text":"<p>Your server needs to be reachable from the internet. Most home/office networks don't have a static public IP, so you need a reverse tunnel service that gives your server a stable public address with SSL.</p> <p>Changemaker Lite has built-in support for Pangolin \u2014 a self-hosted, open-source tunnel that handles SSL certificates, subdomain routing, and access control automatically. The admin dashboard includes a one-click Pangolin setup wizard.</p> <p>What you need:</p> <ul> <li>A Pangolin server (or access to a shared one)</li> <li>An API key and Organization ID</li> <li>Your domain's DNS pointed at the Pangolin server</li> </ul> <p>Alternatives: Cloudflare Tunnel (free tier available), a VPS with a public IP, or any reverse proxy with SSL termination.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#3-smtp-email-provider","title":"3. SMTP Email Provider","text":"<p>Production deployments need a real SMTP server to send emails \u2014 campaign messages, password resets, volunteer invitations, and newsletter delivery all depend on it.</p> <p>What you need:</p> Setting Example SMTP Host <code>smtp.protonmail.ch</code> SMTP Port <code>587</code> (STARTTLS) or <code>465</code> (TLS) SMTP Username <code>your-account@provider.com</code> SMTP Password Your SMTP password or app-specific password <p>Popular SMTP providers:</p> Provider Free Tier Notes Proton Mail Included with paid plan Privacy-focused, recommended for advocacy Mailgun 100 emails/day (FLEX) Good deliverability, easy setup Amazon SES 62,000/month (from EC2) Cheapest at scale, requires verification Brevo (Sendinblue) 300 emails/day Simple setup, good free tier Resend 100 emails/day Developer-friendly, modern API <p>Shared hosting SMTP</p> <p>Avoid using shared hosting SMTP (GoDaddy, Bluehost, etc.) for campaign emails \u2014 they have low sending limits and poor deliverability. Use a dedicated transactional email provider.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#4-a-linux-server","title":"4. A Linux Server","text":"<p>Changemaker Lite runs on any Linux server with Docker. Minimum specs:</p> Component Minimum Recommended RAM 2 GB (core only) 4 GB (full stack) Disk 10 GB 20+ GB (with media uploads) CPU 1 vCPU 2+ vCPU OS Any Linux with Docker Ubuntu 22.04+ LTS <p>Options: A VPS from DigitalOcean, Hetzner, Linode, or a spare machine on your network. If using a tunnel (Pangolin), the server doesn't need a public IP.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#optional-enhance-your-deployment","title":"Optional (Enhance Your Deployment)","text":"<p>These are not required but unlock additional platform features:</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#stripe-account-payments","title":"Stripe Account (Payments)","text":"<p>For accepting donations, selling merchandise, or managing membership plans. Create a free account at stripe.com. You'll enter your Stripe API keys in the admin settings page (they're stored encrypted in the database).</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#mapbox-or-google-maps-api-key-geocoding","title":"Mapbox or Google Maps API Key (Geocoding)","text":"<p>Improves address geocoding accuracy for the mapping module. The platform works without these (using free OpenStreetMap providers), but paid providers are more reliable for bulk operations.</p> <ul> <li>Mapbox: Free tier includes 100,000 requests/month. Sign up.</li> <li>Google Maps: Free tier includes $200/month credit (~40,000 requests). Sign up.</li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#maxmind-geolite2-analytics","title":"MaxMind GeoLite2 (Analytics)","text":"<p>For geographic analytics (visitor location tracking). Free account at maxmind.com. The database auto-downloads at startup when credentials are configured.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#android-phone-with-termux-sms-campaigns","title":"Android Phone with Termux (SMS Campaigns)","text":"<p>The SMS module uses a physical Android phone as an SMS gateway via the Termux app. This is a unique feature for grassroots campaigns that want to send SMS without expensive third-party services.</p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#jitsi-meet-requirements-video-conferencing","title":"Jitsi Meet Requirements (Video Conferencing)","text":"<p>If enabling the self-hosted video conferencing feature:</p> <ul> <li>Server's public IP address (for NAT traversal)</li> <li>UDP port 10000 open in your firewall (for media traffic)</li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#pre-installation-checklist","title":"Pre-Installation Checklist","text":"<p>Use this checklist to make sure you're ready:</p> <ul> <li> Domain name registered and DNS accessible</li> <li> DNS configured \u2014 wildcard <code>*.yourdomain.org</code> or individual subdomain records pointing to your tunnel/server</li> <li> Tunnel or public IP \u2014 Pangolin credentials (API key + Org ID), or server with public IP + SSL</li> <li> SMTP credentials \u2014 host, port, username, password from your email provider</li> <li> Linux server with Docker 24+ and Docker Compose v2 installed</li> <li> OpenSSL installed (for generating secrets during setup)</li> <li> (Optional) Stripe account for payments</li> <li> (Optional) Mapbox or Google Maps API key for geocoding</li> <li> (Optional) MaxMind account for geographic analytics</li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#managed-services","title":"Bunker Operations Can Help","text":"<p>Setting up infrastructure \u2014 domains, tunnels, SMTP, servers \u2014 can be the hardest part of self-hosting. Bunker Operations offers managed infrastructure for organizations running Changemaker Lite:</p> <ul> <li> <p> Managed Pangolin Tunnel</p> <p>Pre-configured tunnel with SSL, wildcard DNS, and automatic subdomain routing. Just plug in your API key and go.</p> </li> <li> <p> SMTP Relay</p> <p>High-deliverability transactional email with SPF/DKIM/DMARC already configured for your domain.</p> </li> <li> <p> Hosted Servers</p> <p>Pre-provisioned Linux servers with Docker, monitoring, and automatic backups \u2014 ready for a one-command install.</p> </li> <li> <p> Setup Assistance</p> <p>We'll walk you through the full deployment \u2014 from domain registration to your first campaign launch.</p> </li> </ul> <p>Built by organizers, for organizers</p> <p>Bunker Operations exists so campaign teams can focus on building power \u2014 not wrestling with infrastructure. We provide the plumbing so you can focus on the mission.</p> <p>Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/prerequisites/#next-steps","title":"Next Steps","text":"<p>Once you have your prerequisites ready:</p> <ul> <li>Installation \u2014 run the configuration wizard and start services</li> <li>Environment Variables \u2014 complete reference for every <code>.env</code> setting</li> <li>Deployment Guide \u2014 production setup with SSL and tunneling</li> </ul>","tags":["guide","getting-started","operator","planning"],"boost":2},{"location":"docs/getting-started/services/","title":"Services Overview","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>Changemaker Lite runs as 30+ Docker containers orchestrated by Docker Compose. This page catalogs every service, organized by category.</p> <p>Quick reference</p> <p>Use <code>docker compose ps</code> to see which services are currently running, or <code>docker compose ps -a</code> to include stopped containers.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#core-required","title":"Core (Required)","text":"<p>These services form the minimum viable platform. Start them first.</p> Container Port Description <code>changemaker-v2-api</code> 4000 Express.js REST API (Prisma ORM) <code>changemaker-v2-admin</code> 3000 React admin GUI (Vite + Ant Design) <code>changemaker-v2-postgres</code> 5433 PostgreSQL 16 \u2014 primary database <code>redis-changemaker</code> 6379 Redis 7 \u2014 cache, rate limiting, job queues <code>changemaker-v2-nginx</code> 80 Nginx reverse proxy \u2014 subdomain routing <pre><code># Start core services only (minimal)\ndocker compose up -d v2-postgres redis api admin nginx\n\n# Or start everything at once\ndocker compose up -d\n</code></pre> <p>The API container automatically runs database migrations and seeding on startup via its entrypoint script.</p> <p>Note</p> <p>Nginx is technically optional for local development (you can access services directly by port), but required for production subdomain routing.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#media","title":"Media","text":"Container Port Description Feature Flag <code>changemaker-media-api</code> 4100 Fastify media API \u2014 video library, analytics, scheduling <code>ENABLE_MEDIA_FEATURES=true</code> <pre><code>docker compose up -d media-api\n</code></pre> <p>The media API runs as a separate Fastify server sharing the same PostgreSQL database. It handles video upload (FFprobe metadata extraction), scheduled publishing via BullMQ, and GDPR-compliant view analytics.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#communication","title":"Communication","text":"","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#rocketchat-team-chat","title":"Rocket.Chat (Team Chat)","text":"Container Port Description Feature Flag <code>rocketchat-changemaker</code> 8891 Rocket.Chat server <code>ENABLE_CHAT=true</code> <code>mongodb-changemaker</code> \u2014 MongoDB (Rocket.Chat data store) \u2014 <code>nats-changemaker</code> \u2014 NATS (Rocket.Chat message bus) \u2014 <pre><code>docker compose up -d rocketchat mongodb nats\n</code></pre>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#gancio-events","title":"Gancio (Events)","text":"Container Port Description Feature Flag <code>gancio-changemaker</code> 8092 Gancio event platform <code>GANCIO_SYNC_ENABLED=true</code> <code>gancio-init</code> \u2014 Init container \u2014 creates Gancio database \u2014 <pre><code>docker compose up -d gancio\n</code></pre> <p>Init containers</p> <p><code>gancio-init</code> runs once on first start to create the Gancio database in PostgreSQL, then exits. This is normal \u2014 don't worry about seeing it in a \"stopped\" state.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#jitsi-meet-video-conferencing","title":"Jitsi Meet (Video Conferencing)","text":"Container Port Description Feature Flag <code>jitsi-web-changemaker</code> 8893 Jitsi web interface <code>ENABLE_MEET=true</code> <code>jitsi-prosody-changemaker</code> \u2014 XMPP server (Prosody) \u2014 <code>jitsi-jicofo-changemaker</code> \u2014 Jitsi conference focus \u2014 <code>jitsi-jvb-changemaker</code> 10000/udp Jitsi video bridge \u2014 <pre><code>docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb\n</code></pre> <p>Firewall requirement</p> <p>Jitsi requires UDP port 10000 open in your firewall for video/audio media traffic. Set <code>JVB_ADVERTISE_IP</code> in <code>.env</code> to your server's public IP address.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#newsletter-email","title":"Newsletter &amp; Email","text":"Container Port Description Feature Flag <code>listmonk-app</code> 9001 Listmonk newsletter platform <code>LISTMONK_SYNC_ENABLED=true</code> <code>listmonk-db</code> 5432 PostgreSQL (Listmonk's own database) \u2014 <code>listmonk-init</code> \u2014 Init container \u2014 creates API user \u2014 <code>mailhog-changemaker</code> 8025 MailHog email capture (development) <code>EMAIL_TEST_MODE=true</code> <pre><code># Newsletter platform\ndocker compose up -d listmonk-app\n\n# Email testing (captures all outgoing emails)\ndocker compose up -d mailhog\n</code></pre> <p>Listmonk has its own PostgreSQL instance separate from the main database. The <code>listmonk-init</code> container auto-creates the API user for platform integration.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#developer-tools","title":"Developer Tools","text":"Container Port Description <code>code-server-changemaker</code> 8888 VS Code in the browser <code>mkdocs-changemaker</code> 4003 MkDocs live preview (hot reload) <code>mkdocs-site-server-changemaker</code> 4004 MkDocs static site server <code>gitea-changemaker</code> 3030 Gitea \u2014 self-hosted Git repository <code>gitea-db</code> \u2014 PostgreSQL (Gitea's database) <code>changemaker-v2-nocodb</code> 8091 NocoDB \u2014 read-only database browser <code>nocodb-init</code> \u2014 Init container \u2014 registers database <code>n8n-changemaker</code> 5678 n8n \u2014 workflow automation <pre><code># Start individual tools\ndocker compose up -d code-server\ndocker compose up -d mkdocs mkdocs-site-server\ndocker compose up -d gitea\ndocker compose up -d nocodb\ndocker compose up -d n8n\n</code></pre> <p>Tip</p> <p><code>mkdocs</code> (port 4003) provides live editing with hot reload for documentation authors. <code>mkdocs-site-server</code> (port 4004) serves the built static site for production visitors.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#utilities","title":"Utilities","text":"Container Port Description <code>mini-qr</code> 8089 QR code PNG generator <code>excalidraw-changemaker</code> 8090 Collaborative whiteboard <code>vaultwarden-changemaker</code> 8445 Vaultwarden \u2014 Bitwarden-compatible password manager <code>vaultwarden-init</code> \u2014 Init container \u2014 configures admin settings <code>homepage-changemaker</code> 3010 Homepage \u2014 service dashboard <pre><code>docker compose up -d mini-qr excalidraw vaultwarden homepage\n</code></pre> <p>Mini QR is used internally by walk sheets and cut export pages to generate printable QR codes.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#monitoring-docker-profile","title":"Monitoring (Docker Profile)","text":"<p>Monitoring services are behind a Docker Compose profile and are not started by default.</p> Container Port Description <code>prometheus-changemaker</code> 9090 Prometheus \u2014 metrics collection <code>grafana-changemaker</code> 3005 Grafana \u2014 monitoring dashboards <code>alertmanager-changemaker</code> 9093 Alertmanager \u2014 alert routing <code>cadvisor-changemaker</code> 8086 cAdvisor \u2014 container metrics <code>node-exporter-changemaker</code> 9100 Node Exporter \u2014 host system metrics <code>redis-exporter-changemaker</code> 9121 Redis Exporter \u2014 Redis metrics <code>gotify-changemaker</code> 8889 Gotify \u2014 push notifications <pre><code># Start the entire monitoring stack\ndocker compose --profile monitoring up -d\n</code></pre> <p>The monitoring stack includes 3 pre-configured Grafana dashboards and 12 custom <code>cm_*</code> Prometheus metrics. See Monitoring for details.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#infrastructure","title":"Infrastructure","text":"Container Port Description <code>newt</code> \u2014 Pangolin tunnel connector (Newt) <code>docker-socket-proxy</code> \u2014 Docker socket proxy for secure container access <pre><code># Newt starts automatically if PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET are set\ndocker compose up -d newt\n</code></pre> <p>The Newt container connects to a Pangolin tunnel server for secure public access without opening inbound ports. See Tunnel for setup.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#subdomain-routing","title":"Subdomain Routing","text":"<p>When Nginx is running, services are accessible via subdomains. The root domain serves documentation only; all application routes are at <code>app.DOMAIN</code>.</p> Subdomain Target Purpose <code>app.DOMAIN</code> Admin (3000) All application routes (admin, public pages, campaigns, map, shifts, media gallery) <code>api.DOMAIN</code> Express API (4000) REST API <code>media.DOMAIN</code> Fastify Media API (4100) Media API <code>DOMAIN</code> MkDocs Static (4004) Documentation / marketing site <code>db.DOMAIN</code> NocoDB (8091) Database browser <code>docs.DOMAIN</code> MkDocs Live (4003) Live documentation preview <code>code.DOMAIN</code> Code Server (8888) Web IDE <code>n8n.DOMAIN</code> n8n (5678) Workflow automation <code>git.DOMAIN</code> Gitea (3030) Git hosting <code>home.DOMAIN</code> Homepage (3010) Service dashboard <code>grafana.DOMAIN</code> Grafana (3005) Metrics visualization <code>listmonk.DOMAIN</code> Listmonk (9001) Newsletter platform <code>qr.DOMAIN</code> Mini QR (8089) QR code generator <code>draw.DOMAIN</code> Excalidraw (8090) Collaborative whiteboard <code>vault.DOMAIN</code> Vaultwarden (8445) Password manager <code>events.DOMAIN</code> Gancio (8092) Event platform <code>chat.DOMAIN</code> Rocket.Chat (8891) Team chat <code>meet.DOMAIN</code> Jitsi Meet (8893) Video conferencing <code>mail.DOMAIN</code> MailHog (8025) Email capture (dev)","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#init-containers","title":"Init Containers","text":"<p>Several services use init containers \u2014 lightweight containers that run once on first startup to bootstrap databases or configuration, then exit with code 0. This pattern is borrowed from Kubernetes.</p> Init Container Purpose <code>listmonk-init</code> Creates the Listmonk API user for platform integration <code>gancio-init</code> Creates the Gancio database in the shared PostgreSQL instance <code>vaultwarden-init</code> Configures Vaultwarden admin settings <code>nocodb-init</code> Registers the main database with NocoDB for browsing <p>Seeing these containers in a \"stopped\" or \"exited (0)\" state is completely normal.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#starting-everything","title":"Starting Everything","text":"<p>To start all services at once (excluding monitoring):</p> <pre><code>docker compose up -d\n</code></pre> <p>To start everything including monitoring:</p> <pre><code>docker compose up -d &amp;&amp; docker compose --profile monitoring up -d\n</code></pre> <p>To see what's running:</p> <pre><code>docker compose ps\n</code></pre> <p>Warning</p> <p>Starting all services at once requires at least 4 GB RAM. For resource-constrained environments, start only the services you need.</p>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/services/#next-steps","title":"Next Steps","text":"<ul> <li>Installation \u2014 setup walkthrough and configuration wizard details</li> <li>Environment Variables \u2014 complete <code>.env</code> reference</li> <li>First Steps \u2014 create your first campaign and volunteer shift</li> </ul>","tags":["reference","getting-started","operator","docker"]},{"location":"docs/getting-started/upgrades/","title":"Updates &amp; Upgrades","text":"<p>Need help getting set up?</p> <p>Bunker Operations provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. Get in touch: bnkops.com | <code>admin@bnkops.ca</code></p> <p>Changemaker Lite includes a built-in upgrade system that pulls code updates, rebuilds containers, runs database migrations, and restarts services \u2014 all while preserving your customizations.</p> <p>There are two ways to upgrade:</p> <ol> <li>Admin GUI \u2014 Check for updates and run upgrades from Settings &gt; System</li> <li>CLI \u2014 Run <code>./scripts/upgrade.sh</code> directly from the command line</li> </ol> <p>Both methods execute the same 6-phase upgrade process.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#prerequisites","title":"Prerequisites","text":"","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#upgrade-watcher-required-for-gui-method","title":"Upgrade Watcher (Required for GUI Method)","text":"<p>The admin GUI triggers upgrades via a systemd path watcher that monitors for trigger files. This must be installed on the host system.</p> <p>Install during initial setup:</p> <p>The <code>config.sh</code> wizard offers to install the watcher automatically (Step 13). If you skipped it, install manually:</p> <pre><code># Edit the systemd units to set your project path and user\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" scripts/systemd/changemaker-upgrade.path &gt; /tmp/changemaker-upgrade.path\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" -e \"s|__USER__|$(whoami)|g\" scripts/systemd/changemaker-upgrade.service &gt; /tmp/changemaker-upgrade.service\n\n# Install and enable\nsudo cp /tmp/changemaker-upgrade.path /tmp/changemaker-upgrade.service /etc/systemd/system/\nsudo systemctl daemon-reload\nsudo systemctl enable --now changemaker-upgrade.path\n</code></pre> <p>Verify it's running:</p> <pre><code>sudo systemctl status changemaker-upgrade.path\n</code></pre> <p>How the watcher works</p> <p>The API container writes a <code>trigger.json</code> file to a shared <code>data/upgrade/</code> volume. The systemd path watcher detects the file and runs <code>scripts/upgrade-watcher.sh</code> on the host, which dispatches to the appropriate script (check or upgrade). Progress and results are communicated back via JSON files that the API reads.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#method-1-admin-gui","title":"Method 1: Admin GUI","text":"","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#checking-for-updates","title":"Checking for Updates","text":"<ol> <li>Navigate to Settings (<code>/app/settings</code>)</li> <li>Click the System tab</li> <li>Click Check for Updates</li> </ol> <p>The System tab shows your current version, last commit message, and auto-upgrade settings:</p> <p></p> <p>The system fetches from the git remote and shows:</p> <ul> <li>Current commit hash and message</li> <li>Remote commit hash (if different)</li> <li>Number of commits behind</li> <li>Changelog of incoming changes</li> </ul> <p>When updates are available, the panel highlights how many commits are behind and lists the incoming changes:</p> <p></p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#starting-an-upgrade","title":"Starting an Upgrade","text":"<ol> <li>Review the changelog to understand what's changing</li> <li>Click Start Upgrade</li> <li>Optionally configure:<ul> <li>Skip backup \u2014 skip the database backup phase (not recommended)</li> <li>Pull images \u2014 also update third-party Docker images (PostgreSQL, Redis, etc.)</li> <li>Use registry images \u2014 pull pre-built images from Gitea instead of compiling from source (faster \u2014 requires <code>scripts/build-and-push.sh</code> to have been run first)</li> <li>Dry run \u2014 preview what would happen without making changes</li> </ul> </li> <li>Monitor the 6-phase progress indicator</li> </ol> <p>The GUI polls for progress updates and displays the current phase, percentage, and status message in real time.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#upgrade-results","title":"Upgrade Results","text":"<p>After the upgrade completes, the System tab shows the result \u2014 including the new version, health check status, and any warnings:</p> <p></p> <p>Tip</p> <p>If health checks show warnings immediately after an upgrade, wait 1-2 minutes for services to fully start before investigating.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#the-6-upgrade-phases","title":"The 6 Upgrade Phases","text":"<p>Both the GUI and CLI methods execute the same 6-phase process:</p> Phase % Name What Happens 1 5% Pre-flight Checks Verifies Docker, git, disk space (2 GB minimum), remote reachability, and clean working directory 2 15% Backup Runs <code>scripts/backup.sh</code> (pg_dump + archive), backs up user-modifiable content, saves pre-upgrade commit hash 3 30% Code Update Saves user paths, stashes local changes, <code>git pull</code>, pops stash with auto-conflict resolution, detects new <code>.env</code> variables 4 50% Container Rebuild Rebuilds <code>api</code>, <code>admin</code>, <code>media-api</code> from source (default) or pulls pre-built images from the Gitea registry (<code>--use-registry</code>); conditionally rebuilds <code>nginx</code> and <code>code-server</code> if their configs changed; optionally pulls third-party images 5 70% Service Restart Stops app containers, force-recreates LSIO containers, verifies Gancio config, starts infrastructure, waits for PostgreSQL, starts API (runs migrations), starts everything else, restarts Newt tunnel and monitoring if they were running 6 90% Verification Health checks for API, Admin, Media API, Gancio, MkDocs; detects containers in restart loops","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#what-gets-preserved","title":"What Gets Preserved","text":"<p>The upgrade script automatically preserves user-modifiable paths that you may have customized:</p> Path What It Contains <code>mkdocs/docs/</code> Your documentation content <code>mkdocs/mkdocs.yml</code> MkDocs configuration <code>mkdocs/site/</code> Built documentation site <code>configs/</code> Prometheus, Grafana, Alertmanager, Homepage configs <code>nginx/conf.d/services.conf</code> Custom nginx service proxies <p>These files are saved before <code>git pull</code> and unconditionally restored afterward, even if the pull introduces changes to them. Your versions always win.</p> <p>Tip</p> <p>The <code>.env</code> file is never touched by <code>git pull</code> (it's in <code>.gitignore</code>). However, if new environment variables are added in <code>.env.example</code>, the upgrade script automatically appends them to your <code>.env</code> with their default values and warns you to review them.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#method-2-cli","title":"Method 2: CLI","text":"<p>Run the upgrade script directly:</p> <pre><code>./scripts/upgrade.sh\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#options","title":"Options","text":"Flag Description <code>--skip-backup</code> Skip the backup phase (requires <code>--force</code>) <code>--pull-services</code> Also pull new third-party Docker images <code>--use-registry</code> Pull pre-built images from Gitea instead of compiling from source <code>--dry-run</code> Show what would happen without executing <code>--force</code> Continue past non-critical warnings <code>--branch BRANCH</code> Git branch to pull (default: current branch) <code>--rollback</code> Rollback to pre-upgrade commit <code>--api-mode</code> Write progress/result JSON for admin GUI (used internally)","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#examples","title":"Examples","text":"<pre><code># Standard upgrade\n./scripts/upgrade.sh\n\n# Preview changes without executing\n./scripts/upgrade.sh --dry-run\n\n# Full upgrade including third-party image updates\n./scripts/upgrade.sh --pull-services\n\n# Upgrade using pre-built images from Gitea registry (faster, no TypeScript compile)\n./scripts/upgrade.sh --use-registry --force --skip-backup\n\n# Rollback to the last pre-upgrade state\n./scripts/upgrade.sh --rollback\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#registry-mode-fast-upgrades","title":"Registry Mode (Fast Upgrades)","text":"<p>By default, the upgrade script compiles TypeScript from source (<code>npm run build</code>) and rebuilds Docker images on the deployment server. Registry mode skips this by pulling pre-built production images from the Gitea container registry \u2014 faster and requires no build tooling on the server.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#how-it-works","title":"How It Works","text":"<ol> <li>Run <code>scripts/build-and-push.sh</code> on a machine with Docker (usually your dev machine) to build and push production images tagged with the current commit SHA</li> <li>During the next upgrade, pass <code>--use-registry</code> (CLI) or enable the checkbox (GUI)</li> <li>The upgrade script pulls <code>gitea.bnkops.com/admin/changemaker-{service}:{sha}</code> instead of rebuilding from source</li> <li>If a registry image is unavailable (e.g., the SHA wasn't pushed), it automatically falls back to a source build</li> </ol>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#building-and-pushing-images","title":"Building and Pushing Images","text":"<pre><code># Build and push all core services (api, admin, media-api, nginx)\n./scripts/build-and-push.sh\n\n# Skip code-server (9 GB \u2014 push only when Dockerfile changes)\n./scripts/build-and-push.sh --services api,admin,media-api,nginx\n\n# Build only, no push (verify locally first)\n./scripts/build-and-push.sh --no-push\n\n# Also mirror third-party images (postgres, redis, etc.) to Gitea\n./scripts/mirror-images.sh\n</code></pre> <p>Registry prerequisites</p> <ul> <li>Run <code>docker login gitea.bnkops.com</code> once per machine before pushing</li> <li>Set <code>GITEA_REGISTRY_USER</code> and <code>GITEA_REGISTRY_PASS</code> in <code>.env</code> for the admin GUI's Registry status endpoint</li> <li>gitea.bnkops.com must be reachable without proxies that limit upload size (Cloudflare free plan blocks blobs &gt;100 MB)</li> </ul> <p>Release installs upgrade automatically via registry</p> <p>If you installed from a release tarball (not git clone), the upgrade script automatically uses registry mode. It downloads the latest release package from Gitea instead of running <code>git pull</code>. No additional configuration needed.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#rollback","title":"Rollback","text":"","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#automatic-rollback","title":"Automatic Rollback","text":"<p>If the upgrade fails at any phase, the script prints detailed rollback instructions including the pre-upgrade commit hash. Use the <code>--rollback</code> flag:</p> <pre><code>./scripts/upgrade.sh --rollback\n</code></pre> <p>This:</p> <ol> <li>Finds the latest backup archive</li> <li>Extracts the pre-upgrade commit hash from <code>git-commit.txt</code> inside the archive</li> <li>Checks out that commit</li> <li>Rebuilds and restarts all containers</li> </ol> <p>Warning</p> <p><code>--rollback</code> restores the code to the pre-upgrade state but does not automatically restore the database. If database migrations were applied during the failed upgrade, you may need to manually restore from the backup archive.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#manual-rollback","title":"Manual Rollback","text":"<pre><code># 1. Restore code\ncd /path/to/changemaker.lite\ngit checkout &lt;pre-upgrade-commit-hash&gt;\n\n# 2. Rebuild and restart\ndocker compose build api admin media-api\ndocker compose up -d\n\n# 3. Database restore (if needed \u2014 destructive!)\nls -lt backups/changemaker-v2-backup-*.tar.gz | head -5\ntar xzf backups/&lt;backup&gt;.tar.gz -C /tmp\ngunzip -c /tmp/&lt;backup&gt;/v2-postgres.sql.gz | \\\n docker exec -i changemaker-v2-postgres psql -U changemaker -d changemaker_v2\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#new-environment-variables","title":"New Environment Variables","text":"<p>When upstream code adds new environment variables to <code>.env.example</code>, the upgrade script automatically:</p> <ol> <li>Compares <code>.env.example</code> against your <code>.env</code></li> <li>Appends any missing variables with their default values</li> <li>Warns you to review the new additions</li> </ol> <pre><code>[WARN] New env vars added to .env (review defaults):\n NEW_FEATURE_FLAG\n NEW_API_KEY\n</code></pre> <p>Always review new variables after an upgrade \u2014 some may need manual configuration.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#update-checker","title":"Update Checker","text":"<p>A separate lightweight script checks for available updates without performing any changes:</p> <pre><code>./scripts/upgrade-check.sh\n</code></pre> <p>This writes <code>data/upgrade/status.json</code> with:</p> <ul> <li>Current and remote commit hashes</li> <li>Number of commits behind</li> <li>Changelog (last 30 commits)</li> <li>Timestamp of last check</li> </ul> <p>The admin GUI reads this file to display update availability.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#troubleshooting","title":"Troubleshooting","text":"","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#stale-progress-indicator","title":"Stale Progress Indicator","text":"<p>If the GUI shows an upgrade \"in progress\" but nothing is happening, the upgrade script may have crashed. The system automatically detects stale progress (no update for 10+ minutes) and treats it as not running.</p> <p>To manually clear:</p> <pre><code>rm -f data/upgrade/progress.json\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#merge-conflicts","title":"Merge Conflicts","text":"<p>If <code>git pull</code> encounters merge conflicts in user-modifiable paths (docs, configs), the upgrade script auto-resolves by keeping your version. If conflicts occur in project-owned files (api/, admin/), the upgrade fails and asks you to resolve manually.</p>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#lock-file","title":"Lock File","text":"<p>The upgrade script uses <code>.upgrade.lock</code> to prevent concurrent upgrades. If a previous upgrade crashed without cleaning up:</p> <pre><code># Verify no upgrade is actually running\nps aux | grep upgrade.sh\n\n# Remove stale lock\nrm -f .upgrade.lock\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#health-check-failures","title":"Health Check Failures","text":"<p>If Phase 6 health checks fail, services may still be starting. Wait 1-2 minutes and check manually:</p> <pre><code># API health\ncurl -s http://localhost:4000/api/health\n\n# Container status\ndocker compose ps\n\n# Recent logs\ndocker compose logs api --tail 50\ndocker compose logs admin --tail 50\n</code></pre>","tags":["guide","operator","upgrades"]},{"location":"docs/getting-started/upgrades/#systemd-watcher-not-triggering","title":"Systemd Watcher Not Triggering","text":"<pre><code># Check watcher status\nsudo systemctl status changemaker-upgrade.path\n\n# Check service logs\nsudo journalctl -u changemaker-upgrade.service --tail 20\n\n# Re-enable if stopped\nsudo systemctl enable --now changemaker-upgrade.path\n</code></pre>","tags":["guide","operator","upgrades"]},{"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>","tags":["reference","operator","services","docker"]},{"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>","tags":["reference","operator","services","docker"]},{"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>","tags":["reference","operator","services","docker"]},{"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>4004</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>","tags":["reference","operator","services","docker"]},{"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>","tags":["reference","operator","services","docker"]},{"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> <li> <p> Vaultwarden</p> <p>Self-hosted Bitwarden-compatible password manager. Secure credential sharing for campaign teams. Requires HTTPS for account creation; local browsing works on HTTP.</p> <p>Port: <code>8445</code> \u00b7 Container: <code>vaultwarden-changemaker</code> \u00b7 Subdomain: <code>vault.DOMAIN</code></p> <p> Vaultwarden Wiki</p> </li> </ul>","tags":["reference","operator","services","docker"]},{"location":"docs/services/#team-communication","title":"Team Communication","text":"<ul> <li> <p> Rocket.Chat</p> <p>Self-hosted team chat for volunteer coordination. Supports channels, direct messaging, threads, and file sharing. Embeddable in the admin dashboard via iframe. Enable with <code>ENABLE_CHAT=true</code>.</p> <p>Port: <code>3000</code> (internal) \u00b7 Container: <code>rocketchat-changemaker</code> \u00b7 Subdomain: <code>chat.DOMAIN</code></p> <p> Rocket.Chat Docs</p> </li> <li> <p> Jitsi Meet</p> <p>Self-hosted video conferencing with JWT authentication. Four containers (web, Prosody, Jicofo, JVB) provide the full video call stack. Integrated with Rocket.Chat for one-click calls from channels and DMs. Enable with <code>ENABLE_MEET=true</code>.</p> <p>Containers: <code>jitsi-web</code>, <code>jitsi-prosody</code>, <code>jitsi-jicofo</code>, <code>jitsi-jvb</code> \u00b7 Subdomain: <code>meet.DOMAIN</code></p> <p> Setup Guide \u00b7 Jitsi Docs</p> </li> <li> <p> Gancio</p> <p>Self-hosted event management platform. Automatic shift-to-event sync (when <code>GANCIO_SYNC_ENABLED=true</code>) publishes shifts as public events. Uses the shared PostgreSQL database. Embeddable calendar widget available for MkDocs pages.</p> <p>Port: <code>8092</code> \u00b7 Container: <code>gancio-changemaker</code> \u00b7 Subdomain: <code>events.DOMAIN</code></p> <p> Gancio Docs</p> </li> </ul>","tags":["reference","operator","services","docker"]},{"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>","tags":["reference","operator","services","docker"]},{"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>3005</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>8086</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>","tags":["reference","operator","services","docker"]},{"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) 4004 (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 Vaultwarden 8445 <code>vault.</code> default Rocket.Chat \u2014 <code>chat.</code> default Jitsi Meet \u2014 <code>meet.</code> default Gancio 8092 <code>events.</code> default Newt (tunnel) \u2014 \u2014 default Prometheus 9090 \u2014 <code>monitoring</code> Grafana 3005 <code>grafana.</code> <code>monitoring</code> Alertmanager 9093 \u2014 <code>monitoring</code> cAdvisor 8086 \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>","tags":["reference","operator","services","docker"]},{"location":"docs/troubleshooting/","title":"Troubleshooting","text":"<p>Common issues and their solutions when running Changemaker Lite.</p>","tags":["troubleshooting","operator"],"boost":2},{"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>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#pangolin-tunnel-403302-errors","title":"Pangolin Tunnel \u2014 403/302 Errors","text":"<p>Symptom: All API endpoints return 302 redirects to Pangolin auth page, or 403 Forbidden.</p> <p>Fix: In the Pangolin dashboard, set each resource to Not Protected (public access). Critical resources to fix first:</p> <ol> <li><code>api.yourdomain.org</code> \u2014 Main API</li> <li><code>app.yourdomain.org</code> \u2014 Admin GUI + public pages</li> <li><code>media.yourdomain.org</code> \u2014 Media API</li> </ol> <p>Verify:</p> <pre><code># Should return JSON, NOT a 302 redirect\ncurl -I https://api.yourdomain.org/api/health\n</code></pre>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#database-connection-failures","title":"Database Connection Failures","text":"<p>Symptom: API logs show database connection errors.</p> <p>Checklist:</p> <ol> <li>Check PostgreSQL: <code>docker compose ps v2-postgres</code></li> <li>Verify <code>DATABASE_URL</code> in <code>.env</code> matches container name and port</li> <li>View logs: <code>docker compose logs v2-postgres --tail 50</code></li> <li>Test connection: <code>docker compose exec api npx prisma db execute --stdin &lt;&lt;&lt; \"SELECT 1\"</code></li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#redis-connection-failures","title":"Redis Connection Failures","text":"<p>Symptom: API logs show Redis connection errors, rate limiting doesn't work.</p> <p>Checklist:</p> <ol> <li>Check Redis: <code>docker compose ps redis-changemaker</code></li> <li>Verify <code>REDIS_PASSWORD</code> and <code>REDIS_URL</code> format (<code>redis://:password@host:port</code>)</li> <li>View logs: <code>docker compose logs redis-changemaker --tail 50</code></li> <li>Test: <code>docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping</code></li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#api-not-starting","title":"API Not Starting","text":"<p>Symptom: API container keeps restarting or won't start.</p> <p>Checklist:</p> <ol> <li>Check logs: <code>docker compose logs api --tail 100</code></li> <li>Verify all required env vars are set (compare with <code>.env.example</code>)</li> <li>Check database is ready: <code>docker compose ps v2-postgres</code> (should show \"healthy\")</li> <li>Run migrations manually: <code>docker compose exec api npx prisma migrate deploy</code></li> <li>Check for port conflicts: <code>ss -tlnp | grep 4000</code></li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#containers-in-restart-loops","title":"Containers in Restart Loops","text":"<p>Symptom: <code>docker compose ps</code> shows containers with \"restarting\" status.</p> <p>Diagnosis:</p> <pre><code># Find restarting containers\ndocker compose ps | grep -i restarting\n\n# Check recent logs for the problem container\ndocker compose logs &lt;service-name&gt; --tail 50\n\n# Check container exit code\ndocker inspect &lt;container-name&gt; --format='{{.State.ExitCode}}'\n</code></pre> <p>Common causes:</p> <ul> <li>Missing environment variables (check <code>.env</code>)</li> <li>Database not ready (healthcheck dependencies)</li> <li>Port already in use by another process</li> <li>Insufficient memory (check with <code>free -h</code>)</li> </ul>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#newt-tunnel-wont-connect","title":"Newt Tunnel Won't Connect","text":"<p>Checklist (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>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#migration-errors","title":"Migration Errors","text":"<p>Symptom: <code>prisma migrate deploy</code> fails.</p> <p>Common fixes:</p> <pre><code># Check migration status\ndocker compose exec api npx prisma migrate status\n\n# If migrations are out of sync, reset (DESTRUCTIVE \u2014 dev only)\ndocker compose exec api npx prisma migrate reset\n\n# If shadow database errors, create one\ndocker compose exec -T v2-postgres createdb -U changemaker prisma_shadow_diff\n</code></pre> <p>Never use <code>prisma db push</code> in production</p> <p>Always use <code>prisma migrate dev</code> (development) or <code>prisma migrate deploy</code> (production) to keep migration history in sync.</p>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#media-api-upload-failures","title":"Media API Upload Failures","text":"<p>Symptom: Video uploads fail with permission errors or 500 status.</p> <p>Checklist:</p> <ol> <li>Verify inbox volume is writable: check <code>media/local/inbox</code> has <code>:rw</code> mount</li> <li>Check disk space: <code>df -h</code></li> <li>Verify FFmpeg is installed in container: <code>docker compose exec media-api ffprobe -version</code></li> <li>Check upload size limit: default is 10 GB in Fastify multipart config</li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#email-not-sending","title":"Email Not Sending","text":"<p>Symptom: Advocacy emails or notifications aren't delivered.</p> <p>Checklist:</p> <ol> <li>Check <code>EMAIL_TEST_MODE</code> \u2014 if <code>true</code>, all emails go to MailHog (<code>http://localhost:8025</code>)</li> <li>Verify SMTP credentials in <code>.env</code> (<code>SMTP_HOST</code>, <code>SMTP_PORT</code>, <code>SMTP_USER</code>, <code>SMTP_PASS</code>)</li> <li>Check BullMQ queue: visit Admin &gt; Email Queue or check logs</li> <li>Test SMTP from Settings: Admin &gt; Settings &gt; Email &gt; Test Connection</li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#services-unreachable-via-tunnel","title":"Services Unreachable via Tunnel","text":"<p>Checklist:</p> <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> <li>Check Pangolin resources are all set to \"Not Protected\"</li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#slow-map-performance","title":"Slow Map Performance","text":"<p>Symptom: Map page is slow or returns 500 errors with many locations.</p> <p>Causes and fixes:</p> <ul> <li>Too many locations loaded at once \u2014 the API limits by address count with debounced bounds queries</li> <li>Missing indexes \u2014 verify database has the 5 performance indexes on Location/Address tables</li> <li>Browser memory \u2014 marker clustering activates above zoom level 18; below that, addresses are grouped</li> </ul>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#docker-disk-space","title":"Docker Disk Space","text":"<p>Symptom: Builds fail, containers can't start, or images won't pull.</p> <pre><code># Check disk usage\ndf -h\n\n# Clean unused Docker resources\ndocker system prune -f\n\n# Clean old images (keep only last 2 days)\ndocker image prune -a --filter \"until=48h\"\n\n# Check what's using space\ndocker system df\n</code></pre>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/troubleshooting/#getting-help","title":"Getting Help","text":"<p>If your issue isn't listed here:</p> <ol> <li>Check the API logs: <code>docker compose logs api --tail 200</code></li> <li>Search the Gitea issues</li> <li>Review the Deployment guide for production-specific issues</li> <li>File a new issue with your logs and <code>.env</code> (redact passwords)</li> </ol>","tags":["troubleshooting","operator"],"boost":2},{"location":"docs/user-guide/","title":"User Guide","text":"<p>This guide covers everything you can do as a visitor or registered supporter \u2014 from contacting representatives to signing up for volunteer shifts.</p>","tags":["guide","user"]},{"location":"docs/user-guide/#what-you-can-do","title":"What You Can Do","text":"<ul> <li> <p> Campaigns</p> <p>Find active advocacy campaigns, look up your representatives, and send emails.</p> </li> <li> <p> Map</p> <p>Explore the interactive map showing locations across your community.</p> </li> <li> <p> Shifts</p> <p>Sign up for volunteer shifts and join canvassing teams.</p> </li> <li> <p> Events</p> <p>Browse upcoming events and RSVP through the event calendar.</p> </li> <li> <p> Gallery</p> <p>Watch campaign videos, browse photos, and explore playlists.</p> </li> <li> <p> Shop &amp; Pricing</p> <p>Purchase campaign merchandise or subscribe to a membership plan.</p> </li> <li> <p> Donations</p> <p>Support the cause with one-time donations on branded pages.</p> </li> <li> <p> Your Profile</p> <p>View and manage your contact profile, preferences, and activity history.</p> </li> </ul>","tags":["guide","user"]},{"location":"docs/user-guide/campaigns/","title":"Campaigns","text":"<p>Browse active advocacy campaigns and contact your elected representatives.</p>","tags":["guide","user","influence","campaigns"]},{"location":"docs/user-guide/campaigns/#how-it-works","title":"How It Works","text":"<ol> <li>Browse campaigns at <code>/campaigns</code> \u2014 see active campaigns with descriptions and email counts</li> <li>Pick a campaign \u2014 read about the issue and who it targets</li> <li>Enter your postal code \u2014 the system looks up your federal, provincial, municipal, and school board representatives</li> <li>Send the email \u2014 use \"Send Now\" to send through the platform, or open it in your own email app (Gmail, Outlook, etc.)</li> <li>Share the response \u2014 if a representative replies, submit it to the public Response Wall</li> </ol>","tags":["guide","user","influence","campaigns"]},{"location":"docs/user-guide/campaigns/#response-wall","title":"Response Wall","text":"<p>Each campaign has a public response wall where supporters share how their representatives responded. Responses can be upvoted and are moderated by admins. Verified responses display a trust badge.</p>","tags":["guide","user","influence","campaigns"]},{"location":"docs/user-guide/campaigns/#submit-your-own-campaign","title":"Submit Your Own Campaign","text":"<p>Registered users can draft and submit their own advocacy campaigns at <code>/campaigns/create</code>. Submissions go through admin review before being published.</p>","tags":["guide","user","influence","campaigns"]},{"location":"docs/user-guide/campaigns/#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> <li><code>/campaigns/create</code> \u2014 submit a user-generated campaign (requires login)</li> <li><code>/campaigns/mine</code> \u2014 manage your submitted campaigns (requires login)</li> </ul>","tags":["guide","user","influence","campaigns"]},{"location":"docs/user-guide/donations/","title":"Donations","text":"<p>Make one-time contributions on branded donation pages.</p>","tags":["guide","user","payments"]},{"location":"docs/user-guide/donations/#how-it-works","title":"How It Works","text":"<ol> <li>Browse donation pages at <code>/donate</code></li> <li>Choose a campaign to support</li> <li>Select a suggested amount or enter a custom amount</li> <li>Complete payment securely through Stripe</li> <li>Receive a confirmation with a thank-you message</li> </ol>","tags":["guide","user","payments"]},{"location":"docs/user-guide/donations/#donation-pages","title":"Donation Pages","text":"<p>Each donation page has:</p> <ul> <li>Custom branding \u2014 unique title, description, and cover image</li> <li>Suggested amounts \u2014 pre-set donation tiers for quick selection</li> <li>Goal tracking \u2014 progress bar showing how close the campaign is to its fundraising goal</li> <li>Anonymous giving \u2014 option to donate without displaying your name</li> </ul>","tags":["guide","user","payments"]},{"location":"docs/user-guide/donations/#public-routes","title":"Public Routes","text":"<ul> <li><code>/donate</code> \u2014 browse donation pages</li> <li><code>/donate/:slug</code> \u2014 donate on a specific campaign page</li> </ul>","tags":["guide","user","payments"]},{"location":"docs/user-guide/events/","title":"Events (Gancio)","text":"<p>Integrated with Gancio for self-hosted event management. When enabled, volunteer shifts are automatically published as public events.</p>","tags":["guide","user","events"]},{"location":"docs/user-guide/events/#shift-to-event-sync","title":"Shift-to-Event Sync","text":"<p>When <code>GANCIO_SYNC_ENABLED=true</code>, the platform:</p> <ol> <li>Creates a Gancio event whenever a new shift is published</li> <li>Updates the event if the shift time, location, or details change</li> <li>Deletes the event if the shift is cancelled</li> </ol> <p>Sync uses OAuth authentication with the Gancio admin account.</p>","tags":["guide","user","events"]},{"location":"docs/user-guide/events/#key-features","title":"Key Features","text":"<ul> <li>Automatic sync \u2014 shifts appear as public events without manual entry</li> <li>Embeddable calendar \u2014 GrapesJS block and MkDocs widget for embedding the event calendar on pages</li> <li>Public events page \u2014 linked from the public navigation when <code>enableEvents</code> is enabled in settings</li> </ul>","tags":["guide","user","events"]},{"location":"docs/user-guide/events/#admin-routes","title":"Admin Routes","text":"<ul> <li><code>/app/gancio</code> \u2014 Gancio service status and iframe embed</li> </ul>","tags":["guide","user","events"]},{"location":"docs/user-guide/events/#public-routes","title":"Public Routes","text":"<ul> <li><code>/events</code> \u2014 public events navigation link (when enabled)</li> <li><code>events.DOMAIN</code> \u2014 Gancio web interface for browsing and RSVPs</li> </ul>","tags":["guide","user","events"]},{"location":"docs/user-guide/gallery/","title":"Gallery","text":"<p>The public gallery at <code>/gallery</code> showcases campaign videos, photos, and curated playlists.</p>","tags":["guide","user","media","gallery"]},{"location":"docs/user-guide/gallery/#videos","title":"Videos","text":"<ul> <li>Browse by category \u2014 videos organized into categories with thumbnails and durations</li> <li>Video player \u2014 full playback with engagement features (reactions, comments)</li> <li>Shorts feed \u2014 TikTok-style vertical video feed for clips under 60 seconds at <code>/gallery/shorts</code></li> </ul>","tags":["guide","user","media","gallery"]},{"location":"docs/user-guide/gallery/#photos","title":"Photos","text":"<ul> <li>Photo albums \u2014 browse photos organized into named collections</li> <li>Reactions and comments \u2014 engage with individual photos</li> </ul>","tags":["guide","user","media","gallery"]},{"location":"docs/user-guide/gallery/#playlists","title":"Playlists","text":"<ul> <li>Curated playlists \u2014 admin and community-created video collections</li> <li>Featured carousel \u2014 highlighted playlists on the gallery homepage</li> <li>Playlist viewer \u2014 continuous playback with up-next queue at <code>/gallery/playlist/:id</code></li> </ul>","tags":["guide","user","media","gallery"]},{"location":"docs/user-guide/gallery/#public-routes","title":"Public Routes","text":"<ul> <li><code>/gallery</code> \u2014 public video and photo gallery</li> <li><code>/gallery/watch/:id</code> \u2014 watch a specific video</li> <li><code>/gallery/playlist/:id</code> \u2014 view a playlist</li> <li><code>/gallery/shorts</code> \u2014 browse the shorts feed</li> </ul>","tags":["guide","user","media","gallery"]},{"location":"docs/user-guide/map/","title":"Map","text":"<p>The public map at <code>/map</code> shows locations across your community on an interactive Leaflet map.</p>","tags":["guide","user","map"]},{"location":"docs/user-guide/map/#features","title":"Features","text":"<ul> <li>Interactive map \u2014 zoom, pan, and click markers to see address details</li> <li>Color-coded markers \u2014 locations are color-coded based on their status</li> <li>Cluster groups \u2014 markers group together when zoomed out for better performance</li> <li>Fullscreen mode \u2014 expand the map to fill your screen</li> </ul>","tags":["guide","user","map"]},{"location":"docs/user-guide/map/#public-routes","title":"Public Routes","text":"<ul> <li><code>/map</code> \u2014 public interactive map</li> </ul>","tags":["guide","user","map"]},{"location":"docs/user-guide/profile/","title":"Self-Service Contact Profile","text":"<p>Give supporters a private, token-based link to view and manage their own contact profile -- no login required.</p>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/profile/#how-it-works","title":"How It Works","text":"<ol> <li>An admin generates a profile link from the People CRM -- each link contains a unique 64-character hex token with a configurable expiration (24 hours to 1 year).</li> <li>The supporter opens the link -- if the link is password-protected, they enter the password first. If expired, they see a branded expiration notice.</li> <li>The supporter views and edits their profile -- they can update their name, email, phone, address, and cover photo.</li> <li>Communication preferences -- supporters can opt out of email and/or SMS communications with simple toggle switches.</li> </ol>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/profile/#profile-tabs","title":"Profile Tabs","text":"<ul> <li>Profile -- edit display name, first/last name, email, phone, and address</li> <li>Preferences -- toggle email and SMS opt-out switches</li> <li>Activity -- paginated timeline of all engagement: emails sent, responses submitted, shift signups, canvass visits, donations, video views, and profile edits</li> <li>Social tabs -- if the viewer is a logged-in user viewing their own profile and the social feature is enabled, additional tabs appear: Friends, Feed, Achievements, Notifications, and Discover</li> </ul>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/profile/#security","title":"Security","text":"<ul> <li>Token-based access -- no account or login needed; the URL token grants access</li> <li>Password protection -- admins can optionally set a password on the profile link</li> <li>Expiration -- links expire after a configurable duration, showing a branded message with the expiration date</li> <li>Rate limiting -- separate rate limits on profile views, edits, photo uploads, and password attempts</li> <li>Cover photo -- supporters can upload a JPEG, PNG, or WebP cover photo (max 5 MB), automatically resized to 800x400</li> </ul>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/profile/#engagement-score","title":"Engagement Score","text":"<p>Each profile shows a circular engagement score (0-100) calculated from the contact's activity across the platform -- emails, shifts, canvass visits, donations, and video views.</p>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/profile/#public-routes","title":"Public Routes","text":"<ul> <li><code>/profile/:token</code> -- self-service contact profile page</li> </ul>","tags":["guide","user","CRM"]},{"location":"docs/user-guide/shifts/","title":"Shifts","text":"<p>Browse available volunteer shifts and sign up to participate in canvassing and other campaign activities.</p>","tags":["guide","user","shifts"]},{"location":"docs/user-guide/shifts/#signing-up","title":"Signing Up","text":"<ol> <li>Visit <code>/shifts</code> to see available time slots</li> <li>Pick a shift that works for your schedule</li> <li>Fill in your name and email</li> <li>You'll receive a confirmation email with login credentials</li> </ol>","tags":["guide","user","shifts"]},{"location":"docs/user-guide/shifts/#quick-join","title":"Quick Join","text":"<p>Organizers may share a QR code at events for instant onboarding:</p> <ol> <li>Scan the QR code \u2014 it opens a Quick Join page</li> <li>Enter your email (and optionally your name and phone)</li> <li>Start immediately \u2014 you're logged in and redirected to the volunteer portal with your area pre-loaded</li> </ol> <p>Quick Join creates a temporary 24-hour account. Your organizer can upgrade it to a permanent account afterward.</p>","tags":["guide","user","shifts"]},{"location":"docs/user-guide/shifts/#after-signing-up","title":"After Signing Up","text":"<p>Once you have an account, log in to access the Volunteer Portal where you can:</p> <ul> <li>View your assigned shifts and canvassing areas</li> <li>Open the canvass map for GPS-guided door-to-door outreach</li> <li>Track your activity and visit history</li> </ul> <p>See the Volunteer Guide for the full volunteer experience.</p>","tags":["guide","user","shifts"]},{"location":"docs/user-guide/shifts/#public-routes","title":"Public Routes","text":"<ul> <li><code>/shifts</code> \u2014 browse and sign up for volunteer shifts</li> <li><code>/join?token=...</code> \u2014 quick join via invite link or QR code</li> </ul>","tags":["guide","user","shifts"]},{"location":"docs/user-guide/shop/","title":"Shop &amp; Pricing","text":"<p>Support the campaign by purchasing merchandise or subscribing to a membership plan.</p>","tags":["guide","user","payments"]},{"location":"docs/user-guide/shop/#shop","title":"Shop","text":"<p>Browse available products at <code>/shop</code>:</p> <ul> <li>Campaign merchandise and branded items</li> <li>One-time purchases with Stripe checkout</li> <li>Product details with images and descriptions</li> </ul>","tags":["guide","user","payments"]},{"location":"docs/user-guide/shop/#membership-plans","title":"Membership Plans","text":"<p>View subscription options at <code>/pricing</code>:</p> <ul> <li>Tiered membership plans with different benefits</li> <li>Monthly and yearly billing options</li> <li>Secure recurring payments through Stripe</li> </ul>","tags":["guide","user","payments"]},{"location":"docs/user-guide/shop/#public-routes","title":"Public Routes","text":"<ul> <li><code>/shop</code> \u2014 browse products</li> <li><code>/pricing</code> \u2014 view subscription plans</li> </ul>","tags":["guide","user","payments"]},{"location":"docs/volunteer/","title":"Volunteer Guide","text":"<p>Welcome! This guide walks you through everything you need as a campaign volunteer.</p>","tags":["guide","volunteer"]},{"location":"docs/volunteer/#getting-started","title":"Getting Started","text":"","tags":["guide","volunteer"]},{"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, and fill in your name and email. You'll receive a confirmation email with login credentials.</p>","tags":["guide","volunteer"]},{"location":"docs/volunteer/#2-log-in","title":"2. Log In","text":"<p>Sign in with the email and password from your confirmation at the login page.</p>","tags":["guide","volunteer"]},{"location":"docs/volunteer/#3-explore-the-volunteer-portal","title":"3. Explore the Volunteer Portal","text":"<p>After logging in, you'll land on the volunteer portal. Use the bottom navigation to access:</p> <ul> <li>Map \u2014 your canvassing area with GPS tracking</li> <li>Shifts \u2014 your upcoming and past shifts</li> <li>Friends \u2014 social connections with other volunteers</li> <li>Achievements \u2014 badges and leaderboards</li> </ul>","tags":["guide","volunteer"]},{"location":"docs/volunteer/#in-this-section","title":"In This Section","text":"<ul> <li>Canvassing \u2014 the GPS-guided canvass map, recording visits, and walking routes</li> <li>Shifts \u2014 viewing your assigned shifts, activity log, and route history</li> <li>Social \u2014 friend connections, activity feed, groups, profiles, and privacy settings</li> <li>Achievements \u2014 unlockable badges, progress tracking, and competitive leaderboards</li> </ul>","tags":["guide","volunteer"]},{"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.</p>","tags":["guide","volunteer"]},{"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.</p> <p>Q: My GPS isn't working. A: Allow location access in your browser. Try moving near a window for better signal.</p> <p>Q: I recorded the wrong outcome. A: Visit the same address again and record the correct outcome. The most recent visit counts.</p> <p>Q: How do I sign up for more shifts? A: Visit the public shifts page at <code>/shifts</code>.</p>","tags":["guide","volunteer"]},{"location":"docs/volunteer/achievements/","title":"Achievements &amp; Leaderboard","text":"<p>Recognize volunteer contributions with unlockable achievement badges and competitive leaderboards.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#how-it-works","title":"How It Works","text":"<p>Achievements are checked automatically when relevant actions occur (e.g., signing up for a shift, completing a canvass session, accepting a friend request). When a user's progress meets the threshold, the badge is unlocked and an in-app notification is sent.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#badge-categories","title":"Badge Categories","text":"","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#shifts","title":"Shifts","text":"Badge Name Threshold Description FIRST_SHIFT First Steps 1 confirmed signup Sign up for your first volunteer shift SHIFT_STREAK_3 Reliable Volunteer 3 confirmed signups Sign up for 3 volunteer shifts SHIFT_STREAK_10 Shift Champion 10 confirmed signups Sign up for 10 volunteer shifts","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#canvassing","title":"Canvassing","text":"Badge Name Threshold Description FIRST_CANVASS Door Knocker 1 completed session Complete your first canvass session CANVASS_50_DOORS Neighbourhood Explorer 50 visits Record 50 canvass visits CANVASS_100_DOORS Community Connector 100 visits Record 100 canvass visits CANVASS_500_DOORS Door-to-Door Legend 500 visits Record 500 canvass visits","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#campaigns","title":"Campaigns","text":"Badge Name Threshold Description FIRST_CAMPAIGN_EMAIL Voice Heard 1 email sent Send your first advocacy email CAMPAIGN_CHAMPION Campaign Champion 5 distinct campaigns Participate in 5 different campaigns","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#social","title":"Social","text":"Badge Name Threshold Description SOCIAL_BUTTERFLY Social Butterfly 10 accepted friends Make 10 friends on the platform TEAM_PLAYER Team Player 3 group memberships Be a member of 3 groups","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#progress-tracking","title":"Progress Tracking","text":"<p>Each badge displays a progress bar showing current progress toward the threshold. Already-unlocked badges show the unlock date and the progress value at unlock time or the current count (whichever is higher).</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#leaderboards","title":"Leaderboards","text":"<p>The Achievements page includes a leaderboard tab with three ranking types:</p> <ul> <li>Canvass \u2014 ranked by total canvass visits recorded</li> <li>Shifts \u2014 ranked by total confirmed shift signups</li> <li>Campaigns \u2014 ranked by number of distinct campaigns participated in</li> </ul> <p>Leaderboard entries show rank, user name, and score. Users who have disabled \"Show in Friend Activity\" in their privacy settings are excluded from leaderboard rankings to respect their privacy choices.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#volunteer-stats","title":"Volunteer Stats","text":"<p>The Achievements page also displays aggregate stats for the current user:</p> <ul> <li>Confirmed shift signups</li> <li>Completed canvass sessions</li> <li>Total canvass visits</li> <li>Advocacy emails sent</li> <li>Campaigns participated in</li> <li>Friend count</li> <li>Group memberships</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/achievements/#volunteer-routes","title":"Volunteer Routes","text":"<ul> <li><code>/volunteer/achievements</code> \u2014 badge gallery, progress bars, leaderboard tabs, and personal stats</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/canvassing/","title":"Canvassing","text":"<p>The volunteer canvass map is your main tool for door-to-door outreach \u2014 a full-screen GPS-tracked experience.</p>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#the-volunteer-map","title":"The Volunteer Map","text":"","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#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)</li> <li>Clusters \u2014 when zoomed out, markers group together showing the address count. 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>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#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 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>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#sessions","title":"Sessions","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, manually tap the correct marker on the map</li> </ul>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#routes","title":"Routes","text":"<p>The Routes tab shows your past canvassing routes on a map, helping you see which areas you've covered and plan your next outing.</p>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/canvassing/#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/activity</code> \u2014 visit history and outcome breakdown</li> <li><code>/volunteer/routes</code> \u2014 past canvassing routes</li> </ul>","tags":["guide","volunteer","map","canvassing"]},{"location":"docs/volunteer/shifts/","title":"Your Shifts","text":"<p>View your upcoming and past volunteer shifts from the Shifts tab in the bottom navigation.</p>","tags":["guide","volunteer","shifts"]},{"location":"docs/volunteer/shifts/#shift-details","title":"Shift Details","text":"<p>Each shift shows:</p> <ul> <li>Date and time</li> <li>Assigned area (if linked to a canvassing territory)</li> <li>A button to open the canvass map for that area</li> </ul>","tags":["guide","volunteer","shifts"]},{"location":"docs/volunteer/shifts/#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 notes</li> <li>Stats \u2014 total visits, addresses covered, and sessions completed</li> </ul>","tags":["guide","volunteer","shifts"]},{"location":"docs/volunteer/shifts/#volunteer-routes","title":"Volunteer Routes","text":"<ul> <li><code>/volunteer/shifts</code> \u2014 view assigned shifts</li> <li><code>/volunteer/activity</code> \u2014 visit history and outcome breakdown</li> </ul>","tags":["guide","volunteer","shifts"]},{"location":"docs/volunteer/social/","title":"Social Connections","text":"<p>Connect with fellow volunteers through friend requests, activity feeds, team groups, and real-time notifications. Enable via Settings &gt; Feature Toggles &gt; Social Connections.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#friends","title":"Friends","text":"<ul> <li>Send requests \u2014 search for other volunteers and send friend requests</li> <li>Accept / decline / cancel \u2014 manage requests from the Friends page</li> <li>Mutual friends \u2014 view shared connections between users</li> <li>Block / unblock \u2014 blocked users cannot send requests or appear in suggestions</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#discover","title":"Discover","text":"<p>The Discover page suggests potential friends using a ranked scoring algorithm based on:</p> <ul> <li>Household/family connections (highest priority)</li> <li>Mutual friends</li> <li>Shared shifts (co-volunteers from the last 90 days)</li> <li>Shared campaigns (co-participants from the last 90 days)</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#activity-feed","title":"Activity Feed","text":"<p>The Social Feed at <code>/volunteer/feed</code> shows recent activity from your friends:</p> <ul> <li>Shift signups, campaign emails, canvass sessions, and response submissions</li> <li>Limited to the last 30 days (max 50 items)</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#groups","title":"Groups","text":"<p>Groups are automatically created based on platform activity:</p> <ul> <li>Shift teams \u2014 created when 2+ volunteers share a shift</li> <li>Campaign teams \u2014 created when 2+ users participate in the same campaign</li> </ul>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#profiles","title":"Profiles","text":"<p>Each volunteer has a social profile showing volunteer stats, achievement badges, friendship status, and recent activity.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#pokes","title":"Pokes","text":"<p>Send a friendly nudge to any accepted friend (24-hour cooldown per pair).</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#privacy-settings","title":"Privacy Settings","text":"Setting Default Description Show online status On Whether friends see you as online Show in friend activity On Whether your actions appear in feeds Allow friend requests On Whether others can send you requests","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#digest-emails","title":"Digest Emails","text":"<p>Opt into periodic social digest emails with friend activity, unread notifications, and pending requests. Choose daily or weekly frequency.</p>","tags":["guide","volunteer","social"]},{"location":"docs/volunteer/social/#volunteer-routes","title":"Volunteer Routes","text":"<ul> <li><code>/volunteer/feed</code> \u2014 social activity feed</li> <li><code>/volunteer/friends</code> \u2014 friends, requests, blocked, and groups</li> <li><code>/volunteer/discover</code> \u2014 ranked friend suggestions</li> <li><code>/volunteer/profile</code> \u2014 your social profile</li> <li><code>/volunteer/profile/:userId</code> \u2014 another volunteer's profile</li> <li><code>/volunteer/notifications</code> \u2014 notification center and preferences</li> <li><code>/volunteer/groups/:id</code> \u2014 group detail with member list</li> </ul>","tags":["guide","volunteer","social"]},{"location":"blog/archive/2026/","title":"2026","text":""},{"location":"blog/category/testing/","title":"Testing","text":""},{"location":"blog/category/announcements/","title":"Announcements","text":""},{"location":"blog/category/platform/","title":"Platform","text":""}]}