diff --git a/mkdocs/.cache/plugin/social/assets/images/social/testing.png b/mkdocs/.cache/plugin/social/assets/images/social/testing.png new file mode 100644 index 00000000..a8ec542a Binary files /dev/null and b/mkdocs/.cache/plugin/social/assets/images/social/testing.png differ diff --git a/mkdocs/.cache/plugin/social/manifest.json b/mkdocs/.cache/plugin/social/manifest.json index c13d2e3e..5952975d 100644 --- a/mkdocs/.cache/plugin/social/manifest.json +++ b/mkdocs/.cache/plugin/social/manifest.json @@ -145,6 +145,7 @@ "assets/images/social/test-2.png": "a6ae43d52d7c58fc106a562777e03b7da2263f83", "assets/images/social/test-page.png": "c9d5751a1f0a4c1341336bb7d00c9bc743d33ef4", "assets/images/social/test.png": "a6ae43d52d7c58fc106a562777e03b7da2263f83", + "assets/images/social/testing.png": "f7aaf394b71cbe7084a6afa0e75a324ca59e23d8", "assets/images/social/v1/adv/ansible.png": "cb542ad9a3cc9a869258b3b1353966e1b9616a2b", "assets/images/social/v1/adv/index.png": "faa3ec092003114c031995ba6258c4d43f4262a4", "assets/images/social/v1/adv/vscode-ssh.png": "7c88c30c6bfb74736a308407d1a3b7cf3381d42b", diff --git a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json index 787e9886..9e7c8e8d 100644 --- a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json +++ b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json @@ -7,10 +7,10 @@ "stars_count": 0, "forks_count": 0, "open_issues_count": 23, - "updated_at": "2026-03-07T13:10:15-07:00", + "updated_at": "2026-03-08T18:11:30-06:00", "created_at": "2025-05-28T14:54:59-06:00", "clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git", "ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git", "default_branch": "main", - "last_build_update": "2026-03-07T13:10:15-07:00" + "last_build_update": "2026-03-08T18:11:30-06:00" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/anthropics-claude-code.json b/mkdocs/docs/assets/repo-data/anthropics-claude-code.json index 3ad69445..331efaf4 100644 --- a/mkdocs/docs/assets/repo-data/anthropics-claude-code.json +++ b/mkdocs/docs/assets/repo-data/anthropics-claude-code.json @@ -4,10 +4,10 @@ "description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.", "html_url": "https://github.com/anthropics/claude-code", "language": "Shell", - "stars_count": 74978, - "forks_count": 6017, - "open_issues_count": 5743, - "updated_at": "2026-03-07T23:09:30Z", + "stars_count": 75344, + "forks_count": 6074, + "open_issues_count": 5793, + "updated_at": "2026-03-09T00:15:16Z", "created_at": "2025-02-22T17:41:21Z", "clone_url": "https://github.com/anthropics/claude-code.git", "ssh_url": "git@github.com:anthropics/claude-code.git", diff --git a/mkdocs/docs/assets/repo-data/coder-code-server.json b/mkdocs/docs/assets/repo-data/coder-code-server.json index b89f89a6..88774ea4 100644 --- a/mkdocs/docs/assets/repo-data/coder-code-server.json +++ b/mkdocs/docs/assets/repo-data/coder-code-server.json @@ -4,10 +4,10 @@ "description": "VS Code in the browser", "html_url": "https://github.com/coder/code-server", "language": "TypeScript", - "stars_count": 76525, - "forks_count": 6538, + "stars_count": 76540, + "forks_count": 6539, "open_issues_count": 169, - "updated_at": "2026-03-07T21:35:32Z", + "updated_at": "2026-03-08T21:32:19Z", "created_at": "2019-02-27T16:50:41Z", "clone_url": "https://github.com/coder/code-server.git", "ssh_url": "git@github.com:coder/code-server.git", diff --git a/mkdocs/docs/assets/repo-data/gethomepage-homepage.json b/mkdocs/docs/assets/repo-data/gethomepage-homepage.json index a1fae1d5..f36f747a 100644 --- a/mkdocs/docs/assets/repo-data/gethomepage-homepage.json +++ b/mkdocs/docs/assets/repo-data/gethomepage-homepage.json @@ -4,13 +4,13 @@ "description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.", "html_url": "https://github.com/gethomepage/homepage", "language": "JavaScript", - "stars_count": 28765, - "forks_count": 1808, + "stars_count": 28793, + "forks_count": 1811, "open_issues_count": 1, - "updated_at": "2026-03-07T22:19:42Z", + "updated_at": "2026-03-08T23:44:14Z", "created_at": "2022-08-24T07:29:42Z", "clone_url": "https://github.com/gethomepage/homepage.git", "ssh_url": "git@github.com:gethomepage/homepage.git", "default_branch": "dev", - "last_build_update": "2026-03-07T15:45:18Z" + "last_build_update": "2026-03-08T12:16:51Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/go-gitea-gitea.json b/mkdocs/docs/assets/repo-data/go-gitea-gitea.json index 8ddc88bc..33ade7df 100644 --- a/mkdocs/docs/assets/repo-data/go-gitea-gitea.json +++ b/mkdocs/docs/assets/repo-data/go-gitea-gitea.json @@ -4,13 +4,13 @@ "description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD", "html_url": "https://github.com/go-gitea/gitea", "language": "Go", - "stars_count": 54165, - "forks_count": 6433, - "open_issues_count": 2845, - "updated_at": "2026-03-07T20:46:55Z", + "stars_count": 54180, + "forks_count": 6438, + "open_issues_count": 2846, + "updated_at": "2026-03-08T23:25:42Z", "created_at": "2016-11-01T02:13:26Z", "clone_url": "https://github.com/go-gitea/gitea.git", "ssh_url": "git@github.com:go-gitea/gitea.git", "default_branch": "main", - "last_build_update": "2026-03-07T20:41:14Z" + "last_build_update": "2026-03-08T20:49:59Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/knadh-listmonk.json b/mkdocs/docs/assets/repo-data/knadh-listmonk.json index ce5478fa..2a8a7dd9 100644 --- a/mkdocs/docs/assets/repo-data/knadh-listmonk.json +++ b/mkdocs/docs/assets/repo-data/knadh-listmonk.json @@ -4,13 +4,13 @@ "description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.", "html_url": "https://github.com/knadh/listmonk", "language": "Go", - "stars_count": 19210, - "forks_count": 1946, + "stars_count": 19221, + "forks_count": 1945, "open_issues_count": 99, - "updated_at": "2026-03-07T22:13:24Z", + "updated_at": "2026-03-08T23:08:55Z", "created_at": "2019-06-26T05:08:39Z", "clone_url": "https://github.com/knadh/listmonk.git", "ssh_url": "git@github.com:knadh/listmonk.git", "default_branch": "master", - "last_build_update": "2026-03-07T18:41:17Z" + "last_build_update": "2026-03-08T14:04:45Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json b/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json index d9a0fe0a..e1f38051 100644 --- a/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json +++ b/mkdocs/docs/assets/repo-data/lyqht-mini-qr.json @@ -4,10 +4,10 @@ "description": "Create & scan cute qr codes easily \ud83d\udc7e", "html_url": "https://github.com/lyqht/mini-qr", "language": "Vue", - "stars_count": 1897, - "forks_count": 238, + "stars_count": 1898, + "forks_count": 240, "open_issues_count": 21, - "updated_at": "2026-03-07T20:18:46Z", + "updated_at": "2026-03-08T15:02:09Z", "created_at": "2023-04-21T14:20:14Z", "clone_url": "https://github.com/lyqht/mini-qr.git", "ssh_url": "git@github.com:lyqht/mini-qr.git", diff --git a/mkdocs/docs/assets/repo-data/n8n-io-n8n.json b/mkdocs/docs/assets/repo-data/n8n-io-n8n.json index 97603d91..ce79f02d 100644 --- a/mkdocs/docs/assets/repo-data/n8n-io-n8n.json +++ b/mkdocs/docs/assets/repo-data/n8n-io-n8n.json @@ -4,13 +4,13 @@ "description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.", "html_url": "https://github.com/n8n-io/n8n", "language": "TypeScript", - "stars_count": 178023, - "forks_count": 55523, - "open_issues_count": 1415, - "updated_at": "2026-03-07T22:25:31Z", + "stars_count": 178152, + "forks_count": 55558, + "open_issues_count": 1405, + "updated_at": "2026-03-09T00:14:18Z", "created_at": "2019-06-22T09:24:21Z", "clone_url": "https://github.com/n8n-io/n8n.git", "ssh_url": "git@github.com:n8n-io/n8n.git", "default_branch": "master", - "last_build_update": "2026-03-07T20:45:37Z" + "last_build_update": "2026-03-09T00:10:51Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/nocodb-nocodb.json b/mkdocs/docs/assets/repo-data/nocodb-nocodb.json index 2f14b81d..4d171dc0 100644 --- a/mkdocs/docs/assets/repo-data/nocodb-nocodb.json +++ b/mkdocs/docs/assets/repo-data/nocodb-nocodb.json @@ -4,10 +4,10 @@ "description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative", "html_url": "https://github.com/nocodb/nocodb", "language": "TypeScript", - "stars_count": 62371, + "stars_count": 62384, "forks_count": 4655, "open_issues_count": 627, - "updated_at": "2026-03-07T22:40:39Z", + "updated_at": "2026-03-08T23:36:57Z", "created_at": "2017-10-29T18:51:48Z", "clone_url": "https://github.com/nocodb/nocodb.git", "ssh_url": "git@github.com:nocodb/nocodb.git", diff --git a/mkdocs/docs/assets/repo-data/ollama-ollama.json b/mkdocs/docs/assets/repo-data/ollama-ollama.json index f3be3063..8fca8fb1 100644 --- a/mkdocs/docs/assets/repo-data/ollama-ollama.json +++ b/mkdocs/docs/assets/repo-data/ollama-ollama.json @@ -4,13 +4,13 @@ "description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.", "html_url": "https://github.com/ollama/ollama", "language": "Go", - "stars_count": 164368, - "forks_count": 14823, - "open_issues_count": 2590, - "updated_at": "2026-03-07T23:01:55Z", + "stars_count": 164479, + "forks_count": 14834, + "open_issues_count": 2613, + "updated_at": "2026-03-09T00:13:40Z", "created_at": "2023-06-26T19:39:32Z", "clone_url": "https://github.com/ollama/ollama.git", "ssh_url": "git@github.com:ollama/ollama.git", "default_branch": "main", - "last_build_update": "2026-03-07T03:18:54Z" + "last_build_update": "2026-03-08T06:32:28Z" } \ No newline at end of file diff --git a/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json b/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json index b16cb38b..26c440e8 100644 --- a/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json +++ b/mkdocs/docs/assets/repo-data/squidfunk-mkdocs-material.json @@ -4,10 +4,10 @@ "description": "Documentation that simply works", "html_url": "https://github.com/squidfunk/mkdocs-material", "language": "Python", - "stars_count": 26201, - "forks_count": 4047, + "stars_count": 26208, + "forks_count": 4048, "open_issues_count": 2, - "updated_at": "2026-03-07T22:55:28Z", + "updated_at": "2026-03-08T17:43:54Z", "created_at": "2016-01-28T22:09:23Z", "clone_url": "https://github.com/squidfunk/mkdocs-material.git", "ssh_url": "git@github.com:squidfunk/mkdocs-material.git", diff --git a/mkdocs/docs/testing.md b/mkdocs/docs/testing.md new file mode 100644 index 00000000..586198d4 --- /dev/null +++ b/mkdocs/docs/testing.md @@ -0,0 +1,2 @@ +# testing +# testing diff --git a/mkdocs/site/assets/images/social/testing.png b/mkdocs/site/assets/images/social/testing.png new file mode 100644 index 00000000..a8ec542a Binary files /dev/null and b/mkdocs/site/assets/images/social/testing.png differ diff --git a/mkdocs/site/assets/repo-data/admin-changemaker.lite.json b/mkdocs/site/assets/repo-data/admin-changemaker.lite.json index 787e9886..9e7c8e8d 100644 --- a/mkdocs/site/assets/repo-data/admin-changemaker.lite.json +++ b/mkdocs/site/assets/repo-data/admin-changemaker.lite.json @@ -7,10 +7,10 @@ "stars_count": 0, "forks_count": 0, "open_issues_count": 23, - "updated_at": "2026-03-07T13:10:15-07:00", + "updated_at": "2026-03-08T18:11:30-06:00", "created_at": "2025-05-28T14:54:59-06:00", "clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git", "ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git", "default_branch": "main", - "last_build_update": "2026-03-07T13:10:15-07:00" + "last_build_update": "2026-03-08T18:11:30-06:00" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/anthropics-claude-code.json b/mkdocs/site/assets/repo-data/anthropics-claude-code.json index 3ad69445..331efaf4 100644 --- a/mkdocs/site/assets/repo-data/anthropics-claude-code.json +++ b/mkdocs/site/assets/repo-data/anthropics-claude-code.json @@ -4,10 +4,10 @@ "description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.", "html_url": "https://github.com/anthropics/claude-code", "language": "Shell", - "stars_count": 74978, - "forks_count": 6017, - "open_issues_count": 5743, - "updated_at": "2026-03-07T23:09:30Z", + "stars_count": 75344, + "forks_count": 6074, + "open_issues_count": 5793, + "updated_at": "2026-03-09T00:15:16Z", "created_at": "2025-02-22T17:41:21Z", "clone_url": "https://github.com/anthropics/claude-code.git", "ssh_url": "git@github.com:anthropics/claude-code.git", diff --git a/mkdocs/site/assets/repo-data/coder-code-server.json b/mkdocs/site/assets/repo-data/coder-code-server.json index b89f89a6..88774ea4 100644 --- a/mkdocs/site/assets/repo-data/coder-code-server.json +++ b/mkdocs/site/assets/repo-data/coder-code-server.json @@ -4,10 +4,10 @@ "description": "VS Code in the browser", "html_url": "https://github.com/coder/code-server", "language": "TypeScript", - "stars_count": 76525, - "forks_count": 6538, + "stars_count": 76540, + "forks_count": 6539, "open_issues_count": 169, - "updated_at": "2026-03-07T21:35:32Z", + "updated_at": "2026-03-08T21:32:19Z", "created_at": "2019-02-27T16:50:41Z", "clone_url": "https://github.com/coder/code-server.git", "ssh_url": "git@github.com:coder/code-server.git", diff --git a/mkdocs/site/assets/repo-data/gethomepage-homepage.json b/mkdocs/site/assets/repo-data/gethomepage-homepage.json index a1fae1d5..f36f747a 100644 --- a/mkdocs/site/assets/repo-data/gethomepage-homepage.json +++ b/mkdocs/site/assets/repo-data/gethomepage-homepage.json @@ -4,13 +4,13 @@ "description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.", "html_url": "https://github.com/gethomepage/homepage", "language": "JavaScript", - "stars_count": 28765, - "forks_count": 1808, + "stars_count": 28793, + "forks_count": 1811, "open_issues_count": 1, - "updated_at": "2026-03-07T22:19:42Z", + "updated_at": "2026-03-08T23:44:14Z", "created_at": "2022-08-24T07:29:42Z", "clone_url": "https://github.com/gethomepage/homepage.git", "ssh_url": "git@github.com:gethomepage/homepage.git", "default_branch": "dev", - "last_build_update": "2026-03-07T15:45:18Z" + "last_build_update": "2026-03-08T12:16:51Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/go-gitea-gitea.json b/mkdocs/site/assets/repo-data/go-gitea-gitea.json index 8ddc88bc..33ade7df 100644 --- a/mkdocs/site/assets/repo-data/go-gitea-gitea.json +++ b/mkdocs/site/assets/repo-data/go-gitea-gitea.json @@ -4,13 +4,13 @@ "description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD", "html_url": "https://github.com/go-gitea/gitea", "language": "Go", - "stars_count": 54165, - "forks_count": 6433, - "open_issues_count": 2845, - "updated_at": "2026-03-07T20:46:55Z", + "stars_count": 54180, + "forks_count": 6438, + "open_issues_count": 2846, + "updated_at": "2026-03-08T23:25:42Z", "created_at": "2016-11-01T02:13:26Z", "clone_url": "https://github.com/go-gitea/gitea.git", "ssh_url": "git@github.com:go-gitea/gitea.git", "default_branch": "main", - "last_build_update": "2026-03-07T20:41:14Z" + "last_build_update": "2026-03-08T20:49:59Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/knadh-listmonk.json b/mkdocs/site/assets/repo-data/knadh-listmonk.json index ce5478fa..2a8a7dd9 100644 --- a/mkdocs/site/assets/repo-data/knadh-listmonk.json +++ b/mkdocs/site/assets/repo-data/knadh-listmonk.json @@ -4,13 +4,13 @@ "description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.", "html_url": "https://github.com/knadh/listmonk", "language": "Go", - "stars_count": 19210, - "forks_count": 1946, + "stars_count": 19221, + "forks_count": 1945, "open_issues_count": 99, - "updated_at": "2026-03-07T22:13:24Z", + "updated_at": "2026-03-08T23:08:55Z", "created_at": "2019-06-26T05:08:39Z", "clone_url": "https://github.com/knadh/listmonk.git", "ssh_url": "git@github.com:knadh/listmonk.git", "default_branch": "master", - "last_build_update": "2026-03-07T18:41:17Z" + "last_build_update": "2026-03-08T14:04:45Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/lyqht-mini-qr.json b/mkdocs/site/assets/repo-data/lyqht-mini-qr.json index d9a0fe0a..e1f38051 100644 --- a/mkdocs/site/assets/repo-data/lyqht-mini-qr.json +++ b/mkdocs/site/assets/repo-data/lyqht-mini-qr.json @@ -4,10 +4,10 @@ "description": "Create & scan cute qr codes easily \ud83d\udc7e", "html_url": "https://github.com/lyqht/mini-qr", "language": "Vue", - "stars_count": 1897, - "forks_count": 238, + "stars_count": 1898, + "forks_count": 240, "open_issues_count": 21, - "updated_at": "2026-03-07T20:18:46Z", + "updated_at": "2026-03-08T15:02:09Z", "created_at": "2023-04-21T14:20:14Z", "clone_url": "https://github.com/lyqht/mini-qr.git", "ssh_url": "git@github.com:lyqht/mini-qr.git", diff --git a/mkdocs/site/assets/repo-data/n8n-io-n8n.json b/mkdocs/site/assets/repo-data/n8n-io-n8n.json index 97603d91..ce79f02d 100644 --- a/mkdocs/site/assets/repo-data/n8n-io-n8n.json +++ b/mkdocs/site/assets/repo-data/n8n-io-n8n.json @@ -4,13 +4,13 @@ "description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.", "html_url": "https://github.com/n8n-io/n8n", "language": "TypeScript", - "stars_count": 178023, - "forks_count": 55523, - "open_issues_count": 1415, - "updated_at": "2026-03-07T22:25:31Z", + "stars_count": 178152, + "forks_count": 55558, + "open_issues_count": 1405, + "updated_at": "2026-03-09T00:14:18Z", "created_at": "2019-06-22T09:24:21Z", "clone_url": "https://github.com/n8n-io/n8n.git", "ssh_url": "git@github.com:n8n-io/n8n.git", "default_branch": "master", - "last_build_update": "2026-03-07T20:45:37Z" + "last_build_update": "2026-03-09T00:10:51Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/nocodb-nocodb.json b/mkdocs/site/assets/repo-data/nocodb-nocodb.json index 2f14b81d..4d171dc0 100644 --- a/mkdocs/site/assets/repo-data/nocodb-nocodb.json +++ b/mkdocs/site/assets/repo-data/nocodb-nocodb.json @@ -4,10 +4,10 @@ "description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative", "html_url": "https://github.com/nocodb/nocodb", "language": "TypeScript", - "stars_count": 62371, + "stars_count": 62384, "forks_count": 4655, "open_issues_count": 627, - "updated_at": "2026-03-07T22:40:39Z", + "updated_at": "2026-03-08T23:36:57Z", "created_at": "2017-10-29T18:51:48Z", "clone_url": "https://github.com/nocodb/nocodb.git", "ssh_url": "git@github.com:nocodb/nocodb.git", diff --git a/mkdocs/site/assets/repo-data/ollama-ollama.json b/mkdocs/site/assets/repo-data/ollama-ollama.json index f3be3063..8fca8fb1 100644 --- a/mkdocs/site/assets/repo-data/ollama-ollama.json +++ b/mkdocs/site/assets/repo-data/ollama-ollama.json @@ -4,13 +4,13 @@ "description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.", "html_url": "https://github.com/ollama/ollama", "language": "Go", - "stars_count": 164368, - "forks_count": 14823, - "open_issues_count": 2590, - "updated_at": "2026-03-07T23:01:55Z", + "stars_count": 164479, + "forks_count": 14834, + "open_issues_count": 2613, + "updated_at": "2026-03-09T00:13:40Z", "created_at": "2023-06-26T19:39:32Z", "clone_url": "https://github.com/ollama/ollama.git", "ssh_url": "git@github.com:ollama/ollama.git", "default_branch": "main", - "last_build_update": "2026-03-07T03:18:54Z" + "last_build_update": "2026-03-08T06:32:28Z" } \ No newline at end of file diff --git a/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json b/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json index b16cb38b..26c440e8 100644 --- a/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json +++ b/mkdocs/site/assets/repo-data/squidfunk-mkdocs-material.json @@ -4,10 +4,10 @@ "description": "Documentation that simply works", "html_url": "https://github.com/squidfunk/mkdocs-material", "language": "Python", - "stars_count": 26201, - "forks_count": 4047, + "stars_count": 26208, + "forks_count": 4048, "open_issues_count": 2, - "updated_at": "2026-03-07T22:55:28Z", + "updated_at": "2026-03-08T17:43:54Z", "created_at": "2016-01-28T22:09:23Z", "clone_url": "https://github.com/squidfunk/mkdocs-material.git", "ssh_url": "git@github.com:squidfunk/mkdocs-material.git", diff --git a/mkdocs/site/search/search_index.json b/mkdocs/site/search/search_index.json index f6201d4e..6cce552a 100644 --- a/mkdocs/site/search/search_index.json +++ b/mkdocs/site/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\u200b\\-_,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"test/","title":"Test","text":"
Testing page.
Loading... PNG 1920\u00d71040 vlcsnap-2026-01-09-15h39m52s898.png 0 views View \u2192 \ud83d\uded2 DIGITAL Test Product 1A test product
$90.00
Buy Now \u2014 $90.00Secure payment via Stripe. Browse all products
View in Shop\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Make my donation anonymous DonateSecure payment via Stripe. Open full donate page
Donate Now\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Amount\ud83d\uded2
Browse Our ProductsReports, toolkits, event tickets, and more.
Shop Now Choose Your PlanGet access to exclusive content and features.
View Plans\u2764\ufe0f
Support Our CauseYour contribution helps us create lasting change in our community.
Donate Now 9:54 Testing This Sucker 0 views Watch \u2192"},{"location":"blog/","title":"Blog","text":""},{"location":"comments/callback/","title":"Signing in...","text":"Completing sign in...
You will be redirected back to the page you were on.
"},{"location":"docs/","title":"Documentation","text":"Welcome to the Changemaker Lite documentation. Whether you're a campaign volunteer, an admin managing operations, or a sysadmin deploying the platform \u2014 start here.
"},{"location":"docs/#use-the-platform","title":"Use the Platform","text":"Getting Started
Install Changemaker Lite, create your first admin account, and explore the dashboard.
Getting Started
Feature Guides
Campaigns, email advocacy, response walls, map locations, landing pages, and media.
Feature Guides
Administration
User management, roles and permissions, site settings, email templates, and newsletters.
Administration
Volunteer Guide
Sign up for shifts, use the canvassing map, record visits, and track your activity.
Volunteer Guide
Deployment
Docker Compose setup, environment variables, SSL/TLS, backups, and production checklist.
Deployment
Architecture
Dual API design, database schema, authentication flow, and system diagram.
Architecture
Services
Nginx routing, Redis, PostgreSQL, Listmonk, MkDocs, Gitea, NocoDB, and more.
Services
Monitoring
Prometheus metrics, Grafana dashboards, Alertmanager rules, and health checks.
Monitoring Coming soon
API Reference
REST endpoints for auth, campaigns, locations, shifts, media, and more.
API Reference
Troubleshooting
Common errors, CORS issues, database problems, tunnel debugging, and FAQ.
Troubleshooting
Security
Password policy, rate limiting, token rotation, encryption, and audit report.
Security See Deployment
Contributing
Development setup, code style, git workflow, and pull request guidelines.
Contributing Coming soon
New here?
Start with the Getting Started guide to have the platform running in under 30 minutes.
Looking for the source?
Changemaker Lite is 100% open source. Browse the code on Gitea.
"},{"location":"docs/phil/","title":"Philosophy","text":""},{"location":"docs/phil/#software-is-political","title":"Software Is Political","text":"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.
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.
Changemaker Lite exists because we believe organizational independence requires technological independence.
"},{"location":"docs/phil/#the-extractive-model","title":"The Extractive Model","text":"Most campaign and political software is extractive by design. The pattern is familiar:
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.
Every subscription to corporate software funds the machine you're fighting.
"},{"location":"docs/phil/#the-alternative-grow-power-dont-rent-it","title":"The Alternative: Grow Power, Don't Rent It","text":"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?\"
Growing change means:
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.
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.
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.
Workers, with the right tools, will build the future.
"},{"location":"docs/phil/#de-corp-your-stack","title":"De-Corp Your Stack","text":"The practical work of digital sovereignty starts with replacing corporate services one at a time:
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 canvassingThe 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.
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.
"},{"location":"docs/phil/#security-culture-starts-with-infrastructure","title":"Security Culture Starts With Infrastructure","text":"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.
Key principles:
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?
"},{"location":"docs/phil/#our-principles","title":"Our Principles","text":"Liberation First Technology should center marginalized voices. The tools we build reflect the values we hold, and they shape the movements that use them.
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.
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.
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.
"},{"location":"docs/phil/#further-reading","title":"Further Reading","text":"These articles explore the ideas behind Changemaker Lite in depth:
The admin panel at /app 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.
Dashboard
Live overview of platform activity, upcoming shifts, email stats, and service health.
People & Access
User management, roles, the People CRM, and contact merging.
Advocacy
Campaigns, response moderation, representative lookup, and email queue monitoring.
Broadcast
Newsletter sync, email templates, and SMS campaigns.
Web Content
Landing pages, homepage, navigation menu, and documentation management.
Map & Canvassing
Locations, areas, shifts, canvassing dashboard, data quality, and map settings.
Media
Video/photo library, analytics, playlists, comment moderation, and gallery ads.
Payments
Products, donations, subscription plans, and Stripe configuration.
Services
Tunnel management, monitoring, and third-party integrations.
Settings
Organization branding, theme colors, email config, feature toggles, and notifications.
SUPER_ADMIN Full platform access INFLUENCE_ADMIN Campaigns, responses, email queue MAP_ADMIN Locations, areas, shifts, canvassing USER Volunteer portal only TEMP Limited volunteer access (auto-created)"},{"location":"docs/admin/dashboard/","title":"Dashboard","text":"The admin dashboard (/app) provides a real-time overview of platform activity.
All cards auto-refresh and gracefully degrade when their associated module is disabled.
"},{"location":"docs/admin/people-access/","title":"People & Access","text":"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.
"},{"location":"docs/admin/people-access/#user-management","title":"User Management","text":""},{"location":"docs/admin/people-access/#creating-users","title":"Creating Users","text":"Navigate to Users (/app/users) and click Add User. Fill in name, email, and role. The user will receive a welcome email with login instructions.
SUPER_ADMIN Full platform access Campaign managers INFLUENCE_ADMIN Campaigns, responses, email queue Advocacy coordinators MAP_ADMIN Locations, areas, shifts, canvassing Field organizers USER Volunteer portal only Active volunteers TEMP Limited volunteer access Shift signups (auto-created)"},{"location":"docs/admin/people-access/#password-policy","title":"Password Policy","text":"Passwords must be at least 12 characters with uppercase, lowercase, and a digit. This is enforced at the API schema level.
"},{"location":"docs/admin/people-access/#deactivating-users","title":"Deactivating Users","text":"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.
"},{"location":"docs/admin/people-access/#service-accounts-panel","title":"Service Accounts Panel","text":"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.
"},{"location":"docs/admin/people-access/#people-crm","title":"People CRM","text":"Enable with enablePeople in Settings. The People module serves as the platform's CRM, aggregating data from all other modules into a unified view.
The People page does not store a separate \"people\" table. Instead, it aggregates records in real time from seven data sources:
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 CRMRecords are deduplicated by normalized email or phone number, with Users taking highest priority.
"},{"location":"docs/admin/people-access/#managed-contacts","title":"Managed Contacts","text":"Any virtual person can be \"promoted\" to a managed Contact record. This creates a persistent Contact entity in the database with:
Each managed contact supports multiple structured data entries:
View a chronological timeline of all interactions for a person, across every module:
The platform identifies potential duplicates by matching normalized email addresses and phone numbers across sources. The merge workflow lets you:
Build a relationship graph between contacts using typed connections:
Generate shareable public profile pages for contacts:
/profile/:tokenThe Household panel groups contacts who share the same physical address, making it easy to see all members of a household and their combined engagement.
"},{"location":"docs/admin/people-access/#create-user-from-contact","title":"Create User from Contact","text":"Promote a CRM contact to a full platform user account directly from the People interface, with role assignment and optional welcome email.
"},{"location":"docs/admin/people-access/#admin-routes","title":"Admin Routes","text":"/app/users \u2014 user CRUD, role assignment, service accounts/app/people \u2014 contact list with search, filters, source/tag filtering, and bulk actionsCentralized configuration for organization identity, theming, email delivery, feature modules, and automated notifications.
"},{"location":"docs/admin/settings/#settings-tabs","title":"Settings Tabs","text":""},{"location":"docs/admin/settings/#organization","title":"Organization","text":"Configure your organization's public identity:
Customize the look of admin and public interfaces:
Admin theme:
Public theme:
A live preview panel shows color swatches and a gradient preview as you configure.
"},{"location":"docs/admin/settings/#email","title":"Email","text":"Configure how the platform sends emails:
A configuration summary card at the top displays the current provider, server, authentication status, and test mode state.
"},{"location":"docs/admin/settings/#feature-toggles","title":"Feature Toggles","text":"Enable or disable platform modules. Disabling a module hides it from navigation but does not delete data.
Category Flag Description Core PlatformenableInfluence Advocacy campaigns, email sending, response wall enableMap Map, locations, canvassing, volunteer shifts enableNewsletter Listmonk newsletter sync enableLandingPages GrapesJS landing page builder Media & Content enableMediaFeatures Video library, public gallery, analytics enableGalleryAds Promotional cards in the video gallery enableEvents Gancio event calendar integration Communication enableChat Rocket.Chat team coordination enableMeet Jitsi video meetings (integrates with Rocket.Chat) enableSms Termux Android SMS campaigns People & Engagement enablePeople Unified contacts CRM enableSocial Volunteer social connections and activity feeds autoSyncPeopleToMap Auto-create map locations from contact addresses Commerce enablePayments Stripe subscriptions, products, and donations"},{"location":"docs/admin/settings/#notifications","title":"Notifications","text":"Control which automated email notifications the platform sends. Disabling a notification stops future emails but does not affect already-queued jobs.
Admin alerts:
Volunteer emails:
Re-engagement:
/app/settings \u2014 multi-tab settings page (supports deep-linking to a specific tab via router state)The advocacy module helps supporters contact their elected representatives through email campaigns.
"},{"location":"docs/admin/advocacy/#in-this-section","title":"In This Section","text":"Help supporters contact their elected representatives through email campaigns.
"},{"location":"docs/admin/advocacy/campaigns/#how-it-works","title":"How It Works","text":"Registered (non-temporary) users can create their own advocacy campaigns and submit them for admin review.
/campaigns/create to draft a campaign through a guided wizard/campaigns/mine, including checking moderation status and editing campaigns that have been sent back for changesPENDING_REVIEW status and remain in DRAFT until an admin approves themPENDING_REVIEW or CHANGES_REQUESTED; editing automatically resets the status back to PENDING_REVIEWAdmins review user-submitted campaigns before they go live.
/app/campaign-moderation, showing all user-generated campaigns filtered by moderation status (pending, approved, rejected, changes requested)APPROVED and the campaign status to ACTIVE, making it publicly visibleREJECTED with an optional reason visible to the submitterCHANGES_REQUESTED with feedback, allowing the user to revise and resubmitThe Campaign Effectiveness dashboard provides cross-campaign performance analytics at /app/influence/effectiveness.
/app/campaigns \u2014 create, edit, and manage campaigns/app/campaign-moderation \u2014 review and moderate user-submitted campaigns/app/influence/effectiveness \u2014 campaign effectiveness analytics dashboard/app/responses \u2014 moderate submitted responses/app/email-queue \u2014 monitor outgoing email delivery/campaigns \u2014 browse active campaigns/campaigns/create \u2014 submit a new user-generated campaign (requires login)/campaigns/mine \u2014 view and manage your submitted campaigns (requires login)/campaign/:slug \u2014 take action on a specific campaign/campaign/:slug/responses \u2014 view the response wallMonitor outgoing advocacy emails processed through the BullMQ queue.
"},{"location":"docs/admin/advocacy/email-queue/#key-features","title":"Key Features","text":"/app/email-queue/app/email-queue \u2014 email queue monitoring and managementThe platform uses the Represent API to look up elected representatives by postal code across all government levels.
"},{"location":"docs/admin/advocacy/representatives/#how-it-works","title":"How It Works","text":"/app/representatives/app/representatives \u2014 representative cache management and lookup testingReview and moderate representative responses submitted by supporters on the public response wall.
"},{"location":"docs/admin/advocacy/responses/#key-features","title":"Key Features","text":"/app/responses with filtering by campaign and status/app/responses \u2014 response moderation dashboardReach supporters through multiple channels \u2014 email newsletters, templated campaigns, and SMS text messages.
"},{"location":"docs/admin/broadcast/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/broadcast/email-templates/#template-categories","title":"Template Categories","text":"Each template belongs to a category that determines where it can be used:
Templates use Handlebars-style {{VARIABLE_NAME}} placeholders that are replaced at send time. Variables must use uppercase letters and underscores (e.g., {{RECIPIENT_NAME}}, {{CAMPAIGN_TITLE}}).
{{#if VARIABLE}}...{{/if}} syntaxThe template validator automatically extracts all variables from the HTML, text, and subject line content and checks for unmatched conditional blocks.
"},{"location":"docs/admin/broadcast/email-templates/#version-history","title":"Version History","text":"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:
Before activating a template, send a test email to verify rendering:
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.
"},{"location":"docs/admin/broadcast/email-templates/#admin-routes","title":"Admin Routes","text":"/app/email-templates -- create and manage email templates with a visual editorIntegrated with Listmonk for opt-in mailing lists and newsletter campaigns. Enable with LISTMONK_SYNC_ENABLED=true.
The platform automatically creates and maintains 13 subscriber lists in Listmonk:
List Name Source Tags All Contacts All synced recordsv2 Campaign Participants Users who sent advocacy emails v2, influence Locations - All Address occupants with email v2, map Support Level 1-4 Addresses by canvass support level v2, map, support Has Campaign Sign Addresses with a yard sign v2, map, signs Users Active non-temp platform accounts v2, users Volunteers Shift signups v2, map, shifts Canvassers Users who completed canvass sessions v2, map, canvass Subscribers Active paid subscribers v2, payments Donors Users who completed a donation v2, payments"},{"location":"docs/admin/broadcast/newsletter/#bulk-sync","title":"Bulk Sync","text":"The admin panel provides a manual \"Sync All\" action that synchronizes four data sources to Listmonk:
Each source upserts subscribers (creates new or merges into existing), preserving existing list memberships and merging metadata attributes.
"},{"location":"docs/admin/broadcast/newsletter/#event-driven-sync","title":"Event-Driven Sync","text":"In addition to bulk sync, the platform fires real-time subscriber upserts on application events:
All event-driven syncs are fire-and-forget and silently fail if Listmonk is unreachable.
"},{"location":"docs/admin/broadcast/newsletter/#admin-routes","title":"Admin Routes","text":"/app/listmonk (sidebar: \"Newsletter\") -- sync status, subscriber counts, campaign stats, and manual sync triggerText 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.
Enable with ENABLE_SMS=true or via the setup wizard.
The SMS system uses a three-tier architecture where your server communicates with a lightweight Python Flask API running on an Android phone:
graph LR\n A[Admin Dashboard<br/>Campaign UI] -->|API calls| B[Express API<br/>BullMQ Queue]\n B -->|HTTP + API Key| C[Android Phone<br/>Flask on Termux]\n C -->|termux-sms-send| D[Android SMS]\n D -->|Carrier Network| E[Recipients]\n E -->|Reply SMS| D\n D -->|termux-sms-list| C\n C -->|HTTP response| B Why this approach?
Before starting setup, you'll need:
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)Both Apps MUST Come from F-Droid
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:
Termux:API is not yet available on Google Play
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.
"},{"location":"docs/admin/broadcast/sms/#phone-setup","title":"Phone Setup","text":""},{"location":"docs/admin/broadcast/sms/#step-1-install-apps-from-f-droid","title":"Step 1: Install Apps from F-Droid","text":"On your Android phone:
termux-api package
You need two things called \"termux-api\": the F-Droid app (Termux:API) and the Termux package (pkg install termux-api). The setup script installs the package automatically, but the F-Droid app must be installed manually.
Go to the admin dashboard SMS Setup page (/app/sms/setup) and click Generate API Key. Copy the key \u2014 you'll paste it into the setup command.
Open Termux on the phone and run:
# Clone the SMS server (first time only)\npkg install -y git && git clone https://gitea.bnkops.com/admin/campaign_connector.git ~/sms-server\n\n# Run the setup script \u2014 paste your API key at the end\nbash ~/sms-server/android/setup.sh YOUR_API_KEY_HERE\n The setup script automatically:
~/.bashrcWhen done, note the Phone URL displayed (e.g. http://100.64.0.5:5001).
After initial setup, install termux-services for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:
cd ~/sms-server && bash android/setup-services.sh\n This registers two supervised services:
This is required for the server to run reliably in the background:
To pull the latest server code and re-run setup:
cd ~/sms-server && git pull && bash android/setup.sh YOUR_API_KEY_HERE\n"},{"location":"docs/admin/broadcast/sms/#service-management","title":"Service Management","text":"If you installed termux-services (recommended):
# 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 Without termux-services (legacy watchdog):
# Check if the server is running\ncurl http://127.0.0.1:5001/health\n\n# Restart manually\ncd ~/sms-server/android && bash sms-watchdog.sh\n"},{"location":"docs/admin/broadcast/sms/#accessing-the-phone","title":"Accessing the Phone","text":"There are several ways to run commands on the phone:
"},{"location":"docs/admin/broadcast/sms/#direct-on-phone","title":"Direct (on phone)","text":"Simply open the Termux app and type commands. Best for initial setup.
"},{"location":"docs/admin/broadcast/sms/#ssh-remote-access","title":"SSH (remote access)","text":"Start the SSH server in Termux, then connect from your computer:
# 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"},{"location":"docs/admin/broadcast/sms/#scrcpy-screen-mirror","title":"scrcpy (screen mirror)","text":"Mirror the phone screen to your computer \u2014 great for setup:
# 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"},{"location":"docs/admin/broadcast/sms/#setup-wizard","title":"Setup Wizard","text":"The admin panel provides a guided three-step wizard at /app/sms/setup:
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.
"},{"location":"docs/admin/broadcast/sms/#step-2-connect","title":"Step 2: Connect","text":"Choose how to find your phone's IP address:
Tailscale Auto-Discovery (Recommended)Manual URL Entrytskey-api-...)100.x.x.x IPGetting a Tailscale API Key
Go to Tailscale Admin Console \u2192 Settings \u2192 Keys \u2192 Generate auth key or API access token.
Enter the phone's URL directly:
http://100.x.x.x:5001 (stable IP, works across networks)http://192.168.x.x:5001 (changes if phone reconnects)http://your-public-ip:5001 (requires router config)/health endpointenableSms feature flag is automatically enabledPOST /api/sms/send on the phonetermux-sms-send to send via Android's native SMSA background service (sms-response-sync.service.ts) polls the phone's inbox at a configurable interval:
GET /api/sms/inbox?since=<last_sync_timestamp> on the phonetermux-sms-list to get new messagesA background service (sms-device-monitor.service.ts) checks phone health periodically:
{name} variable placeholders/app/sms/setup Guided setup wizard with Tailscale auto-discovery /app/sms SMS dashboard \u2014 campaign overview and device status /app/sms/contacts Manage contact lists and entries /app/sms/campaigns Create and monitor SMS campaigns /app/sms/conversations View threaded conversations with contacts"},{"location":"docs/admin/broadcast/sms/#configuration","title":"Configuration","text":""},{"location":"docs/admin/broadcast/sms/#environment-variables","title":"Environment Variables","text":"Variable Default Description ENABLE_SMS false Feature flag (also set via setup wizard) TERMUX_API_URL \u2014 Phone URL, e.g. http://100.x.x.x:5001 TERMUX_API_KEY \u2014 Shared API key for authentication SMS_DELAY_BETWEEN_MS 1000 Delay between messages in a campaign (ms) SMS_MAX_RETRIES 3 Retry attempts for failed sends SMS_RESPONSE_SYNC_INTERVAL_MS 10000 How often to check for incoming replies (ms) SMS_DEVICE_MONITOR_INTERVAL_MS 30000 How often to check device health (ms) TAILSCALE_API_KEY \u2014 Tailscale API key for auto-discovery TAILSCALE_TAILNET \u2014 Tailscale tailnet name (optional) Note
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.
"},{"location":"docs/admin/broadcast/sms/#phone-side-configuration","title":"Phone-Side Configuration","text":"On the phone, only one environment variable is needed:
export SMS_API_SECRET='your-64-char-hex-key'\n The Flask server also accepts TERMUX_API_KEY as an alias for backwards compatibility.
The Flask server running on the phone exposes these endpoints on port 5001:
Method Endpoint Auth DescriptionGET /health No Server status, uptime, messages sent GET / No Web dashboard with endpoint documentation POST /api/sms/send Yes Send an SMS message POST /api/sms/send-reply Yes Send a reply with conversation tracking GET /api/sms/inbox No Get incoming messages (with since filter) GET /api/sms/list No List messages with pagination GET /api/sms/history No Get SMS history for a phone number GET /api/device/battery No Battery level, health, temperature GET /api/device/location No GPS coordinates (requires permission) GET /api/device/info No Device info + battery + uptime GET /api/contacts/list No Phone address book (with search) POST /api/campaign/notify No Push notification to device Authentication uses the X-API-Key header with the shared secret.
Symptoms: Test connection fails, \"Connection refused\" or timeout.
Checks:
ifconfig in Termux to find the current IP# Quick test from your server\ncurl http://PHONE_IP:5001/health\n"},{"location":"docs/admin/broadcast/sms/#authentication-required-errors","title":"\"Authentication required\" errors","text":"Symptoms: API calls return 401 with \"Authentication required\".
Fix: The API key on the phone doesn't match the one in the admin panel.
# 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\"' >> ~/.bashrc\n\n# Restart the server\nsv restart sms-api\n# Or without termux-services: pkill -f termux-sms-api-server.py && cd ~/sms-server/android && python termux-sms-api-server.py\n"},{"location":"docs/admin/broadcast/sms/#sms-not-sending","title":"SMS not sending","text":"Symptoms: Server responds successfully but messages don't arrive.
Checks:
Symptoms: Server stops after some time, especially when phone screen is off.
Fix:
termux-services (if not already): bash ~/sms-server/android/setup-services.sh \u2014 this uses runit, a proper service supervisor that auto-restarts the server immediately if it crashestermux-wake-lock in Termux (included in boot script)Symptoms: Server exits immediately with a security error.
Fix: Set the API key environment variable:
# 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\"' >> ~/.bashrc\n"},{"location":"docs/admin/broadcast/sms/#rcs-chat-features-interfering-with-replies","title":"RCS / Chat Features interfering with replies","text":"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.
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 termux-sms-list, so RCS messages are invisible to it.
Fix: Disable RCS on the SMS phone:
This must be done on the phone running the SMS server
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).
Additional checks:
To pull the latest version of the server 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 && cd android && python termux-sms-api-server.py\n"},{"location":"docs/admin/map/","title":"Map & Canvassing","text":"Manage locations, organize canvassing territories, schedule volunteer shifts, and coordinate door-to-door outreach.
"},{"location":"docs/admin/map/#in-this-section","title":"In This Section","text":"Draw polygon regions on the map to define canvassing territories. Areas organize locations into manageable chunks for volunteers.
"},{"location":"docs/admin/map/areas/#key-features","title":"Key Features","text":"/app/map/cuts to draw, edit, and delete area boundariesBulk-import addresses into an area from multiple data sources:
/app/map/cuts \u2014 draw and manage canvassing areas/app/map/cuts/:id/export \u2014 printable location report for a cutCoordinate and monitor volunteer door-to-door outreach with the canvass dashboard, walk sheets, and contact export tools.
"},{"location":"docs/admin/map/canvassing/#canvass-dashboard","title":"Canvass Dashboard","text":"From /app/map/canvass:
/app/map/walk-sheet with space for recording visit outcomes; includes up to 3 configurable QR codes/app/map/cuts/:id/export for a specific canvassing areaBridge canvassing data with advocacy campaigns:
/app/map/canvass \u2014 canvass dashboard/app/map/walk-sheet \u2014 printable walk sheetMonitor geocoding accuracy and coverage from /app/map/data-quality.
/app/map/data-quality \u2014 geocoding quality dashboardImport addresses via CSV or the Canadian National Address Register (NAR), geocode them with multiple providers, and manage the location database.
"},{"location":"docs/admin/map/locations/#adding-locations","title":"Adding Locations","text":"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.
"},{"location":"docs/admin/map/locations/#bulk-operations","title":"Bulk Operations","text":"Select multiple locations for:
/app/map \u2014 location CRUD, CSV import/export, geocoding, area import wizardConfigure the default map view and QR code links from /app/map/settings.
/app/map/settings \u2014 map configurationSchedule volunteer time slots and let people sign up through a public page. Shifts can be linked to specific areas so volunteers know where they'll be canvassing.
"},{"location":"docs/admin/map/shifts/#creating-shifts","title":"Creating Shifts","text":"/app/map/shifts \u2014 shift CRUD, calendar view, signup managementUpload, organize, and share campaign videos and photos with built-in analytics and engagement features. Enable with enableMediaFeatures in Settings.
Create promotional cards that appear in the public media gallery and documentation site. Manage from /app/media/ads.
/app/media/ads \u2014 gallery ad management and analyticsTrack video engagement with GDPR-compliant analytics (IP hashing, 90-day retention).
"},{"location":"docs/admin/media/analytics/#per-video-metrics","title":"Per-Video Metrics","text":"Each video tracks:
The analytics dashboard at /app/media/analytics provides:
Public endpoints record engagement:
navigator.sendBeacon for reliable end-of-session reporting/app/media/analytics \u2014 global analytics dashboardCurate the public gallery experience with playlists, a shorts feed, and featured content.
"},{"location":"docs/admin/media/curated/#playlists","title":"Playlists","text":"From /app/media/curated:
TikTok-style vertical video feed for clips under 60 seconds:
/gallery/shorts/app/media/curated \u2014 playlist managementThe media library at /app/media/library is where you upload, organize, and publish video and photo content.
Generate 24-hour JWT-authenticated preview links for unpublished videos \u2014 useful for stakeholder review before publishing.
"},{"location":"docs/admin/media/library/#admin-routes","title":"Admin Routes","text":"/app/media/library \u2014 video and photo management/app/media/jobs \u2014 processing job queue monitoringAdmin tools for reviewing and managing comments across all media content at /app/media/moderation.
For each comment, admins can:
A configurable list of words with severity levels:
Severity Action High Auto-blocks the comment Medium Auto-hides for review Low Flags for moderator attention Custom User-defined severityThe filter list is cached with a 1-minute TTL and invalidated on changes.
"},{"location":"docs/admin/media/moderation/#admin-routes","title":"Admin Routes","text":"/app/media/moderation \u2014 comment moderation and word filter managementAccept memberships, product sales, and donations through Stripe. Enable with enablePayments in Settings.
.env (ENABLE_PAYMENTS=true)ENCRYPTION_KEY)Create custom branded donation landing pages with independent branding and goals.
"},{"location":"docs/admin/payments/donations/#donation-pages","title":"Donation Pages","text":"From /app/donation-pages:
/donate/:slugFrom /app/donations:
/app/donations \u2014 donation management/app/donation-pages \u2014 donation page CRUDCreate and manage recurring subscription plans for campaign supporters at /app/plans.
/pricing/app/plans \u2014 subscription plan managementManage campaign merchandise and one-time purchase items at /app/products.
/shop for public browsing and purchase/app/products \u2014 product managementConfigure Stripe integration from Settings > Payments.
"},{"location":"docs/admin/payments/settings/#stripe-configuration","title":"Stripe Configuration","text":"ENCRYPTION_KEY environment variable (AES encryption)Stripe webhooks are automatically configured to handle:
/app/settings (Payments tab) \u2014 Stripe key configurationManage the platform's infrastructure services, monitoring stack, and third-party integrations.
"},{"location":"docs/admin/services/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/services/crowdsec/#architecture","title":"Architecture","text":"graph LR\n User -->|HTTPS| Traefik\n Traefik -->|forwardAuth| Tinyauth\n Tinyauth -->|authenticated| Traefik\n Traefik -->|proxy| CrowdSec-Manager\n CrowdSec-Manager -->|API| CrowdSec\n Traefik -->|bouncer plugin| CrowdSec\n CrowdSec -->|captcha decision| Turnstile[Cloudflare Turnstile] All services run on the same Docker Compose stack and share the pangolin network. Traefik reaches them through Gerbil's network namespace.
Image: hhftechnology/crowdsec-manager:1.1.0
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.
Accessible at: https://crowdsec.bnkserve.org
Image: ghcr.io/steveiliop56/tinyauth:v4
A lightweight forward-auth middleware that protects the CrowdSec Manager dashboard with a login screen. Traefik's forwardAuth middleware checks every request to the manager against Tinyauth before allowing access.
Login page at: https://auth.bnkserve.org
User credentials are stored in a users file (/data/users) mounted from the host, using bcrypt-hashed passwords.
Special Characters in Passwords
Tinyauth v4 has a known issue where special characters (@, !, 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.
crowdsec-manager-router crowdsec.bnkserve.org security-headers, tinyauth Dashboard (HTTPS) crowdsec-manager-redirect crowdsec.bnkserve.org redirect-to-https HTTP \u2192 HTTPS redirect tinyauth-router auth.bnkserve.org security-headers Auth login page (HTTPS) tinyauth-redirect auth.bnkserve.org redirect-to-https HTTP \u2192 HTTPS redirect No tinyauth middleware on the tinyauth router
The tinyauth-router must not have the tinyauth forwardAuth middleware applied \u2014 this would create an infinite redirect loop.
The tinyauth forwardAuth middleware forwards every request to http://tinyauth:3000/api/auth/traefik. If the user has a valid session cookie (scoped to .bnkserve.org), the request passes through. Otherwise, the user is redirected to the Tinyauth login page.
tinyauth:\n forwardAuth:\n address: http://tinyauth:3000/api/auth/traefik\n trustForwardHeader: true\n authResponseHeaders:\n - X-Forwarded-User\n"},{"location":"docs/admin/services/crowdsec/#crowdsec-tuning","title":"CrowdSec Tuning","text":""},{"location":"docs/admin/services/crowdsec/#relaxed-crawl-detection","title":"Relaxed Crawl Detection","text":"The crowdsecurity/http-crawl-non_statics scenario was triggering on legitimate Canadian users browsing the site. The local override at /etc/crowdsec/scenarios/http-crawl-non_statics.yaml (replacing the hub symlink) has relaxed thresholds:
capacity 40 80 Twice as many distinct pages before triggering leakspeed 0.5s 0.25s Bucket drains twice as fast 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.
"},{"location":"docs/admin/services/crowdsec/#canadian-isp-whitelist","title":"Canadian ISP Whitelist","text":"A whitelist expression in /etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml exempts traffic from major Canadian ISPs from all CrowdSec scenarios:
expression:\n - evt.Meta.ASNNumber in ['812', '852', '6327', '5645', '20365', '25668', '577']\n AS Number ISP 812 Rogers Communications 852 TELUS Communications 6327 Shaw Communications 5645 TekSavvy 20365 Freedom Mobile 25668 CipherKey 577 Bell Canada Field name
The GeoIP enricher populates evt.Meta.ASNNumber (not ASNumber). This can be verified by inspecting /etc/crowdsec/parsers/s02-enrich/geoip-enrich.yaml.
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.
Configuration added to the CrowdSec bouncer plugin in dynamic_config.yml:
captchaProvider: turnstile\ncaptchaSiteKey: <site-key>\ncaptchaSecretKey: <secret-key>\ncaptchaHTMLFilePath: /etc/traefik/captcha.html\n Captcha HTML template path
The captcha.html template is copied from the plugin source to /etc/traefik/captcha.html (the mounted config volume). Do not reference the /plugins-storage/ path directly \u2014 the hash in that path changes on every Traefik restart.
Two A records pointing to 72.11.155.21:
crowdsec.bnkserve.org CrowdSec Manager dashboard auth.bnkserve.org Tinyauth login page"},{"location":"docs/admin/services/crowdsec/#verification","title":"Verification","text":"# 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>&1 | grep -i captcha\n"},{"location":"docs/admin/services/integrations/","title":"Integrations","text":"Changemaker Lite integrates with several self-hosted services. Each runs as a Docker container and can be enabled independently.
"},{"location":"docs/admin/services/integrations/#team-chat-rocketchat","title":"Team Chat (Rocket.Chat)","text":"Self-hosted team chat for volunteer coordination. Enable with enableChat in Settings.
/app/services/rocketchat, /volunteer/chatSelf-hosted video calls integrated with Rocket.Chat via JWT authentication. Enable with enableMeet in Settings.
now + 1hourToken Expiration
Set the Jitsi app's Token Expiration to now + 1hour. A raw number like 120 is interpreted as Unix timestamp 120 (Jan 1970), causing all tokens to appear expired.
Bitwarden-compatible password vault for secure team credential sharing.
https://vault.DOMAINCollaborative whiteboard for brainstorming and campaign planning.
/app/services/excalidraw/app/services/excalidrawSelf-hosted Git repository hosting for campaign code and configuration.
git.DOMAIN or embedded in admin at /app/services/giteaSelf-hosted workflow automation for connecting platform events to external services.
n8n.DOMAIN or embedded in admin at /app/services/n8nBuilt-in QR code generation for walk sheets, volunteer invites, and campaign links.
/api/qr/app/services/qrThe monitoring stack runs as a Docker Compose profile and provides metrics collection, visualization, and alerting.
"},{"location":"docs/admin/services/monitoring/#starting-the-stack","title":"Starting the Stack","text":"docker compose --profile monitoring up -d\n This starts Prometheus, Grafana, Alertmanager, cAdvisor, Node Exporter, and Redis Exporter.
"},{"location":"docs/admin/services/monitoring/#custom-metrics","title":"Custom Metrics","text":"The platform exposes 12 custom cm_* Prometheus metrics:
Three pre-configured dashboards auto-provisioned from configs/grafana/:
Alert rules in configs/prometheus/alerts.yml cover:
/app/observability \u2014 embedded Grafana dashboards, alert status, and service health (3 tabs)localhost:3001 or grafana.DOMAINlocalhost:9090localhost:9093Pangolin provides secure tunneling to expose your self-hosted services to the internet without port forwarding or a static IP.
"},{"location":"docs/admin/services/tunnel/#setup","title":"Setup","text":"From /app/pangolin:
.env with credentials, and restarts the Newt tunnel containerThe platform defines 12+ service resources in configs/pangolin/resources.yml:
api.DOMAIN, app.DOMAIN) to an internal serviceThe Newt container runs alongside nginx and tunnels traffic to your services:
PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET environment variablesnginx:80)The Pangolin server runs CrowdSec for intrusion detection with a web management UI protected by Tinyauth forward-auth. See CrowdSec & Security for details on:
crowdsec.bnkserve.org)auth.bnkserve.org)/app/pangolin \u2014 tunnel status, setup wizard, and resource managementAutomatically create and sync user accounts across integrated services when new platform users are registered. Enable with enableUserProvisioning in Settings.
From Settings > User Provisioning:
Trigger a bulk sync from /api/users/provisioning/sync to provision all existing users across enabled services. Useful after enabling a new service.
/app/users (edit drawer) \u2014 per-user service account status and actions/app/settings (User Provisioning tab) \u2014 per-service toggle and timingManage the public-facing web presence \u2014 landing pages, the dynamic homepage, navigation menu, and documentation site.
"},{"location":"docs/admin/web/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/web/documentation/","title":"Documentation","text":"Manage the MkDocs documentation site, track page engagement, and moderate visitor comments.
"},{"location":"docs/admin/web/documentation/#mkdocs-management","title":"MkDocs Management","text":"From Docs (/app/docs):
/app/docs/settings)Track how visitors interact with documentation pages using the MkDocs Material theme's custom analytics provider and navigation.tracking.
docs/overrides/Visitors can leave comments on documentation pages using a Gitea-backed comment system.
/app/docs-comments for approving, hiding, or deleting comments/app/docs \u2014 MkDocs management (file tree, config, build triggers)/app/docs/settings \u2014 documentation configuration/app/docs-comments \u2014 moderate documentation commentsA dynamic public landing page that showcases your organization and aggregates content from across the platform.
"},{"location":"docs/admin/web/homepage/#sections","title":"Sections","text":"The homepage assembles its content from enabled modules. Sections are only displayed when their corresponding module is active and has data to show.
All homepage data is fetched from a single API endpoint (/api/homepage) and cached in Redis for 2 minutes. Individual section queries use Promise.allSettled so that a failure in one module (e.g., Gancio being offline) does not prevent the rest of the page from loading.
homepageTagline field in site settings/home \u2014 public homepageBuild campaign microsites with a drag-and-drop visual editor.
"},{"location":"docs/admin/web/landing-pages/#how-it-works","title":"How It Works","text":"/p/:slug/app/pages \u2014 list and manage landing pages/app/pages/:id/edit \u2014 full-screen GrapesJS editor/p/:slug \u2014 view a published landing pageCustomize 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.
"},{"location":"docs/admin/web/navigation/#key-features","title":"Key Features","text":"enableInfluence) are automatically hidden when that feature is disabledThe platform ships with 11 builtin navigation items that cover the main public routes:
Home, Campaigns, Map, Shifts, Events, Gallery, Pricing, Shop, Donate, Website (landing page), and Docs (documentation site).
Each builtin item has a default icon and path. Some paths use special $ tokens (e.g., $landing, $docs) that are automatically resolved to the correct external URL based on the deployment environment.
Add any number of custom links via the \"Add Custom Link\" button. Custom links support:
/blog)https://example.com) -- automatically detected and opened in a new tabCustom links can be deleted from the navigation; builtin items can only be toggled off.
"},{"location":"docs/admin/web/navigation/#mobile-handling","title":"Mobile Handling","text":"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.
"},{"location":"docs/admin/web/navigation/#admin-routes","title":"Admin Routes","text":"/app/navigation -- navigation editor with per-item toggle, reorder, label editing, and custom link managementChangemaker Lite exposes two REST APIs sharing a single PostgreSQL database.
Server Framework Port Purpose Main API Express.js4000 Auth, campaigns, map, shifts, canvassing, pages, email, settings Media API Fastify 4100 Video library, analytics, playlists, reactions, comments Both APIs use JWT Bearer authentication and return JSON. All request/response bodies are application/json unless noted otherwise.
sequenceDiagram\n participant Client\n participant API\n participant DB\n\n Client->>API: POST /api/auth/login {email, password}\n API->>DB: Verify credentials\n DB-->>API: User record\n API-->>Client: {accessToken, refreshToken}\n Note over Client: Store tokens\n\n Client->>API: GET /api/campaigns (Authorization: Bearer <accessToken>)\n API-->>Client: 200 OK\n\n Note over Client: Access token expires (15 min)\n\n Client->>API: POST /api/auth/refresh {refreshToken}\n API->>DB: Rotate token (atomic transaction)\n DB-->>API: New token pair\n API-->>Client: {accessToken, refreshToken}"},{"location":"docs/api/#headers","title":"Headers","text":"All authenticated requests require:
Authorization: Bearer <accessToken>\n The Media API also accepts tokens via query parameter for SSE streams:
GET /api/public/:id/chat-stream?token=<accessToken>\n"},{"location":"docs/api/#roles","title":"Roles","text":"Role Access Level SUPER_ADMIN Full platform access INFLUENCE_ADMIN Campaign and advocacy management MAP_ADMIN Map, locations, shifts, canvassing USER Volunteer portal, public features TEMP Limited access (auto-created on public shift signup)"},{"location":"docs/api/#middleware-reference","title":"Middleware Reference","text":"Middleware Effect authenticate Requires valid JWT. Sets req.user with id, email, role. Returns 401 if missing or invalid. optionalAuth Same as authenticate but continues without user if token is absent. requireRole(...roles) Checks user role against allowed list. Returns 403 if not authorized. requireNonTemp Blocks TEMP users. Returns 403. validate(schema, source) Validates request body/query/params against a Zod schema. Returns 400 on failure."},{"location":"docs/api/#error-responses","title":"Error Responses","text":"All errors follow a consistent format:
{\n \"error\": {\n \"message\": \"Human-readable error description\",\n \"code\": \"ERROR_CODE\",\n \"statusCode\": 400\n }\n}\n Status Code Meaning 400 VALIDATION_ERROR Request body/query failed schema validation 401 UNAUTHORIZED Missing or invalid access token 403 FORBIDDEN Valid token but insufficient role 404 NOT_FOUND Resource does not exist 429 RATE_LIMITED Too many requests (see Rate Limits) 500 INTERNAL_ERROR Unexpected server error Enumeration Prevention
Auth endpoints (/login, /register, /forgot-password) return generic success messages to prevent user enumeration. A 401 from /api/auth/me does not reveal whether the user exists.
Rate limits are Redis-backed and keyed by IP address.
Endpoint Group Window Max Requests Redis Prefix Auth (login, register, refresh) 15 min 10rl:auth: Email sending 1 hour 30 rl:email: Response submission 1 hour 10 rl:response: Shift signup 1 hour 10 rl:shift-signup: Canvass visits 1 min 30 rl:canvass-visit: Canvass bulk visits 1 min 5 rl:canvass-visit-bulk: GPS tracking 1 min 6 rl:gps-tracking: Canvass geocode 1 min 10 rl:canvass-geocode: Observability 1 min 20 rl:observability: Health/metrics 1 min 30 rl:health-metrics: Global (all other) Configurable Configurable rl:global: When rate-limited, the API returns:
{\n \"error\": {\n \"message\": \"Too many requests, please try again later\",\n \"code\": \"RATE_LIMITED\",\n \"statusCode\": 429\n }\n}\n"},{"location":"docs/api/#main-api-express-port-4000","title":"Main API (Express \u2014 Port 4000)","text":""},{"location":"docs/api/#health-metrics","title":"Health & Metrics","text":"Method Path Auth Description GET /api/health Health check \u2014 PostgreSQL + Redis ping GET /api/metrics Prometheus metrics (text/plain) Health response {\n \"status\": \"healthy\",\n \"checks\": {\n \"database\": \"ok\",\n \"redis\": \"ok\"\n }\n}\n"},{"location":"docs/api/#auth","title":"Auth","text":"Prefix: /api/auth
/api/auth/login Email + password login POST /api/auth/register Create account (always USER role) POST /api/auth/verify-email Verify email with token POST /api/auth/resend-verification Resend verification email POST /api/auth/forgot-password Send password reset email POST /api/auth/reset-password Set new password with reset token POST /api/auth/refresh Rotate refresh token \u2192 new token pair POST /api/auth/logout Invalidate refresh token GET /api/auth/me Current user profile Login request & response Request:
{\n \"email\": \"admin@example.com\",\n \"password\": \"SecurePass123!\"\n}\n Response: {\n \"accessToken\": \"eyJhbG...\",\n \"refreshToken\": \"eyJhbG...\",\n \"user\": {\n \"id\": \"uuid\",\n \"email\": \"admin@example.com\",\n \"name\": \"Admin\",\n \"role\": \"SUPER_ADMIN\"\n }\n}\n Password Policy
Passwords must be at least 12 characters with at least one uppercase letter, one lowercase letter, and one digit.
"},{"location":"docs/api/#users","title":"Users","text":"Prefix: /api/users \u00b7 Auth: All routes require authentication
/api/users Admin Paginated user list with search, role, and status filters GET /api/users/:id Admin or self Single user profile POST /api/users Admin Create user PUT /api/users/:id Admin or self Update user (non-admins cannot change role/status) POST /api/users/:id/approve Admin Approve pending user; sends approval email POST /api/users/:id/reject Admin Reject pending user DELETE /api/users/:id Admin Delete user Query parameters for GET /api/users:
page number Page number (default 1) limit number Items per page (default 20) search string Search by name or email role string Filter by role status string Filter by status"},{"location":"docs/api/#dashboard","title":"Dashboard","text":"Prefix: /api/dashboard \u00b7 Auth: Admin roles required
/api/dashboard/summary Any admin Platform-wide counts (users, campaigns, locations, shifts) GET /api/dashboard/system SUPER_ADMIN Hardware + OS info (CPU, memory, disk) GET /api/dashboard/containers SUPER_ADMIN Docker container statuses GET /api/dashboard/weather Any admin Current weather at map center coordinates GET /api/dashboard/api-metrics SUPER_ADMIN Prometheus API performance metrics GET /api/dashboard/time-series SUPER_ADMIN Prometheus time-series data GET /api/dashboard/container-resources SUPER_ADMIN cAdvisor CPU/memory/network per container Query parameters for GET /api/dashboard/time-series:
metrics string Comma-separated metric keys (whitelist-validated) range string Time range (e.g., 1h, 24h, 7d) step string Sample interval (e.g., 5m, 1h)"},{"location":"docs/api/#campaigns","title":"Campaigns","text":""},{"location":"docs/api/#admin-crud","title":"Admin CRUD","text":"Prefix: /api/campaigns \u00b7 Auth: Admin roles
/api/campaigns Paginated campaign list GET /api/campaigns/:id Single campaign detail POST /api/campaigns Create campaign PUT /api/campaigns/:id Update campaign DELETE /api/campaigns/:id Delete campaign"},{"location":"docs/api/#public","title":"Public","text":"Method Path Auth Description GET /api/campaigns/public List all active campaigns GET /api/campaigns/:slug/details Campaign detail by slug (ACTIVE only)"},{"location":"docs/api/#user-submissions","title":"User Submissions","text":"Auth: Authenticated, non-TEMP users
Method Path Description POST/api/campaigns/user/submit Submit campaign for moderation (5/hour limit) GET /api/campaigns/user/my-campaigns List own submitted campaigns PUT /api/campaigns/user/:id Edit own pending campaign"},{"location":"docs/api/#moderation","title":"Moderation","text":"Auth: Admin roles
Method Path Description GET/api/campaigns/moderation/queue Campaigns pending moderation GET /api/campaigns/moderation/stats Moderation queue statistics PATCH /api/campaigns/moderation/:id Approve or reject campaign"},{"location":"docs/api/#campaign-emails","title":"Campaign Emails","text":"Method Path Auth Description POST /api/campaigns/:slug/send-email Send advocacy email to representatives (rate limited: 30/hour) POST /api/campaigns/:slug/track-mailto Track mailto link click GET /api/campaigns/:id/emails Admin Paginated emails for campaign GET /api/campaigns/:id/email-stats Admin Email statistics"},{"location":"docs/api/#responses","title":"Responses","text":"Prefix: /api/campaigns (public) and /api/responses (admin + actions)
/api/campaigns/:slug/responses List approved public responses GET /api/campaigns/:slug/response-stats Response statistics POST /api/campaigns/:slug/responses Submit response (rate limited: 10/hour) POST /api/responses/:id/upvote Optional Upvote a response DELETE /api/responses/:id/upvote Optional Remove upvote GET /api/responses/:id/verify/:token Verify response via email link"},{"location":"docs/api/#admin","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/responses All responses with filters PATCH /api/responses/:id/status Approve or reject response POST /api/responses/:id/resend-verification Resend verification email DELETE /api/responses/:id Delete response"},{"location":"docs/api/#representatives","title":"Representatives","text":"Prefix: /api/representatives
/api/representatives/by-postal/:postalCode Lookup representatives by postal code (cache-first) GET /api/representatives/test-connection Represent API health check GET /api/representatives/cache-stats Admin Cache statistics GET /api/representatives Admin Paginated cached representatives GET /api/representatives/:id Admin Single cached representative DELETE /api/representatives/by-postal/:postalCode Admin Clear cache for postal code DELETE /api/representatives/:id Admin Delete cached representative Query parameters for postal code lookup:
Param Type Descriptionrefresh boolean Force API call, bypass cache"},{"location":"docs/api/#email-queue","title":"Email Queue","text":"Prefix: /api/email-queue \u00b7 Auth: Admin roles
/api/email-queue/stats BullMQ queue statistics (waiting, active, completed, failed) POST /api/email-queue/pause Pause email processing POST /api/email-queue/resume Resume email processing POST /api/email-queue/clean Clean completed jobs"},{"location":"docs/api/#locations","title":"Locations","text":"Prefix: /api/map/locations
/api/map/locations/public All geocoded locations for map (no PII); optional ?bounds="},{"location":"docs/api/#admin_1","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/locations Paginated locations with filters GET /api/map/locations/stats Location statistics GET /api/map/locations/all All geocoded locations for admin map GET /api/map/locations/export-csv CSV export GET /api/map/locations/:id Single location GET /api/map/locations/:id/history Edit history POST /api/map/locations Create location PUT /api/map/locations/:id Update location DELETE /api/map/locations/:id Delete location POST /api/map/locations/bulk-delete Bulk delete POST /api/map/locations/geocode Geocode single address POST /api/map/locations/geocode-missing Batch geocode all ungeocoded POST /api/map/locations/reverse-geocode Reverse geocode lat/lng to address POST /api/map/locations/import-csv Import from CSV (10 MB limit) POST /api/map/locations/import-bulk Bulk NAR or standard CSV import (100 MB limit)"},{"location":"docs/api/#bulk-geocode","title":"Bulk Geocode","text":"Prefix: /api/map/locations/bulk-geocode \u00b7 Auth: Map admins
/api/map/locations/bulk-geocode Start BullMQ bulk geocoding job GET /api/map/locations/bulk-geocode/:jobId Poll job status GET /api/map/locations/bulk-geocode/stats Queue statistics"},{"location":"docs/api/#nar-import","title":"NAR Import","text":"Prefix: /api/map/nar-import \u00b7 Auth: Map admins
/api/map/nar-import/datasets Available NAR datasets by province POST /api/map/nar-import Start province import (fire-and-forget) GET /api/map/nar-import/status/:importId Poll import progress NAR Import body {\n \"provinceCode\": \"24\",\n \"filterType\": \"city\",\n \"filterCity\": \"Edmonton\",\n \"residentialOnly\": true,\n \"deduplicateRadius\": 10,\n \"batchSize\": 500\n}\n"},{"location":"docs/api/#area-import","title":"Area Import","text":"Prefix: /api/map/area-import \u00b7 Auth: Map admins
/api/map/area-import/preview Preview bounds + estimated record counts POST /api/map/area-import Start area import (fire-and-forget) GET /api/map/area-import/status/:importId Poll import progress"},{"location":"docs/api/#cuts-polygons","title":"Cuts (Polygons)","text":"Prefix: /api/map/cuts
/api/map/cuts/public All public cuts as GeoJSON GET /api/map/cuts Map admin Paginated cuts list GET /api/map/cuts/:id Map admin Single cut POST /api/map/cuts Map admin Create cut (polygon GeoJSON) PUT /api/map/cuts/:id Map admin Update cut DELETE /api/map/cuts/:id Map admin Delete cut GET /api/map/cuts/:id/locations Map admin All locations within cut polygon GET /api/map/cuts/:id/statistics Map admin Support level breakdown GET /api/map/cuts/export-geojson Map admin All cuts as GeoJSON FeatureCollection GET /api/map/cuts/:id/export-geojson Map admin Single cut as GeoJSON Feature POST /api/map/cuts/import-geojson Map admin Import cuts from GeoJSON file"},{"location":"docs/api/#shifts","title":"Shifts","text":"Prefix: /api/map/shifts
/api/map/shifts/public List upcoming public shifts POST /api/map/shifts/public/:id/signup Public signup (creates TEMP user if needed; rate limited: 10/hour)"},{"location":"docs/api/#volunteer","title":"Volunteer","text":"Auth: Any authenticated user
Method Path Description GET/api/map/shifts/volunteer/upcoming Upcoming shifts with signup status GET /api/map/shifts/volunteer/my-signups Own confirmed signups POST /api/map/shifts/volunteer/:id/signup Sign up for shift DELETE /api/map/shifts/volunteer/:id/signup Cancel signup"},{"location":"docs/api/#admin_2","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/shifts Paginated shifts with filters GET /api/map/shifts/stats Statistics GET /api/map/shifts/calendar Calendar data (?startDate=&endDate=) GET /api/map/shifts/:id Single shift with signups POST /api/map/shifts Create shift PUT /api/map/shifts/:id Update shift DELETE /api/map/shifts/:id Delete shift POST /api/map/shifts/:id/signups Admin-add volunteer DELETE /api/map/shifts/:id/signups/:signupId Remove volunteer POST /api/map/shifts/:id/email-details Email details to all volunteers"},{"location":"docs/api/#shift-series","title":"Shift Series","text":"Auth: Map admins
Method Path Description POST/api/map/shifts/series Create recurring shift series GET /api/map/shifts/series/:id Get series PUT /api/map/shifts/series/:id Update series DELETE /api/map/shifts/series/:id Delete series"},{"location":"docs/api/#canvassing","title":"Canvassing","text":"Prefix: /api/map/canvass
Auth: Any authenticated user
Method Path Description GET/api/map/canvass/my/assignments Shift assignments GET /api/map/canvass/my/stats Personal canvass statistics GET /api/map/canvass/my/visits Visit history GET /api/map/canvass/my/session Active canvass session POST /api/map/canvass/sessions Start canvass session POST /api/map/canvass/sessions/:id/end End session GET /api/map/canvass/cuts/:cutId/locations Locations in cut with visit annotations GET /api/map/canvass/cuts/:cutId/route Walking route algorithm for cut GET /api/map/canvass/locations All locations with visit annotations PUT /api/map/canvass/locations/:id Edit address (role-gated fields) POST /api/map/canvass/locations Create location POST /api/map/canvass/reverse-geocode Reverse geocode lat/lng POST /api/map/canvass/geocode-search Geocode address for map (rate limited: 10/min) POST /api/map/canvass/visits Record door knock (rate limited: 30/min) POST /api/map/canvass/visits/bulk Record visit for all unvisited units (rate limited: 5/min)"},{"location":"docs/api/#admin_3","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/canvass/stats Platform-wide canvass statistics GET /api/map/canvass/stats/cuts/:cutId Statistics for specific cut GET /api/map/canvass/activity Recent activity feed GET /api/map/canvass/volunteers All volunteers with canvass activity GET /api/map/canvass/volunteers/:userId Individual volunteer statistics GET /api/map/canvass/visits All visits with filters"},{"location":"docs/api/#gps-tracking","title":"GPS Tracking","text":"Prefix: /api/map/tracking
Auth: Any authenticated user
Method Path Description POST/api/map/tracking/sessions Start GPS tracking session POST /api/map/tracking/sessions/:id/end End tracking session POST /api/map/tracking/sessions/:id/points Submit GPS point batch (rate limited: 6/min) POST /api/map/tracking/sessions/:id/link-canvass Link to canvass session GET /api/map/tracking/my/session Active tracking session GET /api/map/tracking/my/sessions Own historical sessions GET /api/map/tracking/my/sessions/:id/route Full route for own session"},{"location":"docs/api/#admin_4","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/tracking/live Live volunteer positions + trails GET /api/map/tracking/sessions All historical tracking sessions GET /api/map/tracking/sessions/:id/route Full route for any session"},{"location":"docs/api/#map-settings","title":"Map Settings","text":"Prefix: /api/map/settings
/api/map/settings Public map settings (center, zoom, walk sheet config) PUT /api/map/settings Map admin Update map settings"},{"location":"docs/api/#geocoding","title":"Geocoding","text":"Prefix: /api/map/geocoding \u00b7 Auth: Map admins
/api/map/geocoding/search Geocode address search (?q=&limit=1-10)"},{"location":"docs/api/#landing-pages","title":"Landing Pages","text":"Prefix: /api/pages and /api/page-blocks
/api/pages/:slug/view Get published page by slug"},{"location":"docs/api/#admin_5","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/pages Paginated landing pages GET /api/pages/:id Single page POST /api/pages Create page PUT /api/pages/:id Update page DELETE /api/pages/:id Delete page POST /api/pages/sync Sync MkDocs overrides from filesystem POST /api/pages/validate Validate and repair MkDocs exports"},{"location":"docs/api/#block-library","title":"Block Library","text":"Auth: Admin roles
Method Path Description GET/api/page-blocks List blocks GET /api/page-blocks/:id Single block POST /api/page-blocks Create block PUT /api/page-blocks/:id Update block DELETE /api/page-blocks/:id Delete block"},{"location":"docs/api/#email-templates","title":"Email Templates","text":"Prefix: /api/email-templates \u00b7 Auth: Admin roles (seed/cache require SUPER_ADMIN)
/api/email-templates List templates GET /api/email-templates/:id Single template POST /api/email-templates Create template PUT /api/email-templates/:id Update template DELETE /api/email-templates/:id Delete template GET /api/email-templates/:id/versions Version history GET /api/email-templates/:id/versions/:versionNumber Specific version POST /api/email-templates/:id/rollback Rollback to prior version POST /api/email-templates/validate Validate Handlebars syntax POST /api/email-templates/:id/test Send test email (rate limited: 10/15min) GET /api/email-templates/:id/test-logs Test send logs POST /api/email-templates/seed Seed templates from filesystem POST /api/email-templates/clear-cache Clear template cache"},{"location":"docs/api/#qr-codes","title":"QR Codes","text":"Method Path Auth Description GET /api/qr Generate QR code PNG (?text=&size=50-500) Cached for 1 hour. Returns image/png.
Prefix: /api/settings
/api/settings Public site settings (SMTP credentials stripped) GET /api/settings/admin SUPER_ADMIN Full settings including SMTP credentials PUT /api/settings SUPER_ADMIN Update settings POST /api/settings/email/test-connection SUPER_ADMIN Test SMTP connection POST /api/settings/email/test-send SUPER_ADMIN Send test email"},{"location":"docs/api/#listmonk-newsletter-sync","title":"Listmonk (Newsletter Sync)","text":"Prefix: /api/listmonk \u00b7 Auth: SUPER_ADMIN
/api/listmonk Sync status + connection check GET /api/listmonk/stats Subscriber counts from Listmonk POST /api/listmonk/test-connection Health check POST /api/listmonk/sync/participants Sync campaign participants POST /api/listmonk/sync/locations Sync locations POST /api/listmonk/sync/users Sync users POST /api/listmonk/sync/all Run all sync operations POST /api/listmonk/reinitialize Reinitialize Listmonk lists GET /api/listmonk/proxy-url Proxy port + JWT for iframe"},{"location":"docs/api/#documentation-management","title":"Documentation Management","text":"Prefix: /api/docs \u00b7 Auth: Authenticated, non-TEMP (write operations require SUPER_ADMIN)
/api/docs/status MkDocs + Code Server availability GET /api/docs/config Port numbers for iframe URLs GET /api/docs/mkdocs-config Read raw mkdocs.yml PUT /api/docs/mkdocs-config Write mkdocs.yml POST /api/docs/build Trigger MkDocs build POST /api/docs/upload Upload asset (20 MB, whitelisted extensions) GET /api/docs/files File tree (?force=true bypasses cache) POST /api/docs/files/rename Rename or move file GET /api/docs/files/* Read file content PUT /api/docs/files/* Write file content POST /api/docs/files/* Create file or folder DELETE /api/docs/files/* Delete file or empty folder"},{"location":"docs/api/#services","title":"Services","text":"Prefix: /api/services \u00b7 Auth: SUPER_ADMIN
/api/services/status Health check all managed services (NocoDB, n8n, Gitea, MailHog, Mini QR, Excalidraw, Homepage) GET /api/services/config Port numbers + subdomain info"},{"location":"docs/api/#pangolin-tunnel-management","title":"Pangolin (Tunnel Management)","text":"Prefix: /api/pangolin \u00b7 Auth: SUPER_ADMIN
/api/pangolin/status Tunnel health + connection info GET /api/pangolin/config Current env configuration GET /api/pangolin/newt-status Newt container status POST /api/pangolin/newt-restart Restart Newt container GET /api/pangolin/sites List Pangolin sites GET /api/pangolin/exit-nodes Available exit nodes GET /api/pangolin/resource-definitions Resource definitions from YAML GET /api/pangolin/resources List resources POST /api/pangolin/setup Create site + all resources (rate limited: \u2157min) POST /api/pangolin/sync Sync resources (create missing, update changed) PUT /api/pangolin/resource/:id Update resource DELETE /api/pangolin/resource/:id Delete resource GET /api/pangolin/resource/:id/clients Connected clients GET /api/pangolin/certificate/:domainId/:domain Certificate info POST /api/pangolin/certificate/:certId Update certificate"},{"location":"docs/api/#observability","title":"Observability","text":"Prefix: /api/observability \u00b7 Auth: SUPER_ADMIN \u00b7 Rate limited: 20/min
/api/observability/status Check 7 monitoring services GET /api/observability/metrics-summary Key metrics from Prometheus GET /api/observability/alerts Active alerts from Alertmanager"},{"location":"docs/api/#payments","title":"Payments","text":"Prefix: /api/payments
/api/payments/config Stripe publishable key + donation settings GET /api/payments/plans Active subscription plans GET /api/payments/products Active products (?type=) POST /api/payments/subscribe Create subscription checkout POST /api/payments/purchase Optional Product checkout (guest or logged-in) POST /api/payments/donate Donation checkout GET /api/payments/my-subscription Current subscription POST /api/payments/my-subscription/cancel Cancel subscription POST /api/payments/webhook Stripe webhook (raw body)"},{"location":"docs/api/#admin_6","title":"Admin","text":"Auth: SUPER_ADMIN
/api/payments/admin/settings Payment settings (secrets masked) PUT /api/payments/admin/settings Update payment settings POST /api/payments/admin/settings/test-connection Test Stripe connection GET /api/payments/admin/dashboard Subscription + donation statistics GET /api/payments/admin/plans All subscription plans POST /api/payments/admin/plans Create plan PUT /api/payments/admin/plans/:id Update plan DELETE /api/payments/admin/plans/:id Delete plan POST /api/payments/admin/plans/:id/sync-stripe Sync plan to Stripe GET /api/payments/admin/subscriptions All subscriptions with filters POST /api/payments/admin/subscriptions/:id/cancel Cancel subscription GET /api/payments/admin/products All products POST /api/payments/admin/products Create product PUT /api/payments/admin/products/:id Update product DELETE /api/payments/admin/products/:id Delete product POST /api/payments/admin/products/:id/sync-stripe Sync product to Stripe GET /api/payments/admin/orders List orders POST /api/payments/admin/orders/:id/refund Refund order GET /api/payments/admin/donations List donations GET /api/payments/admin/export CSV export of completed orders"},{"location":"docs/api/#media-api-fastify-port-4100","title":"Media API (Fastify \u2014 Port 4100)","text":"The Media API is a separate Fastify server sharing the same PostgreSQL database. It handles all video-related functionality.
"},{"location":"docs/api/#health","title":"Health","text":"Method Path Auth Description GET/health Media API health check"},{"location":"docs/api/#videos-admin","title":"Videos (Admin)","text":"Prefix: /api/videos \u00b7 Auth: Admin roles
/api/videos List videos (?limit=&offset=&search=&orientation=&producers=&isShort=) GET /api/videos/producers Distinct producer list GET /api/videos/health Video count health check GET /api/videos/:id Single video detail PATCH /api/videos/:id Update metadata (title, producer, tags, quality, etc.) POST /api/videos/:id/publish Publish to category POST /api/videos/:id/unpublish Unpublish POST /api/videos/bulk-publish Bulk publish POST /api/videos/bulk-unpublish Bulk unpublish POST /api/videos/:id/lock Lock published video POST /api/videos/:id/unlock Unlock video POST /api/videos/:id/generate-thumbnail Generate thumbnail via FFmpeg POST /api/videos/bulk-generate-thumbnails Bulk thumbnail generation"},{"location":"docs/api/#upload","title":"Upload","text":"Method Path Description POST /api/videos/upload Single video upload (multipart, 10 GB limit, streams to disk) POST /api/videos/upload/batch Batch upload (returns 207 multi-status)"},{"location":"docs/api/#actions","title":"Actions","text":"Method Path Description POST /api/videos/:id/duplicate Duplicate video record POST /api/videos/:id/replace Replace video file, keep metadata GET /api/videos/:id/analytics Detailed analytics (?startDate=&endDate=) POST /api/videos/:id/reset-analytics Reset all analytics GET /api/videos/:id/preview-link Generate 24-hour JWT preview link GET /api/videos/analytics/top Top videos (?metric=views|watchTime&limit=) GET /api/videos/analytics/overview Global analytics overview"},{"location":"docs/api/#scheduling","title":"Scheduling","text":"Method Path Description POST /api/videos/:id/schedule-publish Schedule future publish ({publishAt, timezone?}) POST /api/videos/:id/schedule-unpublish Schedule future unpublish DELETE /api/videos/:id/schedule/:action Cancel scheduled operation GET /api/videos/schedules/upcoming Upcoming scheduled operations GET /api/videos/:id/schedule-history Schedule history for video GET /api/videos/schedules/stats Schedule queue statistics POST /api/videos/schedules/pause Pause schedule queue POST /api/videos/schedules/resume Resume schedule queue POST /api/videos/schedules/cleanup Clean old completed jobs"},{"location":"docs/api/#video-fetch","title":"Video Fetch","text":"Method Path Description POST /api/videos/fetch Submit fetch job ({urls: string[]}, 1\u201320 URLs) GET /api/videos/fetch/jobs List recent fetch jobs GET /api/videos/fetch/jobs/:jobId Job detail + log GET /api/videos/fetch/jobs/:jobId/log SSE log stream (Redis pub/sub) DELETE /api/videos/fetch/jobs/:jobId Cancel fetch job"},{"location":"docs/api/#streaming-public","title":"Streaming (Public)","text":"Prefix: /api/videos
/api/videos/stream/health Streaming health check GET /api/videos/:id/stream Optional HTTP range-supporting video stream GET /api/videos/:id/thumbnail Optional Serve thumbnail image GET /api/videos/:id/metadata Public video metadata for embedding Note
Admins can stream unpublished videos by providing a valid JWT.
"},{"location":"docs/api/#public-gallery","title":"Public Gallery","text":"Prefix: /api/public
/api/public Optional Published videos (?limit=&offset=&search=&sort=recent|popular|oldest&category=) GET /api/public/categories Optional Categories with video counts GET /api/public/producers Optional Published producers GET /api/public/:id Optional Single published video GET /api/public/:id/thumbnail Optional Published thumbnail GET /api/public/:id/stream Optional Published video stream"},{"location":"docs/api/#tracking","title":"Tracking","text":"Prefix: /api/track \u00b7 Auth: None required
/api/track/health Tracking health check POST /api/track/view Record video view (returns {viewId}) POST /api/track/event Record play/pause/seek/complete event POST /api/track/heartbeat Update watch time (10s interval, sendBeacon) POST /api/track/batch Batch up to 50 tracking events Tracking is GDPR-compliant IP addresses are hashed with a daily-rotating salt. Raw IPs are never stored. Tracking data is retained for 90 days.
"},{"location":"docs/api/#reactions","title":"Reactions","text":"Prefix: /api/reactions
/api/reactions/config Available reaction types + emoji mappings GET /api/reactions List reactions (?mediaId=&userId=&limit=) GET /api/reactions/:mediaId/chat Reactions in chat timeline format POST /api/reactions Add reaction (30s cooldown per type) Available types: like, love, laugh, wow, sad, angry
/api/public/:id/comments List comments (?limit=&offset=) POST /api/public/:id/comments Optional Create comment (word-filtered; rate limited: 5/min) GET /api/public/:id/chat-stream SSE stream for real-time chat (30s keepalive)"},{"location":"docs/api/#comment-admin","title":"Comment Admin","text":"Prefix: /api/media/admin/comments \u00b7 Auth: Admin roles
/api/media/admin/comments/stats Counts by status GET /api/media/admin/comments All comments with filters PATCH /api/media/admin/comments/:id/approve Approve comment PATCH /api/media/admin/comments/:id/hide Hide comment PATCH /api/media/admin/comments/:id/unhide Unhide comment PUT /api/media/admin/comments/:id/notes Update moderation notes DELETE /api/media/admin/comments/:id Delete comment"},{"location":"docs/api/#word-filters","title":"Word Filters","text":"Prefix: /api/media/admin/word-filters \u00b7 Auth: Admin roles
/api/media/admin/word-filters List filter entries grouped by level POST /api/media/admin/word-filters Add word ({word, level: low|medium|high|custom}) DELETE /api/media/admin/word-filters/:id Remove word"},{"location":"docs/api/#chat-threads-notifications","title":"Chat Threads & Notifications","text":"Auth: Authenticated
Method Path Description GET/api/media/chat/threads Videos with user's comments + unread counts POST /api/media/chat/threads/:mediaId/read Mark thread as read GET /api/media/notifications/stream Per-user SSE notification stream (?token=)"},{"location":"docs/api/#shorts","title":"Shorts","text":"Method Path Auth Description GET /api/shorts Optional Shorts feed (?sort=recent|popular|random) POST /api/shorts/scan Admin Auto-classify short videos by duration"},{"location":"docs/api/#upvotes","title":"Upvotes","text":"Method Path Auth Description POST /api/public/:id/upvote Toggle upvote (session-based via X-Session-ID header) GET /api/public/:id/upvote-status Check upvote status for current session"},{"location":"docs/api/#playlists","title":"Playlists","text":""},{"location":"docs/api/#public_6","title":"Public","text":"Prefix: /api/playlists
/api/playlists/featured Optional Featured playlists GET /api/playlists/popular Optional Popular public playlists (?search=) GET /api/playlists/share/:token Optional Playlist by share token GET /api/playlists/:id Optional Playlist detail (public, owner, or share token) POST /api/playlists/:id/view Optional Record playlist view"},{"location":"docs/api/#user-playlists","title":"User Playlists","text":"Auth: Authenticated
Method Path Description GET/api/playlists/my Own playlists POST /api/playlists Create playlist PUT /api/playlists/:id Update playlist (ownership check) DELETE /api/playlists/:id Delete playlist POST /api/playlists/:id/videos Add video ({mediaId}) DELETE /api/playlists/:id/videos/:mediaId Remove video PUT /api/playlists/:id/videos/reorder Reorder videos POST /api/playlists/:id/share Generate share token DELETE /api/playlists/:id/share Revoke share token"},{"location":"docs/api/#playlist-admin","title":"Playlist Admin","text":"Prefix: /api/media/playlists \u00b7 Auth: Admin roles
/api/media/playlists All playlists GET /api/media/playlists/featured Featured playlists with admin info POST /api/media/playlists/:id/feature Feature a playlist DELETE /api/media/playlists/:id/feature Unfeature a playlist PUT /api/media/playlists/featured/reorder Reorder featured playlists PUT /api/media/playlists/:id Admin update any playlist POST /api/media/playlists/:id/duplicate Duplicate playlist DELETE /api/media/playlists/:id Admin delete any playlist"},{"location":"docs/api/#user-profile","title":"User Profile","text":"Prefix: /api/media/me \u00b7 Auth: Authenticated
/api/media/me/stats User stats + 30-day activity + achievements GET /api/media/me/watch-history Paginated watch history POST /api/media/me/stats/recalculate Recompute stats from raw data GET /api/media/me/settings Privacy settings PUT /api/media/me/settings Update privacy settings PUT /api/media/me/profile Update display name PUT /api/media/me/password Change password"},{"location":"docs/api/#route-summary","title":"Route Summary","text":"API Module Endpoint Count Express Auth 9 Users 7 Dashboard 7 Campaigns (CRUD + public + user + moderation + emails) 16 Responses 10 Email Queue 4 Representatives 7 Locations (CRUD + geocode + import) 21 Cuts 11 Shifts (CRUD + series) 19 Canvassing 20 GPS Tracking 10 Map Settings + Geocoding 3 Pages + Blocks 12 Email Templates 13 QR Codes 1 Site Settings 5 Listmonk 9 Docs Management 11 Services 2 Pangolin 16 Observability 3 Payments (public + admin) 29 Health + Metrics 3 Express Total ~248 Fastify Videos (CRUD + upload + actions + schedule + fetch) 39 Streaming 4 Public Gallery 6 Tracking 5 Reactions 4 Comments + Chat 13 Shorts + Upvotes 4 Playlists (public + user + admin) 18 User Profile 7 Health 1 Fastify Total ~101 Grand Total ~349"},{"location":"docs/architecture/","title":"Architecture","text":"Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database.
Under Construction
Detailed architecture documentation is being written. Check back soon.
"},{"location":"docs/architecture/#system-overview","title":"System Overview","text":"Browser \u2500\u2500\u25ba Nginx (reverse proxy) \u2500\u2500\u252c\u2500\u2500\u25ba Express API (port 4000) \u2500\u2500\u25ba PostgreSQL\n \u251c\u2500\u2500\u25ba Fastify Media API (port 4100) \u2500\u2500\u2518\n \u251c\u2500\u2500\u25ba React Admin GUI (port 3000)\n \u2514\u2500\u2500\u25ba MkDocs / Other Services\n"},{"location":"docs/architecture/#key-components","title":"Key Components","text":"Component Technology Role Main API Express.js + Prisma Auth, campaigns, map, shifts, pages Media API Fastify + Prisma Video library, analytics, uploads Admin GUI React + Ant Design Single-page admin application Database PostgreSQL 16 Shared by both APIs (30+ tables) Cache Redis Rate limiting, job queues, geocoding Proxy Nginx Subdomain routing, security headers"},{"location":"docs/architecture/#authentication-flow","title":"Authentication Flow","text":"This guide covers how to take Changemaker Lite from a local development setup to a publicly accessible production deployment. The main decision is how to expose your services to the internet.
"},{"location":"docs/deployment/#architecture-overview","title":"Architecture Overview","text":"Regardless of which exposure method you choose, the internal architecture is the same:
Internet \u2192 [Your exposure method] \u2192 Nginx (port 80) \u2192 Backend Services\n Nginx handles all subdomain routing internally. Every service is accessed through nginx on port 80, which proxies to the correct container based on the Host header.
app.DOMAIN Admin GUI + public pages 3000 api.DOMAIN Express API 4000 media.DOMAIN Fastify Media API 4100 DOMAIN (root) MkDocs documentation site 4004 db.DOMAIN NocoDB 8091 docs.DOMAIN MkDocs live preview 4003 code.DOMAIN Code Server 8888 git.DOMAIN Gitea 3030 n8n.DOMAIN Workflow automation 5678 home.DOMAIN Homepage dashboard 3010 listmonk.DOMAIN Newsletter manager 9001 mail.DOMAIN MailHog (dev email) 8025 qr.DOMAIN Mini QR generator 8089 draw.DOMAIN Excalidraw whiteboard 8090 vault.DOMAIN Vaultwarden password manager 8445 chat.DOMAIN Rocket.Chat team chat \u2014 events.DOMAIN Gancio event management 8092 grafana.DOMAIN Monitoring dashboards 3005"},{"location":"docs/deployment/#exposure-methods","title":"Exposure Methods","text":""},{"location":"docs/deployment/#pangolin","title":"Option 1: Pangolin + Newt Tunnel (Recommended)","text":"Admin GUI: Tunnel Management Page
The admin dashboard includes a dedicated Tunnel Management page at Admin \u2192 Settings \u2192 Tunnel. This page provides:
If you're unsure about any step above, the Tunnel page walks you through the same process interactively.
Pangolin is a self-hosted tunnel server. The Newt client container runs alongside your stack and establishes an outbound connection to your Pangolin server, which then routes public traffic back through the tunnel. No port forwarding or static IP required.
Advantages:
Requirements:
If you used config.sh, you may have already set these. Otherwise, add to your .env:
PANGOLIN_API_URL=https://api.your-pangolin-server.org/v1\nPANGOLIN_API_KEY=your_api_key_here\nPANGOLIN_ORG_ID=your_org_id\n"},{"location":"docs/deployment/#step-2-create-a-site-in-pangolin","title":"Step 2: Create a Site in Pangolin","text":"Log in to your Pangolin dashboard and create a new site:
changemaker-yourdomain.org)100.90.128.3/24)Save the credentials
The Newt Secret is only shown once during site creation. Copy it immediately.
"},{"location":"docs/deployment/#step-3-update-env-with-site-credentials","title":"Step 3: Update.env with Site Credentials","text":"PANGOLIN_SITE_ID=your_site_id\nPANGOLIN_ENDPOINT=https://your-pangolin-server.org\nPANGOLIN_NEWT_ID=your_newt_id\nPANGOLIN_NEWT_SECRET=your_newt_secret\n"},{"location":"docs/deployment/#step-4-start-the-newt-container","title":"Step 4: Start the Newt Container","text":"docker compose up -d newt\n The Newt container connects to nginx (its only dependency) and establishes the tunnel:
# From docker-compose.yml\nnewt:\n image: fosrl/newt\n container_name: newt-changemaker\n restart: unless-stopped\n environment:\n - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT}\n - NEWT_ID=${PANGOLIN_NEWT_ID}\n - NEWT_SECRET=${PANGOLIN_NEWT_SECRET}\n depends_on:\n - nginx\n Verify the connection:
docker compose logs newt --tail 20\n You should see a successful connection message.
"},{"location":"docs/deployment/#step-5-create-public-http-resources","title":"Step 5: Create Public HTTP Resources","text":"In the Pangolin dashboard, create an HTTP resource for each service you want exposed. All resources point to nginx:80 \u2014 nginx handles the routing internally.
Required resources (minimum for a working deployment):
Resource Name Domain Target Auth Admin GUIapp.yourdomain.org nginx:80 Not Protected API Server api.yourdomain.org nginx:80 Not Protected Public Site yourdomain.org nginx:80 Not Protected Optional resources (add as needed):
Resource Name Domain Target Media APImedia.yourdomain.org nginx:80 NocoDB db.yourdomain.org nginx:80 Documentation docs.yourdomain.org nginx:80 Code Server code.yourdomain.org nginx:80 Gitea git.yourdomain.org nginx:80 Grafana grafana.yourdomain.org nginx:80 Set resources to Not Protected
By default, Pangolin may enable authentication on new resources. This causes 302 redirects to the Pangolin login page instead of reaching your services. Set each resource to Not Protected (public access) unless you intentionally want Pangolin SSO in front of it.
"},{"location":"docs/deployment/#step-6-update-cors-for-production","title":"Step 6: Update CORS for Production","text":"Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000,http://localhost\n Then restart the API:
docker compose restart api\n"},{"location":"docs/deployment/#step-7-verify","title":"Step 7: Verify","text":"# Should return JSON (not a 302 redirect)\ncurl https://api.yourdomain.org/api/health\n\n# Admin GUI should load\ncurl -I https://app.yourdomain.org\n"},{"location":"docs/deployment/#cloudflare","title":"Option 2: Cloudflare Tunnel","text":"Cloudflare Tunnel (cloudflared) provides a similar zero-trust tunnel approach using Cloudflare's network. No port forwarding needed, and you get Cloudflare's CDN and DDoS protection.
Advantages:
Disadvantages:
Create a Cloudflare Tunnel in the Zero Trust dashboard
Add a cloudflared service to your docker-compose.yml:
cloudflared:\n image: cloudflare/cloudflared:latest\n container_name: cloudflared-changemaker\n restart: unless-stopped\n command: tunnel run\n environment:\n - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}\n depends_on:\n - nginx\n networks:\n - changemaker-lite\n Add your tunnel token to .env:
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here\n Configure public hostnames in the Cloudflare dashboard, all pointing to http://nginx:80:
app.yourdomain.org http://nginx:80 api.yourdomain.org http://nginx:80 yourdomain.org http://nginx:80 (add more as needed) http://nginx:80 Start the tunnel:
docker compose up -d cloudflared\n Note
The cloudflared service is not included in the default docker-compose.yml. Add it manually if you choose this method. The Newt service can be removed or left stopped.
If your server has a public IP address (e.g., a VPS or dedicated server), you can point DNS directly to it and use nginx with SSL certificates.
Advantages:
Disadvantages:
Point DNS for your domain and all subdomains to your server's IP:
A yourdomain.org \u2192 YOUR_SERVER_IP\nA *.yourdomain.org \u2192 YOUR_SERVER_IP\n Or use individual A records for each subdomain if your DNS provider doesn't support wildcards.
Open ports 80 and 443 on your server's firewall.
Install Certbot (or another ACME client) for SSL certificates:
# Ubuntu/Debian\nsudo apt install certbot\n\n# Get a wildcard certificate with DNS challenge\nsudo certbot certonly --manual --preferred-challenges dns \\\n -d yourdomain.org -d '*.yourdomain.org'\n Alternatively, use the Certbot Docker image or a Let's Encrypt companion container.
Update nginx to listen on 443 with your certificates. Add an SSL server block to nginx/conf.d/ssl.conf:
server {\n listen 443 ssl;\n server_name app.yourdomain.org;\n\n ssl_certificate /etc/nginx/ssl/fullchain.pem;\n ssl_certificate_key /etc/nginx/ssl/privkey.pem;\n\n location / {\n proxy_pass http://changemaker-v2-admin:3000;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n }\n}\n\n# Repeat for api.yourdomain.org, media.yourdomain.org, etc.\n# Or use a single server block with $host matching\n Mount certificates into the nginx container via docker-compose.yml:
nginx:\n volumes:\n - /etc/letsencrypt/live/yourdomain.org:/etc/nginx/ssl:ro\n Set up auto-renewal with a cron job or systemd timer:
0 3 * * * certbot renew --quiet && docker compose restart nginx\n Traefik alternative
If you prefer automatic SSL and don't want to manage nginx SSL config manually, consider replacing nginx with Traefik. Traefik can auto-discover Docker containers and provision Let's Encrypt certificates automatically. This would require adapting the container labels and removing the nginx service.
"},{"location":"docs/deployment/#tailscale","title":"Option 4: Tailscale / WireGuard (Private Access)","text":"For deployments that should only be accessible to specific people (not the general public), a mesh VPN like Tailscale or plain WireGuard gives you private networking without exposing anything to the internet.
Use cases:
http://100.x.x.x:3000)Note
With private access methods, you may not need subdomain routing at all. Access services directly by port: http://server-ip:3000 (admin), http://server-ip:4000 (API), etc.
Before going live, verify each item:
"},{"location":"docs/deployment/#security","title":"Security","text":"grep -c \"REQUIRED_STRONG\" .env should return 0)NODE_ENV=production set in .envENCRYPTION_KEY set and differs from JWT secretsEMAIL_TEST_MODE=false (unless you want MailHog in production)CORS_ORIGINS includes your production domainREDIS_PASSWORD)curl https://api.yourdomain.org/api/health returns JSONdocker compose ps shows api, admin, v2-postgres, redis, nginx healthydocker compose exec api npx prisma migrate deploydocker compose exec api npx prisma db seedhttps://app.yourdomain.org./scripts/backup.shdocker compose --profile monitoring up -dThe included backup script dumps PostgreSQL databases, archives uploads, and optionally uploads to S3.
"},{"location":"docs/deployment/#running-a-backup","title":"Running a Backup","text":"./scripts/backup.sh\n This creates a timestamped directory under ./backups/ containing:
changemaker_v2.sql.gz \u2014 Main PostgreSQL dump (compressed)listmonk.sql.gz \u2014 Listmonk database dump (if running)uploads.tar.gz \u2014 Media uploads archivemanifest.json \u2014 Backup metadata# Upload to S3 (requires AWS CLI + S3_BUCKET env var)\n./scripts/backup.sh --s3\n\n# Custom retention (delete local backups older than N days)\n./scripts/backup.sh --retention 14\n"},{"location":"docs/deployment/#automated-backups","title":"Automated Backups","text":"Add a cron job for daily backups:
# Edit crontab\ncrontab -e\n\n# Add daily backup at 3 AM\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh >> /var/log/changemaker-backup.log 2>&1\n\n# With S3 upload\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh --s3 >> /var/log/changemaker-backup.log 2>&1\n"},{"location":"docs/deployment/#restore","title":"Restore","text":"# Restore main database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/changemaker_v2.sql.gz | \\\n docker compose exec -T v2-postgres psql -U changemaker changemaker_v2\n\n# Restore Listmonk database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/listmonk.sql.gz | \\\n docker compose exec -T listmonk-db psql -U listmonk listmonk\n\n# Restore uploads\ntar xzf backups/changemaker-v2-backup-TIMESTAMP/uploads.tar.gz -C ./\n"},{"location":"docs/deployment/#monitoring","title":"Monitoring","text":"The monitoring stack runs behind a Docker Compose profile and is not started by default.
"},{"location":"docs/deployment/#starting-the-monitoring-stack","title":"Starting the Monitoring Stack","text":"docker compose --profile monitoring up -d\n This starts:
Service Port Purpose Prometheus 9090 Metrics collection and queries Grafana 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"},{"location":"docs/deployment/#pre-configured-dashboards","title":"Pre-configured Dashboards","text":"Grafana includes 3 auto-provisioned dashboards:
The API exposes 12 custom Prometheus metrics with the cm_ prefix:
cm_api_uptime_seconds \u2014 API uptimecm_email_queue_size \u2014 BullMQ pending emailscm_active_canvass_sessions \u2014 Active canvassing sessionscm_locations_total \u2014 Total locations in databaseapi/src/utils/metrics.tsPre-configured alerts in configs/prometheus/alerts.yml:
# Pull latest code\ngit pull origin v2\n\n# Rebuild and restart containers\ndocker compose build api admin\ndocker compose up -d api admin\n\n# Run any new migrations\ndocker compose exec api npx prisma migrate deploy\n"},{"location":"docs/deployment/#database-migrations","title":"Database Migrations","text":"Always run migrations after pulling updates:
docker compose exec api npx prisma migrate deploy\n Back up first
Always run ./scripts/backup.sh before applying migrations in production. Migrations may alter table structures and are not easily reversible.
Symptom: API returns 302 redirects to the Pangolin authentication page.
Fix: In the Pangolin dashboard, edit each resource and set Authentication to Not Protected.
"},{"location":"docs/deployment/#cors-errors","title":"CORS Errors","text":"Symptom: Browser console shows CORS errors when accessing the production domain.
Fix: Add your production app. subdomain to CORS_ORIGINS in .env, then docker compose restart api.
Check in order:
PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET in .envPANGOLIN_ENDPOINT matches your Pangolin server URLdocker compose logs newt --tail 50docker compose ps nginxdocker compose ps nginxcurl http://localhost:4000/api/healthdocker compose logs nginx --tail 50dig app.yourdomain.org should point to your Pangolin serverSee Troubleshooting for more common issues.
"},{"location":"docs/getting-started/","title":"Getting Started","text":"This guide walks you through installing Changemaker Lite, running your first deployment, and logging into the admin dashboard.
"},{"location":"docs/getting-started/#prerequisites","title":"Prerequisites","text":"git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n\nbash config.sh\n\ndocker compose up -d\n Open http://localhost:3000 and sign in with the admin email and password you configured. The API container automatically runs database migrations and seeding on first startup \u2014 no manual steps needed.
Change your password
If you used the wizard's generated password, change it immediately from the admin dashboard.
For the full setup walkthrough, see Installation.
"},{"location":"docs/getting-started/#configuration-wizard","title":"Configuration Wizard","text":"The config.sh wizard produces a fully populated .env file in 14 steps:
.env from .env.example (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 .conf.template files with domain substitution 11. Homepage Generates services.yaml 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 See Installation for detailed documentation of each step.
"},{"location":"docs/getting-started/#services","title":"Services","text":"Changemaker Lite includes 30+ Docker services organized into 8 categories:
Category Services Startup Core API, Admin, PostgreSQL, Redis, Nginxdocker compose up -d v2-postgres redis api admin nginx Media Fastify media API docker compose up -d media-api Communication Rocket.Chat, Gancio, Jitsi Meet Individual docker compose up -d commands Newsletter & Email Listmonk, MailHog docker compose up -d listmonk-app Developer Tools Code Server, MkDocs, Gitea, NocoDB, n8n Individual docker compose up -d commands Utilities Mini QR, Excalidraw, Vaultwarden, Homepage docker compose up -d mini-qr excalidraw vaultwarden homepage Monitoring Prometheus, Grafana, Alertmanager, exporters docker compose --profile monitoring up -d Infrastructure Newt tunnel, Docker socket proxy Auto-starts with tunnel configuration See Services Overview for the complete catalog with ports, feature flags, and detailed descriptions.
"},{"location":"docs/getting-started/#next-steps","title":"Next Steps","text":".env referenceThe 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.
Single instance?
If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with First Steps.
"},{"location":"docs/getting-started/control-panel/#when-to-use-ccp","title":"When to Use CCP","text":"CCP is designed for:
CCP handles the entire instance lifecycle: provisioning, configuration, health monitoring, backups, and upgrades \u2014 all from a single dashboard.
"},{"location":"docs/getting-started/control-panel/#architecture","title":"Architecture","text":"CCP runs as 4 Docker containers alongside (but independent from) your CML instances:
\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 Service Container Port Description CCP API ccp-api 5000 Express API with Docker CLI access CCP Admin ccp-admin 5100 React admin GUI CCP PostgreSQL ccp-postgres 5480 CCP metadata database CCP Redis ccp-redis 6399 Rate limiting, caching Each managed CML instance gets its own isolated set of containers and PostgreSQL database, with ports allocated from non-overlapping ranges.
"},{"location":"docs/getting-started/control-panel/#setup","title":"Setup","text":""},{"location":"docs/getting-started/control-panel/#1-run-the-setup-script","title":"1. Run the Setup Script","text":"cd changemaker-control-panel\nchmod +x setup.sh\n./setup.sh\n The setup script:
instances/ and backups/ directories.env.example to .env if not presentINSTANCES_BASE_PATH, BACKUP_STORAGE_PATH, and CML_SOURCE_PATHEdit .env and verify the key settings:
JWT_ACCESS_SECRET Auto-generated JWT signing key JWT_REFRESH_SECRET Auto-generated Refresh token signing key ENCRYPTION_KEY Auto-generated AES-256 key for instance secrets at rest INITIAL_ADMIN_EMAIL admin@example.com Bootstrap admin email INITIAL_ADMIN_PASSWORD ChangeMe2025!! Bootstrap admin password INSTANCES_BASE_PATH ./instances Where instance directories are created CML_SOURCE_PATH Auto-detected Path to CML source repo for provisioning BACKUP_STORAGE_PATH ./backups Backup archive storage PANGOLIN_API_URL \u2014 Pangolin API for tunnel management PANGOLIN_API_KEY \u2014 Pangolin authentication PANGOLIN_ORG_ID \u2014 Pangolin organization"},{"location":"docs/getting-started/control-panel/#3-start-ccp","title":"3. Start CCP","text":"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"},{"location":"docs/getting-started/control-panel/#4-log-in","title":"4. Log In","text":"Open http://localhost:5100 and sign in with the admin credentials from .env.
The Create Instance wizard walks through 5 steps:
"},{"location":"docs/getting-started/control-panel/#step-1-basic-information","title":"Step 1: Basic Information","text":"edmonton), used for directory names and compose projectedmonton.example.org)Toggle which platform features to enable for this instance:
Configure SMTP for the instance, or use MailHog for testing.
"},{"location":"docs/getting-started/control-panel/#step-4-tunnel","title":"Step 4: Tunnel","text":"Optionally configure Pangolin tunnel credentials for public access.
"},{"location":"docs/getting-started/control-panel/#step-5-review","title":"Step 5: Review","text":"Review all settings, then click Create to start provisioning.
"},{"location":"docs/getting-started/control-panel/#provisioning-flow","title":"Provisioning Flow","text":"When you create an instance, CCP runs a 13-step async provisioning process:
Step What Happens 1 Validate uniqueness (slug + domain) 2 Allocate 4 ports from ranges 3 Generate 14 secrets (passwords, JWT keys, encryption keys) 4 Create Instance record (status: PROVISIONING) 5 Create instance directory 6 Copy CML source code (rsync, excluding node_modules/.git/.env) 7 Decrypt secrets and build template context 8 Render 7 config files from Handlebars templates (docker-compose.yml, .env, nginx configs, Pangolin, Prometheus) 9 Copy static files (nginx.conf) 10docker compose pull (non-fatal if images are cached) 11 docker compose build 12 Start infrastructure (PostgreSQL + Redis), wait for healthy 13 Start API (runs migrations + seed), then start all remaining services The admin GUI polls every 3 seconds during provisioning to show progress. When complete, the instance status changes to RUNNING.
"},{"location":"docs/getting-started/control-panel/#port-allocation","title":"Port Allocation","text":"CCP allocates ports from 4 non-overlapping ranges to prevent conflicts between instances:
Range Start End Purpose API 14000 14999 Express API server Admin 13000 13999 React admin GUI PostgreSQL 15400 15499 Database Nginx 10000 10999 Reverse proxyEach new instance receives one port from each range. Ports are tracked in the database and released when instances are deleted.
"},{"location":"docs/getting-started/control-panel/#pages-overview","title":"Pages Overview","text":""},{"location":"docs/getting-started/control-panel/#dashboard","title":"Dashboard","text":"At-a-glance fleet status:
Searchable, filterable table of all instances with status, domain, health, and creation date.
"},{"location":"docs/getting-started/control-panel/#instance-detail","title":"Instance Detail","text":"5-tab view for each instance:
Tab Content Overview Status, domain, ports, features, health summary Services Per-container status grid with restart and log-view actions Logs Real-time log viewer with service filter, tail count, and time range Backups Backup list with create, download, and delete actions Tunnel Pangolin tunnel status and configuration"},{"location":"docs/getting-started/control-panel/#backups","title":"Backups","text":"Cross-instance backup management:
Filterable activity trail with 18 action types:
Each entry includes timestamp, user, action, instance, IP address, and details (expandable JSON).
"},{"location":"docs/getting-started/control-panel/#settings","title":"Settings","text":"CCP-level configuration:
Changemaker Lite uses a single .env file at the project root to configure all services. Copy the example file to get started:
cp .env.example .env\n Security Essentials
REQUIRED_STRONG_PASSWORD_CHANGE_THIS value before starting servicesopenssl rand -hex 32 (or -hex 16 where noted).env to version controlVariables are grouped by service. Each table marks whether a variable is required for a basic deployment or optional (has a sensible default or only needed for specific features).
Symbol Meaning Must be set before first run Has a working default; change for production Feature flag \u2014 opt-in"},{"location":"docs/getting-started/environment-variables/#general","title":"General","text":"Variable Default DescriptionNODE_ENV development Set to production for production deployments. Controls logging, error detail, and security checks. DOMAIN cmlite.org Root domain. Used for nginx subdomain routing (app.DOMAIN, api.DOMAIN, etc.). The root domain serves the MkDocs documentation site; all application routes live under app.DOMAIN. USER_ID 1000 UID for container file ownership. Match your host user's UID (id -u). GROUP_ID 1000 GID for container file ownership. Match your host user's GID (id -g). DOCKER_GROUP_ID 984 GID of the docker group on the host. Needed for containers that access the Docker socket. Find with getent group docker."},{"location":"docs/getting-started/environment-variables/#postgresql-main-database","title":"PostgreSQL (Main Database)","text":"The primary database for both the Express API and the Fastify Media API (shared).
Variable Default DescriptionV2_POSTGRES_USER changemaker Database username. V2_POSTGRES_PASSWORD \u2014 Must change. Database password. V2_POSTGRES_DB changemaker_v2 Database name. V2_POSTGRES_PORT 5433 Host port mapping. The container listens on 5432 internally. Connection string
The DATABASE_URL is constructed automatically inside Docker. If running locally, set:
DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2\n"},{"location":"docs/getting-started/environment-variables/#jwt-authentication","title":"JWT Authentication","text":"Variable Default Description JWT_ACCESS_SECRET \u2014 Secret for signing access tokens. Generate with openssl rand -hex 32. JWT_REFRESH_SECRET \u2014 Secret for signing refresh tokens. Must differ from the access secret. JWT_ACCESS_EXPIRY 15m Access token lifetime. Short-lived by design. JWT_REFRESH_EXPIRY 7d Refresh token lifetime. Tokens are rotated atomically on each refresh."},{"location":"docs/getting-started/environment-variables/#encryption-key","title":"Encryption Key","text":"Variable Default Description ENCRYPTION_KEY \u2014 AES key for encrypting secrets stored in the database (SMTP passwords, API keys, etc.). Generate with openssl rand -hex 32. Must not reuse a JWT secret. Required in production (NODE_ENV=production)."},{"location":"docs/getting-started/environment-variables/#initial-admin-account","title":"Initial Admin Account","text":"These credentials create the first super-admin user during database seeding (npx prisma db seed).
INITIAL_ADMIN_EMAIL admin@cmlite.org Email address for the initial admin. INITIAL_ADMIN_PASSWORD \u2014 Must change. Must be 12+ characters with uppercase, lowercase, and a digit. Change this password after first login."},{"location":"docs/getting-started/environment-variables/#api-server","title":"API Server","text":"Variable Default Description API_PORT 4000 Host port for the Express API. API_URL http://localhost:4000 Public URL of the API. Used for generating links in emails and QR codes. CORS_ORIGINS http://localhost:3000,http://localhost Comma-separated list of allowed CORS origins. Add your production domain (e.g., https://app.yourdomain.org) for production. Production CORS
If you deploy behind a tunnel (Pangolin, Cloudflare) and API requests fail with CORS errors, add your production app. subdomain here:
CORS_ORIGINS=https://app.betteredmonton.org,http://localhost:3000,http://localhost\n Then restart the API: docker compose restart api"},{"location":"docs/getting-started/environment-variables/#admin-gui","title":"Admin GUI","text":"Variable Default Description ADMIN_PORT 3000 Host port for the React admin dashboard. ADMIN_URL http://localhost:3000 Public URL of the admin GUI."},{"location":"docs/getting-started/environment-variables/#nginx-reverse-proxy","title":"Nginx Reverse Proxy","text":"Variable Default Description NGINX_HTTP_PORT 80 HTTP port. All subdomains route through nginx. NGINX_HTTPS_PORT 443 HTTPS port. SSL is typically handled by the tunnel provider (Pangolin/Cloudflare)."},{"location":"docs/getting-started/environment-variables/#redis","title":"Redis","text":"Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.
Variable Default DescriptionREDIS_PASSWORD \u2014 Must change. Redis requires authentication. REDIS_URL redis://:${REDIS_PASSWORD}@redis-changemaker:6379 Full connection URL. Uses the password variable automatically."},{"location":"docs/getting-started/environment-variables/#payments-stripe","title":"Payments (Stripe)","text":"Variable Default Description ENABLE_PAYMENTS false Set to true to enable the payments feature (memberships, products, donations). Stripe API keys are stored encrypted in the database via the admin settings page."},{"location":"docs/getting-started/environment-variables/#email-smtp","title":"Email / SMTP","text":"Variable Default Description SMTP_HOST mailhog-changemaker SMTP server. Default points to the MailHog dev container. SMTP_PORT 1025 SMTP port. 1025 for MailHog, 587 for most production SMTP. SMTP_USER (empty) SMTP username. Not needed for MailHog. SMTP_PASS (empty) SMTP password. SMTP_FROM noreply@cmlite.org \"From\" address on outgoing emails. SMTP_FROM_NAME Changemaker Lite Display name for the \"From\" header. EMAIL_TEST_MODE true When true, all emails go to MailHog instead of real SMTP. Set to false in production. TEST_EMAIL_RECIPIENT admin@cmlite.org Catch-all recipient when test mode is on. Development email
With EMAIL_TEST_MODE=true, all outgoing email is captured in MailHog at http://localhost:8025. No real emails are sent.
Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.
Variable Default DescriptionLISTMONK_PORT 9001 Listmonk web UI port. LISTMONK_DB_PORT 5434 Listmonk's own PostgreSQL port (separate from the main DB). Uses 5434 to avoid conflict with the main PostgreSQL (5432 internal / 5433 host). LISTMONK_DB_USER listmonk Listmonk database user. LISTMONK_DB_PASSWORD \u2014 Listmonk database password. LISTMONK_DB_NAME listmonk Listmonk database name. LISTMONK_WEB_ADMIN_USER admin Login for the Listmonk web dashboard. LISTMONK_WEB_ADMIN_PASSWORD \u2014 Password for the Listmonk web dashboard. LISTMONK_API_USER v2-api API user for programmatic access (auto-created by init container). LISTMONK_API_TOKEN \u2014 Token for API user. Generate with openssl rand -hex 16. LISTMONK_ADMIN_USER v2-api Same as LISTMONK_API_USER (used by the sync service). LISTMONK_ADMIN_PASSWORD \u2014 Same as LISTMONK_API_TOKEN. LISTMONK_SYNC_ENABLED false Set to true to sync participants/locations/users to Listmonk lists. LISTMONK_WEBHOOK_SECRET (empty) Shared secret for Listmonk webhook callbacks. LISTMONK_PROXY_PORT 9002 Nginx proxy port for Listmonk. Listmonk SMTP settings Listmonk has its own SMTP configuration, separate from the main platform's:
Variable Default DescriptionLISTMONK_SMTP_HOST mailhog-changemaker SMTP host for Listmonk. LISTMONK_SMTP_PORT 1025 SMTP port. LISTMONK_SMTP_USER (empty) SMTP username. LISTMONK_SMTP_PASSWORD (empty) SMTP password. LISTMONK_SMTP_TLS_TYPE none TLS mode: none, STARTTLS, or TLS. LISTMONK_SMTP_FROM Changemaker Lite <noreply@cmlite.org> From address for newsletters."},{"location":"docs/getting-started/environment-variables/#represent-api-canadian-electoral-data","title":"Represent API (Canadian Electoral Data)","text":"Variable Default Description REPRESENT_API_URL https://represent.opennorth.ca OpenNorth Represent API endpoint. Used for postal code \u2192 representative lookups. No API key required."},{"location":"docs/getting-started/environment-variables/#nocodb-data-browser","title":"NocoDB (Data Browser)","text":"Read-only database browser. Useful for inspecting data without SQL.
Variable Default DescriptionNOCODB_V2_PORT / NOCODB_PORT 8091 Host port for the NocoDB web UI. NOCODB_URL http://changemaker-v2-nocodb:8080 Internal Docker URL. NC_ADMIN_EMAIL admin@cmlite.org NocoDB admin email. NC_ADMIN_PASSWORD \u2014 NocoDB admin password."},{"location":"docs/getting-started/environment-variables/#media-manager","title":"Media Manager","text":"Video library with upload, analytics, scheduling, and a public gallery.
Variable Default DescriptionENABLE_MEDIA_FEATURES false Set to true to enable the media system. MEDIA_API_PORT 4100 Fastify media API port. MEDIA_API_PUBLIC_URL http://media-api:4100 Internal URL for the media API container. MEDIA_ROOT /media/library Path to the video library inside the container. MEDIA_UPLOADS /media/uploads Path for upload processing. MAX_UPLOAD_SIZE_GB 10 Maximum single-file upload size in gigabytes. PUBLIC_MEDIA_PORT 3100 Public media gallery server port. VIDEO_PLAYER_DEBUG false Enable verbose video player logging. Analytics & scheduling settings Variable Default Description VIDEO_ANALYTICS_RETENTION_DAYS 90 Days to retain analytics data. GDPR-compliant with IP hashing. VIDEO_ANALYTICS_IP_HASHING_ENABLED true Hash viewer IPs for privacy. VIDEO_SCHEDULE_DEFAULT_TIMEZONE UTC Default timezone for scheduled publishing. VIDEO_SCHEDULE_NOTIFICATION_ENABLED true Notify on scheduled publish/unpublish. VIDEO_PREVIEW_LINK_EXPIRY_HOURS 24 Preview link JWT expiry (hours)."},{"location":"docs/getting-started/environment-variables/#gitea-git-hosting","title":"Gitea (Git Hosting)","text":"Self-hosted Git repository. Optional service.
Variable Default DescriptionGITEA_URL http://gitea-changemaker:3000 Internal container URL for Gitea. GITEA_PORT / GITEA_WEB_PORT 3030 Gitea web UI port. GITEA_SSH_PORT 2222 Gitea SSH port for git operations. GITEA_DB_TYPE mysql Database type (Gitea uses its own MySQL). GITEA_DB_HOST gitea-db:3306 Internal database host. GITEA_DB_NAME gitea Database name. GITEA_DB_USER gitea Database user. GITEA_DB_PASSWD \u2014 Gitea database password. GITEA_DB_ROOT_PASSWORD \u2014 MySQL root password for Gitea. GITEA_ROOT_URL https://git.cmlite.org Public-facing URL for Gitea. GITEA_DOMAIN git.cmlite.org Domain used in git clone URLs. Gitea Docs Comments Enable comments on MkDocs documentation pages, backed by Gitea Issues.
Variable Default DescriptionGITEA_COMMENTS_ENABLED false Enable comments on MkDocs pages. GITEA_API_TOKEN (empty) Personal access token with repo write scope. Create in Gitea \u2192 Settings \u2192 Applications. GITEA_COMMENTS_REPO_OWNER (empty) Gitea username that owns the docs-comments repo. GITEA_COMMENTS_REPO_NAME docs-comments Repository name (auto-created via admin setup). GITEA_OAUTH_CLIENT_ID (empty) OAuth2 application client ID (create in Gitea \u2192 Settings \u2192 Applications \u2192 OAuth2). GITEA_OAUTH_CLIENT_SECRET (empty) OAuth2 application client secret."},{"location":"docs/getting-started/environment-variables/#n8n-workflow-automation","title":"n8n (Workflow Automation)","text":"Variable Default Description N8N_PORT 5678 n8n web UI port. N8N_HOST n8n.cmlite.org Public hostname for n8n. N8N_ENCRYPTION_KEY \u2014 Encryption key for n8n credentials storage. N8N_USER_EMAIL admin@example.com Initial n8n admin email. N8N_USER_PASSWORD \u2014 Initial n8n admin password. GENERIC_TIMEZONE UTC Timezone for n8n cron triggers."},{"location":"docs/getting-started/environment-variables/#mkdocs-documentation","title":"MkDocs (Documentation)","text":"Variable Default Description MKDOCS_PORT 4003 MkDocs dev server port (live preview). MKDOCS_SITE_SERVER_PORT 4004 MkDocs static site server port. BASE_DOMAIN https://cmlite.org Base URL for generated documentation links. MKDOCS_PREVIEW_URL http://mkdocs:8000 Internal container URL. MKDOCS_DOCS_PATH /mkdocs/docs Documentation source directory inside the container."},{"location":"docs/getting-started/environment-variables/#code-server-web-ide","title":"Code Server (Web IDE)","text":"Variable Default Description CODE_SERVER_PORT 8888 Code Server web UI port. CODE_SERVER_URL http://code-server:8080 Internal container URL. USER_NAME coder User account inside the Code Server container."},{"location":"docs/getting-started/environment-variables/#homepage-service-dashboard","title":"Homepage (Service Dashboard)","text":"Variable Default Description HOMEPAGE_PORT 3010 Homepage web UI port. HOMEPAGE_EMBED_PORT 8887 Port for iframe embedding in admin. HOMEPAGE_VAR_BASE_URL http://localhost Base URL used in Homepage service links."},{"location":"docs/getting-started/environment-variables/#mini-qr-qr-code-generator","title":"Mini QR (QR Code Generator)","text":"Variable Default Description MINI_QR_PORT 8089 Mini QR direct access port. MINI_QR_URL http://mini-qr:8080 Internal container URL. MINI_QR_EMBED_PORT 8885 Port for iframe embedding (walk sheets, cut exports)."},{"location":"docs/getting-started/environment-variables/#excalidraw-whiteboard","title":"Excalidraw (Whiteboard)","text":"Variable Default Description EXCALIDRAW_PORT 8090 Excalidraw web UI port. EXCALIDRAW_URL http://excalidraw-changemaker:80 Internal container URL. EXCALIDRAW_EMBED_PORT 8886 Port for iframe embedding. EXCALIDRAW_WS_URL wss://draw.cmlite.org WebSocket URL for real-time collaboration."},{"location":"docs/getting-started/environment-variables/#vaultwarden-password-manager","title":"Vaultwarden (Password Manager)","text":"Self-hosted Bitwarden-compatible password manager. Optional service.
Variable Default DescriptionVAULTWARDEN_PORT 8445 Vaultwarden web UI port. VAULTWARDEN_URL http://vaultwarden-changemaker:80 Internal container URL. VAULTWARDEN_EMBED_PORT 8890 Port for iframe embedding in admin. VAULTWARDEN_ADMIN_TOKEN (empty) Admin panel token (access at /admin). Generate with openssl rand -hex 32. VAULTWARDEN_DOMAIN https://vault.cmlite.org Public-facing URL. Must use HTTPS \u2014 Bitwarden web vault enforces HTTPS for account creation. Set to your Pangolin tunnel URL. VAULTWARDEN_SIGNUPS_ALLOWED false Allow new user self-registration. Keep false and use admin panel invites. VAULTWARDEN_WEBSOCKET_ENABLED true Enable WebSocket notifications for real-time sync. VAULTWARDEN_SMTP_SECURITY off SMTP security mode: off for MailHog, starttls or force_tls for production. Uses the main SMTP_* variables for host/credentials. Initial setup
The vaultwarden-init container automatically invites the INITIAL_ADMIN_EMAIL user when starting. Check MailHog (or your SMTP) for the invitation email.
Self-hosted team chat for volunteer coordination. Requires MongoDB (auto-configured).
Variable Default DescriptionENABLE_CHAT false Set to true to enable the Rocket.Chat integration. The initial default; once saved in admin Settings, the DB value is authoritative. ROCKETCHAT_ADMIN_USER rcadmin Rocket.Chat admin username. ROCKETCHAT_ADMIN_PASSWORD \u2014 Rocket.Chat admin password. ROCKETCHAT_URL http://rocketchat-changemaker:3000 Internal container URL. ROCKETCHAT_EMBED_PORT 8891 Port for iframe embedding in admin."},{"location":"docs/getting-started/environment-variables/#gancio-event-management","title":"Gancio (Event Management)","text":"Self-hosted event management platform. Uses the shared PostgreSQL database (auto-created by init-gancio-db.sh).
GANCIO_PORT 8092 Gancio web UI port. GANCIO_URL http://gancio-changemaker:13120 Internal container URL. GANCIO_EMBED_PORT 8892 Port for iframe embedding in admin. GANCIO_BASE_URL https://events.cmlite.org Public-facing URL for Gancio. Used in event links. GANCIO_ADMIN_USER admin Gancio admin username for shift-to-event sync (OAuth login). GANCIO_ADMIN_PASSWORD \u2014 Gancio admin password. GANCIO_SYNC_ENABLED false Set to true to enable automatic shift \u2192 Gancio event synchronization."},{"location":"docs/getting-started/environment-variables/#jitsi-meet-video-conferencing","title":"Jitsi Meet (Video Conferencing)","text":"Self-hosted video conferencing with JWT authentication. Integrates with Rocket.Chat for in-channel video calls.
Variable Default DescriptionENABLE_MEET false Set to true to enable the Jitsi Meet integration. The initial default; once saved in admin Settings, the DB value is authoritative. JITSI_APP_ID changemaker JWT application ID. Must match across Jitsi Prosody, Rocket.Chat app settings, and JWT_ACCEPTED_ISSUERS/JWT_ACCEPTED_AUDIENCES. JITSI_APP_SECRET \u2014 JWT secret for signing Jitsi tokens. Generate with openssl rand -hex 32. Shared between Jitsi Prosody, Rocket.Chat, and the API. JITSI_JICOFO_AUTH_PASSWORD \u2014 Internal XMPP password for Jicofo (conference focus). Generate with openssl rand -hex 16. JITSI_JVB_AUTH_PASSWORD \u2014 Internal XMPP password for JVB (video bridge). Generate with openssl rand -hex 16. JITSI_EMBED_PORT 8893 Port for iframe embedding in admin. JITSI_URL http://jitsi-web-changemaker:80 Internal container URL. JVB_ADVERTISE_IP (empty) Server's public IP address. Required in production for NAT traversal so remote participants can connect. JVB_PORT 10000 UDP port for media traffic. Must be open in your firewall. Production requirements
JVB_ADVERTISE_IP must be set to your server's public IP for calls to work outside the local network.10000/udp must be open in your firewall for media traffic.Send SMS messages via an Android phone running the Termux API server. The phone acts as an SMS gateway.
Variable Default DescriptionENABLE_SMS false Set to true to enable SMS campaigns. The initial default; once saved in admin Settings, the DB value is authoritative. TERMUX_API_URL http://10.0.0.193:5001 URL of the Termux API server running on the Android phone. TERMUX_API_KEY (empty) API key for authenticating with the Termux server (HMAC auth via X-API-Key header). SMS_DELAY_BETWEEN_MS 3000 Delay between sending individual SMS messages (ms). Prevents carrier throttling. SMS_MAX_RETRIES 3 Maximum retry attempts for failed SMS sends. SMS_RESPONSE_SYNC_INTERVAL_MS 30000 How often to poll the phone's inbox for responses (ms). SMS_DEVICE_MONITOR_INTERVAL_MS 30000 How often to check device health \u2014 battery, connectivity (ms). GUI configuration
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.
"},{"location":"docs/getting-started/environment-variables/#mailhog-development-email","title":"MailHog (Development Email)","text":"Variable Default DescriptionMAILHOG_SMTP_PORT 1025 SMTP port for capturing emails. MAILHOG_WEB_PORT 8025 Web UI to view captured emails."},{"location":"docs/getting-started/environment-variables/#nar-national-address-register","title":"NAR (National Address Register)","text":"Canadian address data import for geographic canvassing.
Variable Default DescriptionNAR_DATA_DIR /data Path to extracted NAR data inside the container. Expects YYYYMM/Addresses/ and YYYYMM/Locations/ subdirectories. Mount via ./data:/data:ro in Docker Compose. Download NAR data from Statistics Canada.
"},{"location":"docs/getting-started/environment-variables/#geocoding","title":"Geocoding","text":"Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.
Variable Default DescriptionMAPBOX_API_KEY (empty) Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. Sign up. GEOCODING_RATE_LIMIT_MS 1100 Delay between requests to free providers (ms). Respects rate limits. GEOCODING_CACHE_ENABLED true Enable Redis-backed geocoding cache. GEOCODING_CACHE_TTL_HOURS 24 Cache lifetime in hours. GOOGLE_MAPS_API_KEY (empty) Google Maps API key. Most accurate but $0.005/request after free tier. GOOGLE_MAPS_ENABLED false Enable Google Maps as a geocoding provider. GEOCODING_PARALLEL_ENABLED true Enable parallel geocoding for bulk imports (~10x speedup). GEOCODING_BATCH_SIZE 10 Number of concurrent geocoding requests during bulk operations. BULK_GEOCODE_ENABLED true Enable bulk re-geocoding from the admin UI. BULK_GEOCODE_MAX_BATCH 5000 Maximum locations per bulk geocoding run."},{"location":"docs/getting-started/environment-variables/#overpass-area-import","title":"Overpass / Area Import","text":"OpenStreetMap data import for map enrichment.
Variable Default DescriptionOVERPASS_API_URL https://overpass-api.de/api/interpreter Overpass API endpoint. Use a private instance for heavy usage. OVERPASS_MIN_DELAY_MS 30000 Minimum delay between requests (ms). The public API requires 30 seconds. AREA_IMPORT_MAX_GRID_POINTS 500 Maximum reverse-geocode grid points per area import."},{"location":"docs/getting-started/environment-variables/#pangolin-tunnel","title":"Pangolin Tunnel","text":"Expose services to the internet without port forwarding, using a self-hosted Pangolin instance.
Variable Default DescriptionPANGOLIN_API_URL https://api.bnkserve.org/v1 Pangolin server API endpoint. PANGOLIN_API_KEY (empty) API key for Pangolin management. PANGOLIN_ORG_ID (empty) Organization ID in Pangolin. PANGOLIN_SITE_ID (empty) Site ID (populated after setup via admin GUI). PANGOLIN_ENDPOINT https://pangolin.bnkserve.org Pangolin tunnel endpoint. PANGOLIN_NEWT_ID (empty) Newt client ID (populated after setup). PANGOLIN_NEWT_SECRET (empty) Newt client secret (populated after setup). Setup flow
Configure the tunnel from Admin \u2192 Settings \u2192 Pangolin. The setup wizard walks you through creating a site, copying credentials, and connecting the Newt container. See Deployment for the full guide.
"},{"location":"docs/getting-started/environment-variables/#monitoring","title":"Monitoring","text":"These services are behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Variable Default Description PROMETHEUS_PORT 9090 Prometheus web UI / query port. GRAFANA_PORT 3005 Grafana dashboard port. GRAFANA_ADMIN_PASSWORD admin Change in production. GRAFANA_ROOT_URL http://localhost:3005 Public URL for Grafana (used in links). CADVISOR_PORT 8086 cAdvisor container metrics port. NODE_EXPORTER_PORT 9100 Prometheus node exporter port. REDIS_EXPORTER_PORT 9121 Redis metrics exporter port. ALERTMANAGER_PORT 9093 Alertmanager web UI port. GOTIFY_PORT 8889 Gotify push notification port. GOTIFY_ADMIN_USER admin Gotify admin username. GOTIFY_ADMIN_PASSWORD admin Change in production. GRAFANA_EMBED_PORT 8894 Port for iframe embedding Grafana in admin. ALERTMANAGER_EMBED_PORT 8895 Port for iframe embedding Alertmanager in admin."},{"location":"docs/getting-started/environment-variables/#bunker-ops-fleet-management","title":"Bunker Ops (Fleet Management)","text":"Remote metrics push for managing multiple Changemaker Lite instances from a central monitoring server.
Variable Default DescriptionINSTANCE_LABEL (empty) Unique label for this instance (used as a Prometheus metric label). Falls back to DOMAIN if empty. BUNKER_OPS_ENABLED false Enable remote metrics push to a central VictoriaMetrics server. BUNKER_OPS_REMOTE_WRITE_URL (empty) VictoriaMetrics remote_write endpoint (e.g., https://ops.example.com/api/v1/write)."},{"location":"docs/getting-started/environment-variables/#generating-secrets","title":"Generating Secrets","text":"Use these commands to generate all required secrets at once:
# JWT secrets (two separate values)\necho \"JWT_ACCESS_SECRET=$(openssl rand -hex 32)\"\necho \"JWT_REFRESH_SECRET=$(openssl rand -hex 32)\"\n\n# Encryption key (must differ from JWT secrets)\necho \"ENCRYPTION_KEY=$(openssl rand -hex 32)\"\n\n# Database and Redis passwords\necho \"V2_POSTGRES_PASSWORD=$(openssl rand -hex 24)\"\necho \"REDIS_PASSWORD=$(openssl rand -hex 24)\"\n\n# Listmonk\necho \"LISTMONK_DB_PASSWORD=$(openssl rand -hex 24)\"\necho \"LISTMONK_WEB_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\nLISTMONK_TOKEN=$(openssl rand -hex 16)\necho \"LISTMONK_API_TOKEN=$LISTMONK_TOKEN\"\necho \"LISTMONK_ADMIN_PASSWORD=$LISTMONK_TOKEN\"\n\n# Supporting services\necho \"GITEA_DB_PASSWD=$(openssl rand -hex 24)\"\necho \"GITEA_DB_ROOT_PASSWORD=$(openssl rand -hex 24)\"\necho \"N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)\"\necho \"N8N_USER_PASSWORD=$(openssl rand -hex 16)\"\necho \"NC_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\necho \"INITIAL_ADMIN_PASSWORD=$(openssl rand -base64 18)\"\n\n# Vaultwarden\necho \"VAULTWARDEN_ADMIN_TOKEN=$(openssl rand -hex 32)\"\n\n# Rocket.Chat\necho \"ROCKETCHAT_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\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 Tip
Copy the output and paste the values into your .env file. The INITIAL_ADMIN_PASSWORD uses base64 encoding to ensure it contains uppercase, lowercase, and digits (meeting the password policy).
For a basic deployment with campaigns, map, and admin:
Required variablesV2_POSTGRES_PASSWORD=...\nREDIS_PASSWORD=...\nJWT_ACCESS_SECRET=...\nJWT_REFRESH_SECRET=...\nENCRYPTION_KEY=...\nINITIAL_ADMIN_PASSWORD=...\n Start servicesdocker compose up -d v2-postgres redis api admin\n For the complete platform including media, newsletters, monitoring, and all services:
Additional variables needed# Everything above, plus:\nENABLE_MEDIA_FEATURES=true\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=...\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 Start servicesdocker compose up -d\ndocker compose --profile monitoring up -d\n"},{"location":"docs/getting-started/features/","title":"Features at a Glance","text":"Changemaker Lite bundles advocacy campaigns, geographic mapping, volunteer management, media hosting, and landing pages into a single self-hosted platform. Every feature can be toggled on or off from Settings in the admin panel.
"},{"location":"docs/getting-started/features/#core-features","title":"Core Features","text":"Advocacy Campaigns
Help supporters contact elected representatives through email campaigns with postal code lookup and a public response wall.
Campaign guide
Map & Canvassing
Manage locations, draw canvassing territories, schedule volunteer shifts, and run GPS-tracked door-to-door outreach.
Map guide
Media Manager
Upload videos and photos, curate playlists, publish a shorts feed, and track engagement with built-in analytics.
Media guide
Landing Pages
Build campaign microsites with a drag-and-drop GrapesJS visual editor and publish at custom slugs.
Landing pages guide
Payments (Stripe)
Accept memberships, product sales, and donations with encrypted Stripe integration and branded donation pages.
Payments guide
SMS Campaigns
Text message outreach via a Termux Android bridge with contact lists, templates, and response tracking.
SMS guide
Public Homepage
Customizable landing page with hero section, live stats, featured campaigns, upcoming shifts, and activity feed.
Homepage guide
Newsletter (Listmonk)
Opt-in mailing lists and newsletter campaigns with automatic subscriber sync from shifts and contacts.
Newsletter guide
Email Templates
Reusable email templates with variable substitution for campaign communications.
Email templates guide
Team Chat (Rocket.Chat)
Self-hosted team chat with iframe integration, floating widget, and native mobile app support.
Chat guide
Video Conferencing (Jitsi)
Self-hosted video calls integrated with Rocket.Chat via JWT authentication \u2014 no separate login required.
Video conferencing guide
Events (Gancio)
Self-hosted event management with automatic shift-to-event sync and an embeddable calendar widget.
Events guide
Password Manager (Vaultwarden)
Bitwarden-compatible password vault for secure team credential sharing.
Password manager guide
User Provisioning
Automatic account creation and sync across Rocket.Chat, Gitea, Vaultwarden, and Listmonk.
User provisioning guide
People / Contacts
Centralized contact management for supporters, donors, and community members with cross-module linking.
People guide
Whiteboard (Excalidraw)
Self-hosted collaborative whiteboard for brainstorming, planning, and visual collaboration.
Whiteboard guide
Social Connections
Friend system, activity feed, groups, profiles, pokes, and privacy controls for volunteer community building.
Social guide
Achievements & Leaderboard
Badge system with 11 achievements across 4 categories, progress tracking, and competitive leaderboards.
Achievements guide
Volunteer Quick Join
QR code invite links for instant volunteer onboarding \u2014 scan, fill a short form, and start canvassing.
Quick join guide
Email Automation
Automated volunteer lifecycle emails \u2014 thank-you notes, shift reminders, weekly summaries, and re-engagement campaigns.
Automation guide
Data Quality Dashboard
Geocoding quality metrics with per-provider stats, confidence tiers, and coverage analysis.
Data quality guide
Documentation Analytics
Page view tracking and engagement metrics for MkDocs documentation pages.
Docs analytics guide
Docs Comments
Gitea-backed comment system for documentation pages with anonymous posting and moderation.
Docs comments guide
Command Palette
Global Ctrl+K search across pages, campaigns, locations, users, settings, and media.
Command palette guide
Navigation Settings
Customize the public navigation menu with feature toggles, custom links, and drag-and-drop reordering.
Navigation guide
Platform Settings
Five-tab settings page covering organization details, theme colors, email configuration, feature flags, and notifications.
Settings guide
Social Sharing (OG Tags)
Open Graph meta tags for campaigns, landing pages, and gallery videos \u2014 rich link previews on social media.
OG sharing guide
Gallery Ads
Internal ad system with 5 ad types, audience targeting, scheduling, frequency caps, and CTR analytics.
Gallery ads guide
Self-Service Contact Profile
Token-based public profile pages where contacts can view and update their information and preferences.
Contact profile guide
You've installed Changemaker Lite \u2014 here's what to do next.
"},{"location":"docs/getting-started/first-steps/#1-log-in","title":"1. Log In","text":"Open the admin panel at http://localhost:3000 (or app.DOMAIN in production) and sign in with the admin email and password you configured during setup.
Change your password
If you used the wizard's generated password, change it immediately from Settings > Organization.
"},{"location":"docs/getting-started/first-steps/#2-explore-the-dashboard","title":"2. Explore the Dashboard","text":"The dashboard gives you an at-a-glance view of platform activity. Initially it will be empty \u2014 that's normal.
"},{"location":"docs/getting-started/first-steps/#3-configure-settings","title":"3. Configure Settings","text":"Visit Settings (/app/settings) to:
Go to Campaigns (/app/campaigns) and click Create Campaign:
/campaignsGo to Locations (/app/map) and add addresses:
Go to Shifts (/app/map/shifts) and create your first volunteer shift:
/shifts) with volunteersShare the shifts page link or generate QR codes for in-person events. Volunteers sign up with just an email address.
"},{"location":"docs/getting-started/first-steps/#next-steps","title":"Next Steps","text":"Changemaker Lite runs as a set of Docker containers orchestrated by Docker Compose. The config.sh wizard handles all configuration \u2014 or you can set things up manually.
# Clone the repository\ngit clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n\n# Run the configuration wizard\nbash config.sh\n\n# Start all services\ndocker compose up -d\n Open http://localhost:3000 and sign in with the admin credentials you configured. Database migrations and seeding run automatically on first startup.
Change your password
If you used the wizard's generated password, change it immediately from the admin dashboard.
"},{"location":"docs/getting-started/installation/#configuration-wizard-configsh","title":"Configuration Wizard (config.sh)","text":"The wizard performs 14 steps to produce a fully configured .env file and prepare the system for startup. Each step is interactive with sensible defaults.
Verifies that Docker, Docker Compose v2, and OpenSSL are installed. Exits immediately if any are missing, with links to installation guides.
"},{"location":"docs/getting-started/installation/#step-2-environment-file-setup","title":"Step 2: Environment File Setup","text":".env exists, copies .env.example as the starting point.env already exists, offers to back it up (timestamped copy) and create a fresh one, or update values in placePrompts for your root domain (default: cmlite.org) and derives 14 environment variables from it:
DOMAIN example.org BASE_DOMAIN https://example.org GITEA_ROOT_URL https://git.example.org GITEA_DOMAIN git.example.org N8N_HOST n8n.example.org SMTP_FROM noreply@example.org INITIAL_ADMIN_EMAIL admin@example.org NC_ADMIN_EMAIL admin@example.org EXCALIDRAW_WS_URL wss://draw.example.org LISTMONK_SMTP_FROM Changemaker Lite <noreply@example.org> HOMEPAGE_VAR_BASE_URL https://example.org VAULTWARDEN_DOMAIN https://vault.example.org GANCIO_BASE_URL https://events.example.org TEST_EMAIL_RECIPIENT admin@example.org Also updates mkdocs/mkdocs.yml with the new site_url and repo_url, and asks whether this is a production deployment (sets NODE_ENV=production).
Prompts for the initial super-admin email and password. The password is validated against the security policy:
Auto-generates 21 unique secrets \u2014 no placeholder passwords remain after this step:
Category Count Secrets JWT & Encryption 3JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY (64-char hex) Database 2 V2_POSTGRES_PASSWORD, REDIS_PASSWORD (24-char alphanumeric) Listmonk 3 LISTMONK_DB_PASSWORD, LISTMONK_WEB_ADMIN_PASSWORD, LISTMONK_API_TOKEN NocoDB 1 NC_ADMIN_PASSWORD Gitea 2 GITEA_DB_PASSWD, GITEA_DB_ROOT_PASSWORD n8n 2 N8N_ENCRYPTION_KEY, N8N_USER_PASSWORD Monitoring 2 GRAFANA_ADMIN_PASSWORD, GOTIFY_ADMIN_PASSWORD Vaultwarden 1 VAULTWARDEN_ADMIN_TOKEN (64-char hex) Rocket.Chat 1 ROCKETCHAT_ADMIN_PASSWORD Gancio 1 GANCIO_ADMIN_PASSWORD Jitsi Meet 3 JITSI_APP_SECRET (64-char hex), JITSI_JICOFO_AUTH_PASSWORD, JITSI_JVB_AUTH_PASSWORD"},{"location":"docs/getting-started/installation/#step-6-email-configuration","title":"Step 6: Email Configuration","text":"Choose between:
http://localhost:8025 for developmentEnable or disable 9 optional platform features:
Flag Environment Variable What It Enables Media ManagerENABLE_MEDIA_FEATURES=true Video library, analytics, scheduled publishing Listmonk Sync LISTMONK_SYNC_ENABLED=true Newsletter subscriber sync from platform participants Payments ENABLE_PAYMENTS=true Stripe-based products, donations, and plans Rocket.Chat ENABLE_CHAT=true Team communication platform Gancio Events GANCIO_SYNC_ENABLED=true Shift-to-event sync with Gancio Jitsi Meet ENABLE_MEET=true Video conferencing (also prompts for server public IP) SMS Campaigns ENABLE_SMS=true Termux Android bridge for SMS (also prompts for API URL) Docs Comments GITEA_COMMENTS_ENABLED=true Gitea-backed page comments on documentation Bunker Ops BUNKER_OPS_ENABLED=true Fleet metrics push to central server (also prompts for remote write URL)"},{"location":"docs/getting-started/installation/#step-8-tunnel-configuration-pangolin","title":"Step 8: Tunnel Configuration (Pangolin)","text":"Optionally configures Pangolin tunnel credentials for secure public access:
PANGOLIN_API_URL \u2014 API endpoint (default: https://api.bnkserve.org/v1)PANGOLIN_API_KEY \u2014 Authentication keyPANGOLIN_ORG_ID \u2014 Organization identifierComplete tunnel setup is done from the admin GUI at Settings > Tunnel after services are running.
"},{"location":"docs/getting-started/installation/#step-9-cors-origins","title":"Step 9: CORS Origins","text":"Automatically calculates allowed origins from your domain:
http://app.DOMAIN,https://app.DOMAIN,http://DOMAIN,https://DOMAIN,http://localhost:3000,http://localhost,http://localhost:4003\n"},{"location":"docs/getting-started/installation/#step-10-nginx-config-generation","title":"Step 10: Nginx Config Generation","text":"Renders all .conf.template files in nginx/conf.d/ by substituting ${DOMAIN} with your configured domain. This produces the nginx configuration files that handle subdomain routing.
Generates configs/homepage/services.yaml with 27 service entries (both production and local development URLs) for the Homepage service dashboard.
Creates and sets permissions (775) on 12 directories needed by containers:
Directory Purposeconfigs/code-server/.config Code Server configuration configs/code-server/.local Code Server local data mkdocs/.cache MkDocs build cache mkdocs/site MkDocs built site output assets/uploads Listmonk uploads assets/images Shared images assets/icons Homepage icons media/local/inbox Media upload inbox media/local/thumbnails Video thumbnails media/public Public media files local-files n8n local files data NAR import data"},{"location":"docs/getting-started/installation/#step-13-upgrade-watcher-optional","title":"Step 13: Upgrade Watcher (Optional)","text":"Installs a systemd path watcher that enables the admin GUI's \"Check for Updates\" and \"Start Upgrade\" buttons. This step requires sudo and is optional \u2014 you can install it later or use the CLI upgrade script directly.
The watcher installs two systemd units:
changemaker-upgrade.path \u2014 watches for data/upgrade/trigger.jsonchangemaker-upgrade.service \u2014 runs scripts/upgrade-watcher.sh when triggeredDisplays a configuration summary showing all choices made, then prints startup commands.
"},{"location":"docs/getting-started/installation/#what-gets-modified","title":"What Gets Modified","text":"After the wizard completes, the following files have been created or modified:
File Action.env Created (or updated) with all configuration values mkdocs/mkdocs.yml Updated site_url and repo_url with domain nginx/conf.d/*.conf Generated from .conf.template files configs/homepage/services.yaml Generated with all service URLs 12 directories Created with container-friendly permissions systemd units (optional) Installed to /etc/systemd/system/"},{"location":"docs/getting-started/installation/#manual-setup-alternative","title":"Manual Setup (Alternative)","text":"If you prefer to configure by hand instead of using the wizard:
cp .env.example .env\n At minimum, set these required secrets:
# 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)\nENCRYPTION_KEY=$(openssl rand -hex 32)\n Set your admin credentials (password must meet the 12+ char complexity requirement):
INITIAL_ADMIN_EMAIL=admin@yourdomain.org\nINITIAL_ADMIN_PASSWORD=YourStrongPassword1\n Then configure optional sections:
DOMAIN and all derived variables (see Step 3 table above)SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_TEST_MODE=falsePANGOLIN_API_URL, PANGOLIN_API_KEY, PANGOLIN_ORG_IDSee Environment Variables for every available option.
"},{"location":"docs/getting-started/installation/#full-stack-startup","title":"Full Stack Startup","text":"After configuration, start the entire platform:
docker compose up -d\n That's it. Docker handles the startup order automatically:
Watch the startup progress:
docker compose logs -f api --tail 20\n Once you see Starting server on port 4000, open http://localhost:3000 and log in.
The monitoring stack (Prometheus, Grafana, Alertmanager) uses a Docker Compose profile and isn't included by default:
docker compose --profile monitoring up -d\n"},{"location":"docs/getting-started/installation/#start-only-core-services","title":"Start Only Core Services","text":"If you prefer a minimal startup (lower resource usage):
docker compose up -d v2-postgres redis api admin nginx\n Manual migrations
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:
cd api && npx prisma migrate deploy && npx prisma db seed\n See Services Overview for the complete service catalog.
"},{"location":"docs/getting-started/installation/#verifying-installation","title":"Verifying Installation","text":"After starting services, verify everything is healthy:
# 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 You should see the API return {\"status\":\"ok\"} and all started containers in a \"running\" state.
.env referenceChangemaker Lite runs as 30+ Docker containers orchestrated by Docker Compose. This page catalogs every service, organized by category.
Quick reference
Use docker compose ps to see which services are currently running, or docker compose ps -a to include stopped containers.
These services form the minimum viable platform. Start them first.
Container Port Descriptionchangemaker-v2-api 4000 Express.js REST API (Prisma ORM) changemaker-v2-admin 3000 React admin GUI (Vite + Ant Design) changemaker-v2-postgres 5433 PostgreSQL 16 \u2014 primary database redis-changemaker 6379 Redis 7 \u2014 cache, rate limiting, job queues changemaker-v2-nginx 80 Nginx reverse proxy \u2014 subdomain routing # 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 The API container automatically runs database migrations and seeding on startup via its entrypoint script.
Note
Nginx is technically optional for local development (you can access services directly by port), but required for production subdomain routing.
"},{"location":"docs/getting-started/services/#media","title":"Media","text":"Container Port Description Feature Flagchangemaker-media-api 4100 Fastify media API \u2014 video library, analytics, scheduling ENABLE_MEDIA_FEATURES=true docker compose up -d media-api\n 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.
"},{"location":"docs/getting-started/services/#communication","title":"Communication","text":""},{"location":"docs/getting-started/services/#rocketchat-team-chat","title":"Rocket.Chat (Team Chat)","text":"Container Port Description Feature Flagrocketchat-changemaker 8891 Rocket.Chat server ENABLE_CHAT=true mongodb-changemaker \u2014 MongoDB (Rocket.Chat data store) \u2014 nats-changemaker \u2014 NATS (Rocket.Chat message bus) \u2014 docker compose up -d rocketchat mongodb nats\n"},{"location":"docs/getting-started/services/#gancio-events","title":"Gancio (Events)","text":"Container Port Description Feature Flag gancio-changemaker 8092 Gancio event platform GANCIO_SYNC_ENABLED=true gancio-init \u2014 Init container \u2014 creates Gancio database \u2014 docker compose up -d gancio\n Init containers
gancio-init 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.
jitsi-web-changemaker 8893 Jitsi web interface ENABLE_MEET=true jitsi-prosody-changemaker \u2014 XMPP server (Prosody) \u2014 jitsi-jicofo-changemaker \u2014 Jitsi conference focus \u2014 jitsi-jvb-changemaker 10000/udp Jitsi video bridge \u2014 docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb\n Firewall requirement
Jitsi requires UDP port 10000 open in your firewall for video/audio media traffic. Set JVB_ADVERTISE_IP in .env to your server's public IP address.
listmonk-app 9001 Listmonk newsletter platform LISTMONK_SYNC_ENABLED=true listmonk-db 5432 PostgreSQL (Listmonk's own database) \u2014 listmonk-init \u2014 Init container \u2014 creates API user \u2014 mailhog-changemaker 8025 MailHog email capture (development) EMAIL_TEST_MODE=true # Newsletter platform\ndocker compose up -d listmonk-app\n\n# Email testing (captures all outgoing emails)\ndocker compose up -d mailhog\n Listmonk has its own PostgreSQL instance separate from the main database. The listmonk-init container auto-creates the API user for platform integration.
code-server-changemaker 8888 VS Code in the browser mkdocs-changemaker 4003 MkDocs live preview (hot reload) mkdocs-site-server-changemaker 4004 MkDocs static site server gitea-changemaker 3030 Gitea \u2014 self-hosted Git repository gitea-db \u2014 PostgreSQL (Gitea's database) changemaker-v2-nocodb 8091 NocoDB \u2014 read-only database browser nocodb-init \u2014 Init container \u2014 registers database n8n-changemaker 5678 n8n \u2014 workflow automation # 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 Tip
mkdocs (port 4003) provides live editing with hot reload for documentation authors. mkdocs-site-server (port 4004) serves the built static site for production visitors.
mini-qr 8089 QR code PNG generator excalidraw-changemaker 8090 Collaborative whiteboard vaultwarden-changemaker 8445 Vaultwarden \u2014 Bitwarden-compatible password manager vaultwarden-init \u2014 Init container \u2014 configures admin settings homepage-changemaker 3010 Homepage \u2014 service dashboard docker compose up -d mini-qr excalidraw vaultwarden homepage\n Mini QR is used internally by walk sheets and cut export pages to generate printable QR codes.
"},{"location":"docs/getting-started/services/#monitoring-docker-profile","title":"Monitoring (Docker Profile)","text":"Monitoring services are behind a Docker Compose profile and are not started by default.
Container Port Descriptionprometheus-changemaker 9090 Prometheus \u2014 metrics collection grafana-changemaker 3005 Grafana \u2014 monitoring dashboards alertmanager-changemaker 9093 Alertmanager \u2014 alert routing cadvisor-changemaker 8086 cAdvisor \u2014 container metrics node-exporter-changemaker 9100 Node Exporter \u2014 host system metrics redis-exporter-changemaker 9121 Redis Exporter \u2014 Redis metrics gotify-changemaker 8889 Gotify \u2014 push notifications # Start the entire monitoring stack\ndocker compose --profile monitoring up -d\n The monitoring stack includes 3 pre-configured Grafana dashboards and 12 custom cm_* Prometheus metrics. See Monitoring for details.
newt \u2014 Pangolin tunnel connector (Newt) docker-socket-proxy \u2014 Docker socket proxy for secure container access # Newt starts automatically if PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET are set\ndocker compose up -d newt\n The Newt container connects to a Pangolin tunnel server for secure public access without opening inbound ports. See Tunnel for setup.
"},{"location":"docs/getting-started/services/#subdomain-routing","title":"Subdomain Routing","text":"When Nginx is running, services are accessible via subdomains. The root domain serves documentation only; all application routes are at app.DOMAIN.
app.DOMAIN Admin (3000) All application routes (admin, public pages, campaigns, map, shifts, media gallery) api.DOMAIN Express API (4000) REST API media.DOMAIN Fastify Media API (4100) Media API DOMAIN MkDocs Static (4004) Documentation / marketing site db.DOMAIN NocoDB (8091) Database browser docs.DOMAIN MkDocs Live (4003) Live documentation preview code.DOMAIN Code Server (8888) Web IDE n8n.DOMAIN n8n (5678) Workflow automation git.DOMAIN Gitea (3030) Git hosting home.DOMAIN Homepage (3010) Service dashboard grafana.DOMAIN Grafana (3005) Metrics visualization listmonk.DOMAIN Listmonk (9001) Newsletter platform qr.DOMAIN Mini QR (8089) QR code generator draw.DOMAIN Excalidraw (8090) Collaborative whiteboard vault.DOMAIN Vaultwarden (8445) Password manager events.DOMAIN Gancio (8092) Event platform chat.DOMAIN Rocket.Chat (8891) Team chat meet.DOMAIN Jitsi Meet (8893) Video conferencing mail.DOMAIN MailHog (8025) Email capture (dev)"},{"location":"docs/getting-started/services/#init-containers","title":"Init Containers","text":"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.
Init Container Purposelistmonk-init Creates the Listmonk API user for platform integration gancio-init Creates the Gancio database in the shared PostgreSQL instance vaultwarden-init Configures Vaultwarden admin settings nocodb-init Registers the main database with NocoDB for browsing Seeing these containers in a \"stopped\" or \"exited (0)\" state is completely normal.
"},{"location":"docs/getting-started/services/#starting-everything","title":"Starting Everything","text":"To start all services at once (excluding monitoring):
docker compose up -d\n To start everything including monitoring:
docker compose up -d && docker compose --profile monitoring up -d\n To see what's running:
docker compose ps\n Warning
Starting all services at once requires at least 4 GB RAM. For resource-constrained environments, start only the services you need.
"},{"location":"docs/getting-started/services/#next-steps","title":"Next Steps","text":".env referenceChangemaker 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.
There are two ways to upgrade:
./scripts/upgrade.sh directly from the command lineBoth methods execute the same 6-phase upgrade process.
"},{"location":"docs/getting-started/upgrades/#prerequisites","title":"Prerequisites","text":""},{"location":"docs/getting-started/upgrades/#upgrade-watcher-required-for-gui-method","title":"Upgrade Watcher (Required for GUI Method)","text":"The admin GUI triggers upgrades via a systemd path watcher that monitors for trigger files. This must be installed on the host system.
Install during initial setup:
The config.sh wizard offers to install the watcher automatically (Step 13). If you skipped it, install manually:
# Edit the systemd units to set your project path and user\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" scripts/systemd/changemaker-upgrade.path > /tmp/changemaker-upgrade.path\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" -e \"s|__USER__|$(whoami)|g\" scripts/systemd/changemaker-upgrade.service > /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 Verify it's running:
sudo systemctl status changemaker-upgrade.path\n How the watcher works
The API container writes a trigger.json file to a shared data/upgrade/ volume. The systemd path watcher detects the file and runs scripts/upgrade-watcher.sh 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.
/app/settings)The system fetches from the git remote and shows:
The GUI polls for progress updates and displays the current phase, percentage, and status message in real time.
"},{"location":"docs/getting-started/upgrades/#the-6-upgrade-phases","title":"The 6 Upgrade Phases","text":"Both the GUI and CLI methods execute the same 6-phase process:
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 Runsscripts/backup.sh (pg_dump + archive), backs up user-modifiable content, saves pre-upgrade commit hash 3 30% Code Update Saves user paths, stashes local changes, git pull, pops stash with auto-conflict resolution, detects new .env variables 4 50% Container Rebuild Rebuilds api, admin, media-api; conditionally rebuilds nginx and code-server 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"},{"location":"docs/getting-started/upgrades/#what-gets-preserved","title":"What Gets Preserved","text":"The upgrade script automatically preserves user-modifiable paths that you may have customized:
Path What It Containsmkdocs/docs/ Your documentation content mkdocs/mkdocs.yml MkDocs configuration mkdocs/site/ Built documentation site configs/ Prometheus, Grafana, Alertmanager, Homepage configs nginx/conf.d/services.conf Custom nginx service proxies These files are saved before git pull and unconditionally restored afterward, even if the pull introduces changes to them. Your versions always win.
Tip
The .env file is never touched by git pull (it's in .gitignore). However, if new environment variables are added in .env.example, the upgrade script automatically appends them to your .env with their default values and warns you to review them.
Run the upgrade script directly:
./scripts/upgrade.sh\n"},{"location":"docs/getting-started/upgrades/#options","title":"Options","text":"Flag Description --skip-backup Skip the backup phase (requires --force) --pull-services Also pull new third-party Docker images --dry-run Show what would happen without executing --force Continue past non-critical warnings --branch BRANCH Git branch to pull (default: current branch) --rollback Rollback to pre-upgrade commit --api-mode Write progress/result JSON for admin GUI (used internally)"},{"location":"docs/getting-started/upgrades/#examples","title":"Examples","text":"# 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# Rollback to the last pre-upgrade state\n./scripts/upgrade.sh --rollback\n"},{"location":"docs/getting-started/upgrades/#rollback","title":"Rollback","text":""},{"location":"docs/getting-started/upgrades/#automatic-rollback","title":"Automatic Rollback","text":"If the upgrade fails at any phase, the script prints detailed rollback instructions including the pre-upgrade commit hash. Use the --rollback flag:
./scripts/upgrade.sh --rollback\n This:
git-commit.txt inside the archiveWarning
--rollback 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.
# 1. Restore code\ncd /path/to/changemaker.lite\ngit checkout <pre-upgrade-commit-hash>\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/<backup>.tar.gz -C /tmp\ngunzip -c /tmp/<backup>/v2-postgres.sql.gz | \\\n docker exec -i changemaker-v2-postgres psql -U changemaker -d changemaker_v2\n"},{"location":"docs/getting-started/upgrades/#new-environment-variables","title":"New Environment Variables","text":"When upstream code adds new environment variables to .env.example, the upgrade script automatically:
.env.example against your .env[WARN] New env vars added to .env (review defaults):\n NEW_FEATURE_FLAG\n NEW_API_KEY\n Always review new variables after an upgrade \u2014 some may need manual configuration.
"},{"location":"docs/getting-started/upgrades/#update-checker","title":"Update Checker","text":"A separate lightweight script checks for available updates without performing any changes:
./scripts/upgrade-check.sh\n This writes data/upgrade/status.json with:
The admin GUI reads this file to display update availability.
"},{"location":"docs/getting-started/upgrades/#troubleshooting","title":"Troubleshooting","text":""},{"location":"docs/getting-started/upgrades/#stale-progress-indicator","title":"Stale Progress Indicator","text":"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.
To manually clear:
rm -f data/upgrade/progress.json\n"},{"location":"docs/getting-started/upgrades/#merge-conflicts","title":"Merge Conflicts","text":"If git pull 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.
The upgrade script uses .upgrade.lock to prevent concurrent upgrades. If a previous upgrade crashed without cleaning up:
# Verify no upgrade is actually running\nps aux | grep upgrade.sh\n\n# Remove stale lock\nrm -f .upgrade.lock\n"},{"location":"docs/getting-started/upgrades/#health-check-failures","title":"Health Check Failures","text":"If Phase 6 health checks fail, services may still be starting. Wait 1-2 minutes and check manually:
# 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"},{"location":"docs/getting-started/upgrades/#systemd-watcher-not-triggering","title":"Systemd Watcher Not Triggering","text":"# 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"},{"location":"docs/services/","title":"Services","text":"Changemaker Lite orchestrates 20+ services via Docker Compose. This page is your map to every service: what it does, how to reach it, and where to find its upstream documentation.
"},{"location":"docs/services/#core-platform","title":"Core Platform","text":"The essential services that power the application.
Express API
Main V2 API server. Handles authentication, campaigns, map, shifts, pages, email, and all business logic. Prisma ORM with PostgreSQL.
Port: 4000 \u00b7 Container: changemaker-v2-api
API Reference
Fastify Media API
Video library server. Upload, metadata extraction (FFprobe), analytics, scheduled publishing, and public gallery. Shares the same PostgreSQL database.
Port: 4100 \u00b7 Container: changemaker-media-api
Media Guide
Admin GUI
React single-page application (Vite + Ant Design + Zustand). Serves the admin dashboard, public campaign pages, volunteer portal, and media gallery \u2014 all from one build.
Port: 3000 \u00b7 Container: changemaker-v2-admin
Feature Guides
PostgreSQL 16
Primary database shared by both APIs. Managed by Prisma migrations. Contains 30+ tables covering users, campaigns, locations, shifts, media, and more.
Port: 5433 (host) / 5432 (container) \u00b7 Container: changemaker-v2-postgres
PostgreSQL Docs
Redis
In-memory store for rate limiting, BullMQ job queues (email, video scheduling), geocoding cache, and session data. Requires authentication.
Port: 6379 \u00b7 Container: redis-changemaker
Redis Docs
Nginx
Reverse proxy handling all subdomain routing (app., api., media., docs., etc.). Includes security headers (HSTS, CSP, Permissions-Policy) and WebSocket support.
Port: 80 / 443 \u00b7 Container: changemaker-v2-nginx
Nginx Docs
Listmonk
Self-hosted newsletter and mailing list manager. Drop-in replacement for Mailchimp. Opt-in sync with the main platform imports participants, locations, and users as subscriber lists.
Port: 9001 \u00b7 Container: listmonk-app \u00b7 Subdomain: listmonk.DOMAIN
Listmonk Docs
MailHog
Email capture for development. All outgoing email is intercepted and displayed in a web UI when EMAIL_TEST_MODE=true. No real emails are sent.
Port: 8025 (web) / 1025 (SMTP) \u00b7 Container: mailhog-changemaker \u00b7 Subdomain: mail.DOMAIN
MailHog GitHub
MkDocs
Material-themed documentation site with full-text search, blog, social cards, and Jinja2 template overrides. Two containers: live preview (dev) and static site (production).
Port: 4003 (dev) / 4004 (static) \u00b7 Container: mkdocs-changemaker \u00b7 Subdomain: docs.DOMAIN
MkDocs Material
Code Server
Full VS Code in the browser. Edit configuration files, templates, and documentation from anywhere without SSH. Supports extensions.
Port: 8888 \u00b7 Container: code-server-changemaker \u00b7 Subdomain: code.DOMAIN
Code Server Docs
NocoDB
Airtable-alternative database browser. Provides a spreadsheet-like interface to browse, filter, sort, and export campaign data. Read-only access to the main database.
Port: 8091 \u00b7 Container: changemaker-v2-nocodb \u00b7 Subdomain: db.DOMAIN
NocoDB Docs
n8n
Visual workflow automation platform. Connect APIs, trigger actions on events, schedule tasks, and build custom integrations \u2014 all without code. 400+ built-in integrations.
Port: 5678 \u00b7 Container: n8n-changemaker \u00b7 Subdomain: n8n.DOMAIN
n8n Docs
Gitea
Self-hosted Git repository hosting. Version control for campaign code, configuration, templates, and documentation. Includes issues, pull requests, and CI/CD.
Port: 3030 (web) / 2222 (SSH) \u00b7 Container: gitea-changemaker \u00b7 Subdomain: git.DOMAIN
Gitea Docs
Mini QR
Lightweight QR code generator. Produces PNG images for walk sheets, campaign materials, and event signage. Embedded in the admin dashboard via iframe.
Port: 8089 \u00b7 Container: mini-qr \u00b7 Subdomain: qr.DOMAIN
Homepage
Service dashboard showing the status of all containers at a glance. Auto-generated services.yaml from config.sh provides both production and local links.
Port: 3010 \u00b7 Container: homepage-changemaker \u00b7 Subdomain: home.DOMAIN
Homepage Docs
Excalidraw
Collaborative whiteboard for brainstorming, diagramming, and visual planning. Real-time collaboration via WebSocket.
Port: 8090 \u00b7 Container: excalidraw-changemaker \u00b7 Subdomain: draw.DOMAIN
Excalidraw
Vaultwarden
Self-hosted Bitwarden-compatible password manager. Secure credential sharing for campaign teams. Requires HTTPS for account creation; local browsing works on HTTP.
Port: 8445 \u00b7 Container: vaultwarden-changemaker \u00b7 Subdomain: vault.DOMAIN
Vaultwarden Wiki
Rocket.Chat
Self-hosted team chat for volunteer coordination. Supports channels, direct messaging, threads, and file sharing. Embeddable in the admin dashboard via iframe. Enable with ENABLE_CHAT=true.
Port: 3000 (internal) \u00b7 Container: rocketchat-changemaker \u00b7 Subdomain: chat.DOMAIN
Rocket.Chat Docs
Jitsi Meet
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 ENABLE_MEET=true.
Containers: jitsi-web, jitsi-prosody, jitsi-jicofo, jitsi-jvb \u00b7 Subdomain: meet.DOMAIN
Setup Guide \u00b7 Jitsi Docs
Gancio
Self-hosted event management platform. Automatic shift-to-event sync (when GANCIO_SYNC_ENABLED=true) publishes shifts as public events. Uses the shared PostgreSQL database. Embeddable calendar widget available for MkDocs pages.
Port: 8092 \u00b7 Container: gancio-changemaker \u00b7 Subdomain: events.DOMAIN
Gancio Docs
Pangolin + Newt
Self-hosted tunnel server with the Newt client container. Exposes your services to the internet without port forwarding. Handles SSL/TLS, works behind CGNAT and double NAT.
Container: newt-changemaker \u00b7 Managed from Admin \u2192 Settings \u2192 Tunnel
Deployment Guide \u00b7 Pangolin GitHub
These services run behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Prometheus
Metrics collection and time-series database. Scrapes 12 custom cm_* application metrics plus container, host, and Redis metrics. Pre-configured alert rules.
Port: 9090 \u00b7 Container: prometheus-changemaker
Prometheus Docs
Grafana
Metrics visualization with 3 auto-provisioned dashboards: API Overview, Infrastructure, and Campaign Activity. Supports custom dashboards and alerting.
Port: 3005 \u00b7 Container: grafana-changemaker \u00b7 Subdomain: grafana.DOMAIN
Grafana Docs
Alertmanager
Alert routing and notification delivery. Receives alerts from Prometheus and dispatches to Gotify, email, or webhooks based on configurable rules.
Port: 9093 \u00b7 Container: alertmanager-changemaker
Alertmanager Docs
cAdvisor
Container resource metrics. Exposes CPU, memory, network, and filesystem usage per container for Prometheus to scrape.
Port: 8086 \u00b7 Container: cadvisor-changemaker
cAdvisor GitHub
Node Exporter
Host system metrics. Reports CPU, memory, disk, and network stats for the underlying server.
Port: 9100 \u00b7 Container: node-exporter-changemaker
Node Exporter
Redis Exporter
Redis metrics for Prometheus. Exposes connection counts, memory usage, command stats, and keyspace info.
Port: 9121 \u00b7 Container: redis-exporter-changemaker
Redis Exporter GitHub
Gotify
Self-hosted push notification server. Receives alerts from Alertmanager and delivers them to mobile/desktop clients.
Port: 8889 \u00b7 Container: gotify-changemaker
Gotify Docs
All services at a glance with their default ports and subdomains.
Service Port Subdomain Docker Profile Express API 4000api. default Media API 4100 media. default Admin GUI 3000 app. default PostgreSQL 5433 \u2014 default Redis 6379 \u2014 default Nginx 80/443 (all) default Listmonk 9001 listmonk. default MailHog 8025 mail. default MkDocs (dev) 4003 docs. default MkDocs (static) 4004 (root) default Code Server 8888 code. default NocoDB 8091 db. default n8n 5678 n8n. default Gitea 3030 git. default Mini QR 8089 qr. default Homepage 3010 home. default Excalidraw 8090 draw. default Vaultwarden 8445 vault. default Rocket.Chat \u2014 chat. default Jitsi Meet \u2014 meet. default Gancio 8092 events. default Newt (tunnel) \u2014 \u2014 default Prometheus 9090 \u2014 monitoring Grafana 3005 grafana. monitoring Alertmanager 9093 \u2014 monitoring cAdvisor 8086 \u2014 monitoring Node Exporter 9100 \u2014 monitoring Redis Exporter 9121 \u2014 monitoring Gotify 8889 \u2014 monitoring Starting services selectively
You don't need to run everything. Start only what you need:
# Core only\ndocker compose up -d v2-postgres redis api admin\n\n# Add nginx for subdomain routing\ndocker compose up -d nginx\n\n# Add monitoring\ndocker compose --profile monitoring up -d\n See Getting Started for the recommended startup order.
"},{"location":"docs/troubleshooting/","title":"Troubleshooting","text":"Common issues and their solutions when running Changemaker Lite.
Under Construction
This troubleshooting guide is being expanded. Check back soon for more solutions.
"},{"location":"docs/troubleshooting/#cors-errors-in-production","title":"CORS Errors in Production","text":"Symptom: Browser console shows CORS errors when accessing production domain.
Fix: Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000\n Then restart the API: docker compose restart api
Symptom: All API endpoints return 302 redirects to Pangolin auth page.
Fix: In the Pangolin dashboard, set each resource to \"Not Protected\" (public access).
"},{"location":"docs/troubleshooting/#database-connection-failures","title":"Database Connection Failures","text":"docker compose ps v2-postgresDATABASE_URL in .envdocker compose logs v2-postgres --tail 50docker compose ps redis-changemakerREDIS_PASSWORD and REDIS_URL format in .envdocker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD pingdocker compose logs api --tail 100.env.example)docker compose exec api npx prisma migrate deployThis guide covers everything you can do as a visitor or registered supporter \u2014 from contacting representatives to signing up for volunteer shifts.
"},{"location":"docs/user-guide/#what-you-can-do","title":"What You Can Do","text":"Campaigns
Find active advocacy campaigns, look up your representatives, and send emails.
Map
Explore the interactive map showing locations across your community.
Shifts
Sign up for volunteer shifts and join canvassing teams.
Events
Browse upcoming events and RSVP through the event calendar.
Gallery
Watch campaign videos, browse photos, and explore playlists.
Shop & Pricing
Purchase campaign merchandise or subscribe to a membership plan.
Donations
Support the cause with one-time donations on branded pages.
Your Profile
View and manage your contact profile, preferences, and activity history.
Browse active advocacy campaigns and contact your elected representatives.
"},{"location":"docs/user-guide/campaigns/#how-it-works","title":"How It Works","text":"/campaigns \u2014 see active campaigns with descriptions and email countsEach 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.
"},{"location":"docs/user-guide/campaigns/#submit-your-own-campaign","title":"Submit Your Own Campaign","text":"Registered users can draft and submit their own advocacy campaigns at /campaigns/create. Submissions go through admin review before being published.
/campaigns \u2014 browse active campaigns/campaign/:slug \u2014 take action on a specific campaign/campaign/:slug/responses \u2014 view the response wall/campaigns/create \u2014 submit a user-generated campaign (requires login)/campaigns/mine \u2014 manage your submitted campaigns (requires login)Make one-time contributions on branded donation pages.
"},{"location":"docs/user-guide/donations/#how-it-works","title":"How It Works","text":"/donateEach donation page has:
/donate \u2014 browse donation pages/donate/:slug \u2014 donate on a specific campaign pageIntegrated with Gancio for self-hosted event management. When enabled, volunteer shifts are automatically published as public events.
"},{"location":"docs/user-guide/events/#shift-to-event-sync","title":"Shift-to-Event Sync","text":"When GANCIO_SYNC_ENABLED=true, the platform:
Sync uses OAuth authentication with the Gancio admin account.
"},{"location":"docs/user-guide/events/#key-features","title":"Key Features","text":"enableEvents is enabled in settings/app/gancio \u2014 Gancio service status and iframe embed/events \u2014 public events navigation link (when enabled)events.DOMAIN \u2014 Gancio web interface for browsing and RSVPsThe public gallery at /gallery showcases campaign videos, photos, and curated playlists.
/gallery/shorts/gallery/playlist/:id/gallery \u2014 public video and photo gallery/gallery/watch/:id \u2014 watch a specific video/gallery/playlist/:id \u2014 view a playlist/gallery/shorts \u2014 browse the shorts feedThe public map at /map shows locations across your community on an interactive Leaflet map.
/map \u2014 public interactive mapGive supporters a private, token-based link to view and manage their own contact profile -- no login required.
"},{"location":"docs/user-guide/profile/#how-it-works","title":"How It Works","text":"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.
"},{"location":"docs/user-guide/profile/#public-routes","title":"Public Routes","text":"/profile/:token -- self-service contact profile pageBrowse available volunteer shifts and sign up to participate in canvassing and other campaign activities.
"},{"location":"docs/user-guide/shifts/#signing-up","title":"Signing Up","text":"/shifts to see available time slotsOrganizers may share a QR code at events for instant onboarding:
Quick Join creates a temporary 24-hour account. Your organizer can upgrade it to a permanent account afterward.
"},{"location":"docs/user-guide/shifts/#after-signing-up","title":"After Signing Up","text":"Once you have an account, log in to access the Volunteer Portal where you can:
See the Volunteer Guide for the full volunteer experience.
"},{"location":"docs/user-guide/shifts/#public-routes","title":"Public Routes","text":"/shifts \u2014 browse and sign up for volunteer shifts/join?token=... \u2014 quick join via invite link or QR codeSupport the campaign by purchasing merchandise or subscribing to a membership plan.
"},{"location":"docs/user-guide/shop/#shop","title":"Shop","text":"Browse available products at /shop:
View subscription options at /pricing:
/shop \u2014 browse products/pricing \u2014 view subscription plansWelcome! This guide walks you through everything you need as a campaign volunteer.
"},{"location":"docs/volunteer/#getting-started","title":"Getting Started","text":""},{"location":"docs/volunteer/#1-sign-up-for-a-shift","title":"1. Sign Up for a Shift","text":"Visit the Shifts page (your organizer will share the link, or find it at /shifts). Browse available time slots, pick one that works, and fill in your name and email. You'll receive a confirmation email with login credentials.
Sign in with the email and password from your confirmation at the login page.
"},{"location":"docs/volunteer/#3-explore-the-volunteer-portal","title":"3. Explore the Volunteer Portal","text":"After logging in, you'll land on the volunteer portal. Use the bottom navigation to access:
Tap your name/avatar in the header and select Browse Site to visit the public pages \u2014 campaigns, the public map, and shift signups.
"},{"location":"docs/volunteer/#faq","title":"FAQ","text":"Q: I can't find my assigned area on the map. A: Make sure your shift has an area assigned. Check with your organizer.
Q: My GPS isn't working. A: Allow location access in your browser. Try moving near a window for better signal.
Q: I recorded the wrong outcome. A: Visit the same address again and record the correct outcome. The most recent visit counts.
Q: How do I sign up for more shifts? A: Visit the public shifts page at /shifts.
Recognize volunteer contributions with unlockable achievement badges and competitive leaderboards.
"},{"location":"docs/volunteer/achievements/#how-it-works","title":"How It Works","text":"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.
"},{"location":"docs/volunteer/achievements/#badge-categories","title":"Badge Categories","text":""},{"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"},{"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"},{"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"},{"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"},{"location":"docs/volunteer/achievements/#progress-tracking","title":"Progress Tracking","text":"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).
"},{"location":"docs/volunteer/achievements/#leaderboards","title":"Leaderboards","text":"The Achievements page includes a leaderboard tab with three ranking types:
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.
"},{"location":"docs/volunteer/achievements/#volunteer-stats","title":"Volunteer Stats","text":"The Achievements page also displays aggregate stats for the current user:
/volunteer/achievements \u2014 badge gallery, progress bars, leaderboard tabs, and personal statsThe volunteer canvass map is your main tool for door-to-door outreach \u2014 a full-screen GPS-tracked experience.
"},{"location":"docs/volunteer/canvassing/#the-volunteer-map","title":"The Volunteer Map","text":""},{"location":"docs/volunteer/canvassing/#what-you-see","title":"What You See","text":"The Routes tab shows your past canvassing routes on a map, helping you see which areas you've covered and plan your next outing.
"},{"location":"docs/volunteer/canvassing/#volunteer-routes","title":"Volunteer Routes","text":"/volunteer \u2014 full-screen canvass map with GPS and visit recording/volunteer/activity \u2014 visit history and outcome breakdown/volunteer/routes \u2014 past canvassing routesView your upcoming and past volunteer shifts from the Shifts tab in the bottom navigation.
"},{"location":"docs/volunteer/shifts/#shift-details","title":"Shift Details","text":"Each shift shows:
The Activity tab shows your complete visit history:
/volunteer/shifts \u2014 view assigned shifts/volunteer/activity \u2014 visit history and outcome breakdownConnect with fellow volunteers through friend requests, activity feeds, team groups, and real-time notifications. Enable via Settings > Feature Toggles > Social Connections.
"},{"location":"docs/volunteer/social/#friends","title":"Friends","text":"The Discover page suggests potential friends using a ranked scoring algorithm based on:
The Social Feed at /volunteer/feed shows recent activity from your friends:
Groups are automatically created based on platform activity:
Each volunteer has a social profile showing volunteer stats, achievement badges, friendship status, and recent activity.
"},{"location":"docs/volunteer/social/#pokes","title":"Pokes","text":"Send a friendly nudge to any accepted friend (24-hour cooldown per pair).
"},{"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"},{"location":"docs/volunteer/social/#digest-emails","title":"Digest Emails","text":"Opt into periodic social digest emails with friend activity, unread notifications, and pending requests. Choose daily or weekly frequency.
"},{"location":"docs/volunteer/social/#volunteer-routes","title":"Volunteer Routes","text":"/volunteer/feed \u2014 social activity feed/volunteer/friends \u2014 friends, requests, blocked, and groups/volunteer/discover \u2014 ranked friend suggestions/volunteer/profile \u2014 your social profile/volunteer/profile/:userId \u2014 another volunteer's profile/volunteer/notifications \u2014 notification center and preferences/volunteer/groups/:id \u2014 group detail with member listTesting page.
Loading... Choose Your PlanGet access to exclusive content and features.
View Plans PNG 1920\u00d71040 vlcsnap-2026-01-09-15h39m52s898.png 0 views View \u2192 \ud83d\uded2 DIGITAL Test Product 1A test product
$90.00
Buy Now \u2014 $90.00Secure payment via Stripe. Browse all products
View in Shop\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Make my donation anonymous DonateSecure payment via Stripe. Open full donate page
Donate Now\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Amount\ud83d\uded2
Browse Our ProductsReports, toolkits, event tickets, and more.
Shop Now Choose Your PlanGet access to exclusive content and features.
View Plans\u2764\ufe0f
Support Our CauseYour contribution helps us create lasting change in our community.
Donate Now 9:54 Testing This Sucker 0 views Watch \u2192"},{"location":"test/#test_1","title":"Test","text":"Testing page.
Loading... PNG 1920\u00d71040 vlcsnap-2026-01-09-15h39m52s898.png 0 views View \u2192 \ud83d\uded2 DIGITAL Test Product 1A test product
$90.00
Buy Now \u2014 $90.00Secure payment via Stripe. Browse all products
View in Shop\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Make my donation anonymous DonateSecure payment via Stripe. Open full donate page
Donate Now\u2764\ufe0f
Support Our WorkEvery contribution makes a difference. Choose an amount below.
$10 $25 $50 $100 Custom Amount\ud83d\uded2
Browse Our ProductsReports, toolkits, event tickets, and more.
Shop Now Choose Your PlanGet access to exclusive content and features.
View Plans\u2764\ufe0f
Support Our CauseYour contribution helps us create lasting change in our community.
Donate Now 9:54 Testing This Sucker 0 views Watch \u2192"},{"location":"testing/","title":"testing","text":""},{"location":"blog/","title":"Blog","text":""},{"location":"comments/callback/","title":"Signing in...","text":"Completing sign in...
You will be redirected back to the page you were on.
"},{"location":"docs/","title":"Documentation","text":"Welcome to the Changemaker Lite documentation. Whether you're a campaign volunteer, an admin managing operations, or a sysadmin deploying the platform \u2014 start here.
"},{"location":"docs/#use-the-platform","title":"Use the Platform","text":"Getting Started
Install Changemaker Lite, create your first admin account, and explore the dashboard.
Getting Started
Feature Guides
Campaigns, email advocacy, response walls, map locations, landing pages, and media.
Feature Guides
Administration
User management, roles and permissions, site settings, email templates, and newsletters.
Administration
Volunteer Guide
Sign up for shifts, use the canvassing map, record visits, and track your activity.
Volunteer Guide
Deployment
Docker Compose setup, environment variables, SSL/TLS, backups, and production checklist.
Deployment
Architecture
Dual API design, database schema, authentication flow, and system diagram.
Architecture
Services
Nginx routing, Redis, PostgreSQL, Listmonk, MkDocs, Gitea, NocoDB, and more.
Services
Monitoring
Prometheus metrics, Grafana dashboards, Alertmanager rules, and health checks.
Monitoring Coming soon
API Reference
REST endpoints for auth, campaigns, locations, shifts, media, and more.
API Reference
Troubleshooting
Common errors, CORS issues, database problems, tunnel debugging, and FAQ.
Troubleshooting
Security
Password policy, rate limiting, token rotation, encryption, and audit report.
Security See Deployment
Contributing
Development setup, code style, git workflow, and pull request guidelines.
Contributing Coming soon
New here?
Start with the Getting Started guide to have the platform running in under 30 minutes.
Looking for the source?
Changemaker Lite is 100% open source. Browse the code on Gitea.
"},{"location":"docs/phil/","title":"Philosophy","text":""},{"location":"docs/phil/#software-is-political","title":"Software Is Political","text":"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.
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.
Changemaker Lite exists because we believe organizational independence requires technological independence.
"},{"location":"docs/phil/#the-extractive-model","title":"The Extractive Model","text":"Most campaign and political software is extractive by design. The pattern is familiar:
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.
Every subscription to corporate software funds the machine you're fighting.
"},{"location":"docs/phil/#the-alternative-grow-power-dont-rent-it","title":"The Alternative: Grow Power, Don't Rent It","text":"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?\"
Growing change means:
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.
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.
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.
Workers, with the right tools, will build the future.
"},{"location":"docs/phil/#de-corp-your-stack","title":"De-Corp Your Stack","text":"The practical work of digital sovereignty starts with replacing corporate services one at a time:
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 canvassingThe 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.
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.
"},{"location":"docs/phil/#security-culture-starts-with-infrastructure","title":"Security Culture Starts With Infrastructure","text":"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.
Key principles:
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?
"},{"location":"docs/phil/#our-principles","title":"Our Principles","text":"Liberation First Technology should center marginalized voices. The tools we build reflect the values we hold, and they shape the movements that use them.
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.
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.
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.
"},{"location":"docs/phil/#further-reading","title":"Further Reading","text":"These articles explore the ideas behind Changemaker Lite in depth:
The admin panel at /app 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.
Dashboard
Live overview of platform activity, upcoming shifts, email stats, and service health.
People & Access
User management, roles, the People CRM, and contact merging.
Advocacy
Campaigns, response moderation, representative lookup, and email queue monitoring.
Broadcast
Newsletter sync, email templates, and SMS campaigns.
Web Content
Landing pages, homepage, navigation menu, and documentation management.
Map & Canvassing
Locations, areas, shifts, canvassing dashboard, data quality, and map settings.
Media
Video/photo library, analytics, playlists, comment moderation, and gallery ads.
Payments
Products, donations, subscription plans, and Stripe configuration.
Services
Tunnel management, monitoring, and third-party integrations.
Settings
Organization branding, theme colors, email config, feature toggles, and notifications.
SUPER_ADMIN Full platform access INFLUENCE_ADMIN Campaigns, responses, email queue MAP_ADMIN Locations, areas, shifts, canvassing USER Volunteer portal only TEMP Limited volunteer access (auto-created)"},{"location":"docs/admin/dashboard/","title":"Dashboard","text":"The admin dashboard (/app) provides a real-time overview of platform activity.
All cards auto-refresh and gracefully degrade when their associated module is disabled.
"},{"location":"docs/admin/people-access/","title":"People & Access","text":"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.
"},{"location":"docs/admin/people-access/#user-management","title":"User Management","text":""},{"location":"docs/admin/people-access/#creating-users","title":"Creating Users","text":"Navigate to Users (/app/users) and click Add User. Fill in name, email, and role. The user will receive a welcome email with login instructions.
SUPER_ADMIN Full platform access Campaign managers INFLUENCE_ADMIN Campaigns, responses, email queue Advocacy coordinators MAP_ADMIN Locations, areas, shifts, canvassing Field organizers USER Volunteer portal only Active volunteers TEMP Limited volunteer access Shift signups (auto-created)"},{"location":"docs/admin/people-access/#password-policy","title":"Password Policy","text":"Passwords must be at least 12 characters with uppercase, lowercase, and a digit. This is enforced at the API schema level.
"},{"location":"docs/admin/people-access/#deactivating-users","title":"Deactivating Users","text":"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.
"},{"location":"docs/admin/people-access/#service-accounts-panel","title":"Service Accounts Panel","text":"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.
"},{"location":"docs/admin/people-access/#people-crm","title":"People CRM","text":"Enable with enablePeople in Settings. The People module serves as the platform's CRM, aggregating data from all other modules into a unified view.
The People page does not store a separate \"people\" table. Instead, it aggregates records in real time from seven data sources:
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 CRMRecords are deduplicated by normalized email or phone number, with Users taking highest priority.
"},{"location":"docs/admin/people-access/#managed-contacts","title":"Managed Contacts","text":"Any virtual person can be \"promoted\" to a managed Contact record. This creates a persistent Contact entity in the database with:
Each managed contact supports multiple structured data entries:
View a chronological timeline of all interactions for a person, across every module:
The platform identifies potential duplicates by matching normalized email addresses and phone numbers across sources. The merge workflow lets you:
Build a relationship graph between contacts using typed connections:
Generate shareable public profile pages for contacts:
/profile/:tokenThe Household panel groups contacts who share the same physical address, making it easy to see all members of a household and their combined engagement.
"},{"location":"docs/admin/people-access/#create-user-from-contact","title":"Create User from Contact","text":"Promote a CRM contact to a full platform user account directly from the People interface, with role assignment and optional welcome email.
"},{"location":"docs/admin/people-access/#admin-routes","title":"Admin Routes","text":"/app/users \u2014 user CRUD, role assignment, service accounts/app/people \u2014 contact list with search, filters, source/tag filtering, and bulk actionsCentralized configuration for organization identity, theming, email delivery, feature modules, and automated notifications.
"},{"location":"docs/admin/settings/#settings-tabs","title":"Settings Tabs","text":""},{"location":"docs/admin/settings/#organization","title":"Organization","text":"Configure your organization's public identity:
Customize the look of admin and public interfaces:
Admin theme:
Public theme:
A live preview panel shows color swatches and a gradient preview as you configure.
"},{"location":"docs/admin/settings/#email","title":"Email","text":"Configure how the platform sends emails:
A configuration summary card at the top displays the current provider, server, authentication status, and test mode state.
"},{"location":"docs/admin/settings/#feature-toggles","title":"Feature Toggles","text":"Enable or disable platform modules. Disabling a module hides it from navigation but does not delete data.
Category Flag Description Core PlatformenableInfluence Advocacy campaigns, email sending, response wall enableMap Map, locations, canvassing, volunteer shifts enableNewsletter Listmonk newsletter sync enableLandingPages GrapesJS landing page builder Media & Content enableMediaFeatures Video library, public gallery, analytics enableGalleryAds Promotional cards in the video gallery enableEvents Gancio event calendar integration Communication enableChat Rocket.Chat team coordination enableMeet Jitsi video meetings (integrates with Rocket.Chat) enableSms Termux Android SMS campaigns People & Engagement enablePeople Unified contacts CRM enableSocial Volunteer social connections and activity feeds autoSyncPeopleToMap Auto-create map locations from contact addresses Commerce enablePayments Stripe subscriptions, products, and donations"},{"location":"docs/admin/settings/#notifications","title":"Notifications","text":"Control which automated email notifications the platform sends. Disabling a notification stops future emails but does not affect already-queued jobs.
Admin alerts:
Volunteer emails:
Re-engagement:
/app/settings \u2014 multi-tab settings page (supports deep-linking to a specific tab via router state)The advocacy module helps supporters contact their elected representatives through email campaigns.
"},{"location":"docs/admin/advocacy/#in-this-section","title":"In This Section","text":"Help supporters contact their elected representatives through email campaigns.
"},{"location":"docs/admin/advocacy/campaigns/#how-it-works","title":"How It Works","text":"Registered (non-temporary) users can create their own advocacy campaigns and submit them for admin review.
/campaigns/create to draft a campaign through a guided wizard/campaigns/mine, including checking moderation status and editing campaigns that have been sent back for changesPENDING_REVIEW status and remain in DRAFT until an admin approves themPENDING_REVIEW or CHANGES_REQUESTED; editing automatically resets the status back to PENDING_REVIEWAdmins review user-submitted campaigns before they go live.
/app/campaign-moderation, showing all user-generated campaigns filtered by moderation status (pending, approved, rejected, changes requested)APPROVED and the campaign status to ACTIVE, making it publicly visibleREJECTED with an optional reason visible to the submitterCHANGES_REQUESTED with feedback, allowing the user to revise and resubmitThe Campaign Effectiveness dashboard provides cross-campaign performance analytics at /app/influence/effectiveness.
/app/campaigns \u2014 create, edit, and manage campaigns/app/campaign-moderation \u2014 review and moderate user-submitted campaigns/app/influence/effectiveness \u2014 campaign effectiveness analytics dashboard/app/responses \u2014 moderate submitted responses/app/email-queue \u2014 monitor outgoing email delivery/campaigns \u2014 browse active campaigns/campaigns/create \u2014 submit a new user-generated campaign (requires login)/campaigns/mine \u2014 view and manage your submitted campaigns (requires login)/campaign/:slug \u2014 take action on a specific campaign/campaign/:slug/responses \u2014 view the response wallMonitor outgoing advocacy emails processed through the BullMQ queue.
"},{"location":"docs/admin/advocacy/email-queue/#key-features","title":"Key Features","text":"/app/email-queue/app/email-queue \u2014 email queue monitoring and managementThe platform uses the Represent API to look up elected representatives by postal code across all government levels.
"},{"location":"docs/admin/advocacy/representatives/#how-it-works","title":"How It Works","text":"/app/representatives/app/representatives \u2014 representative cache management and lookup testingReview and moderate representative responses submitted by supporters on the public response wall.
"},{"location":"docs/admin/advocacy/responses/#key-features","title":"Key Features","text":"/app/responses with filtering by campaign and status/app/responses \u2014 response moderation dashboardReach supporters through multiple channels \u2014 email newsletters, templated campaigns, and SMS text messages.
"},{"location":"docs/admin/broadcast/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/broadcast/email-templates/#template-categories","title":"Template Categories","text":"Each template belongs to a category that determines where it can be used:
Templates use Handlebars-style {{VARIABLE_NAME}} placeholders that are replaced at send time. Variables must use uppercase letters and underscores (e.g., {{RECIPIENT_NAME}}, {{CAMPAIGN_TITLE}}).
{{#if VARIABLE}}...{{/if}} syntaxThe template validator automatically extracts all variables from the HTML, text, and subject line content and checks for unmatched conditional blocks.
"},{"location":"docs/admin/broadcast/email-templates/#version-history","title":"Version History","text":"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:
Before activating a template, send a test email to verify rendering:
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.
"},{"location":"docs/admin/broadcast/email-templates/#admin-routes","title":"Admin Routes","text":"/app/email-templates -- create and manage email templates with a visual editorIntegrated with Listmonk for opt-in mailing lists and newsletter campaigns. Enable with LISTMONK_SYNC_ENABLED=true.
The platform automatically creates and maintains 13 subscriber lists in Listmonk:
List Name Source Tags All Contacts All synced recordsv2 Campaign Participants Users who sent advocacy emails v2, influence Locations - All Address occupants with email v2, map Support Level 1-4 Addresses by canvass support level v2, map, support Has Campaign Sign Addresses with a yard sign v2, map, signs Users Active non-temp platform accounts v2, users Volunteers Shift signups v2, map, shifts Canvassers Users who completed canvass sessions v2, map, canvass Subscribers Active paid subscribers v2, payments Donors Users who completed a donation v2, payments"},{"location":"docs/admin/broadcast/newsletter/#bulk-sync","title":"Bulk Sync","text":"The admin panel provides a manual \"Sync All\" action that synchronizes four data sources to Listmonk:
Each source upserts subscribers (creates new or merges into existing), preserving existing list memberships and merging metadata attributes.
"},{"location":"docs/admin/broadcast/newsletter/#event-driven-sync","title":"Event-Driven Sync","text":"In addition to bulk sync, the platform fires real-time subscriber upserts on application events:
All event-driven syncs are fire-and-forget and silently fail if Listmonk is unreachable.
"},{"location":"docs/admin/broadcast/newsletter/#admin-routes","title":"Admin Routes","text":"/app/listmonk (sidebar: \"Newsletter\") -- sync status, subscriber counts, campaign stats, and manual sync triggerText 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.
Enable with ENABLE_SMS=true or via the setup wizard.
The SMS system uses a three-tier architecture where your server communicates with a lightweight Python Flask API running on an Android phone:
graph LR\n A[Admin Dashboard<br/>Campaign UI] -->|API calls| B[Express API<br/>BullMQ Queue]\n B -->|HTTP + API Key| C[Android Phone<br/>Flask on Termux]\n C -->|termux-sms-send| D[Android SMS]\n D -->|Carrier Network| E[Recipients]\n E -->|Reply SMS| D\n D -->|termux-sms-list| C\n C -->|HTTP response| B Why this approach?
Before starting setup, you'll need:
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)Both Apps MUST Come from F-Droid
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:
Termux:API is not yet available on Google Play
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.
"},{"location":"docs/admin/broadcast/sms/#phone-setup","title":"Phone Setup","text":""},{"location":"docs/admin/broadcast/sms/#step-1-install-apps-from-f-droid","title":"Step 1: Install Apps from F-Droid","text":"On your Android phone:
termux-api package
You need two things called \"termux-api\": the F-Droid app (Termux:API) and the Termux package (pkg install termux-api). The setup script installs the package automatically, but the F-Droid app must be installed manually.
Go to the admin dashboard SMS Setup page (/app/sms/setup) and click Generate API Key. Copy the key \u2014 you'll paste it into the setup command.
Open Termux on the phone and run:
# Clone the SMS server (first time only)\npkg install -y git && git clone https://gitea.bnkops.com/admin/campaign_connector.git ~/sms-server\n\n# Run the setup script \u2014 paste your API key at the end\nbash ~/sms-server/android/setup.sh YOUR_API_KEY_HERE\n The setup script automatically:
~/.bashrcWhen done, note the Phone URL displayed (e.g. http://100.64.0.5:5001).
After initial setup, install termux-services for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:
cd ~/sms-server && bash android/setup-services.sh\n This registers two supervised services:
This is required for the server to run reliably in the background:
To pull the latest server code and re-run setup:
cd ~/sms-server && git pull && bash android/setup.sh YOUR_API_KEY_HERE\n"},{"location":"docs/admin/broadcast/sms/#service-management","title":"Service Management","text":"If you installed termux-services (recommended):
# 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 Without termux-services (legacy watchdog):
# Check if the server is running\ncurl http://127.0.0.1:5001/health\n\n# Restart manually\ncd ~/sms-server/android && bash sms-watchdog.sh\n"},{"location":"docs/admin/broadcast/sms/#accessing-the-phone","title":"Accessing the Phone","text":"There are several ways to run commands on the phone:
"},{"location":"docs/admin/broadcast/sms/#direct-on-phone","title":"Direct (on phone)","text":"Simply open the Termux app and type commands. Best for initial setup.
"},{"location":"docs/admin/broadcast/sms/#ssh-remote-access","title":"SSH (remote access)","text":"Start the SSH server in Termux, then connect from your computer:
# 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"},{"location":"docs/admin/broadcast/sms/#scrcpy-screen-mirror","title":"scrcpy (screen mirror)","text":"Mirror the phone screen to your computer \u2014 great for setup:
# 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"},{"location":"docs/admin/broadcast/sms/#setup-wizard","title":"Setup Wizard","text":"The admin panel provides a guided three-step wizard at /app/sms/setup:
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.
"},{"location":"docs/admin/broadcast/sms/#step-2-connect","title":"Step 2: Connect","text":"Choose how to find your phone's IP address:
Tailscale Auto-Discovery (Recommended)Manual URL Entrytskey-api-...)100.x.x.x IPGetting a Tailscale API Key
Go to Tailscale Admin Console \u2192 Settings \u2192 Keys \u2192 Generate auth key or API access token.
Enter the phone's URL directly:
http://100.x.x.x:5001 (stable IP, works across networks)http://192.168.x.x:5001 (changes if phone reconnects)http://your-public-ip:5001 (requires router config)/health endpointenableSms feature flag is automatically enabledPOST /api/sms/send on the phonetermux-sms-send to send via Android's native SMSA background service (sms-response-sync.service.ts) polls the phone's inbox at a configurable interval:
GET /api/sms/inbox?since=<last_sync_timestamp> on the phonetermux-sms-list to get new messagesA background service (sms-device-monitor.service.ts) checks phone health periodically:
{name} variable placeholders/app/sms/setup Guided setup wizard with Tailscale auto-discovery /app/sms SMS dashboard \u2014 campaign overview and device status /app/sms/contacts Manage contact lists and entries /app/sms/campaigns Create and monitor SMS campaigns /app/sms/conversations View threaded conversations with contacts"},{"location":"docs/admin/broadcast/sms/#configuration","title":"Configuration","text":""},{"location":"docs/admin/broadcast/sms/#environment-variables","title":"Environment Variables","text":"Variable Default Description ENABLE_SMS false Feature flag (also set via setup wizard) TERMUX_API_URL \u2014 Phone URL, e.g. http://100.x.x.x:5001 TERMUX_API_KEY \u2014 Shared API key for authentication SMS_DELAY_BETWEEN_MS 1000 Delay between messages in a campaign (ms) SMS_MAX_RETRIES 3 Retry attempts for failed sends SMS_RESPONSE_SYNC_INTERVAL_MS 10000 How often to check for incoming replies (ms) SMS_DEVICE_MONITOR_INTERVAL_MS 30000 How often to check device health (ms) TAILSCALE_API_KEY \u2014 Tailscale API key for auto-discovery TAILSCALE_TAILNET \u2014 Tailscale tailnet name (optional) Note
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.
"},{"location":"docs/admin/broadcast/sms/#phone-side-configuration","title":"Phone-Side Configuration","text":"On the phone, only one environment variable is needed:
export SMS_API_SECRET='your-64-char-hex-key'\n The Flask server also accepts TERMUX_API_KEY as an alias for backwards compatibility.
The Flask server running on the phone exposes these endpoints on port 5001:
Method Endpoint Auth DescriptionGET /health No Server status, uptime, messages sent GET / No Web dashboard with endpoint documentation POST /api/sms/send Yes Send an SMS message POST /api/sms/send-reply Yes Send a reply with conversation tracking GET /api/sms/inbox No Get incoming messages (with since filter) GET /api/sms/list No List messages with pagination GET /api/sms/history No Get SMS history for a phone number GET /api/device/battery No Battery level, health, temperature GET /api/device/location No GPS coordinates (requires permission) GET /api/device/info No Device info + battery + uptime GET /api/contacts/list No Phone address book (with search) POST /api/campaign/notify No Push notification to device Authentication uses the X-API-Key header with the shared secret.
Symptoms: Test connection fails, \"Connection refused\" or timeout.
Checks:
ifconfig in Termux to find the current IP# Quick test from your server\ncurl http://PHONE_IP:5001/health\n"},{"location":"docs/admin/broadcast/sms/#authentication-required-errors","title":"\"Authentication required\" errors","text":"Symptoms: API calls return 401 with \"Authentication required\".
Fix: The API key on the phone doesn't match the one in the admin panel.
# 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\"' >> ~/.bashrc\n\n# Restart the server\nsv restart sms-api\n# Or without termux-services: pkill -f termux-sms-api-server.py && cd ~/sms-server/android && python termux-sms-api-server.py\n"},{"location":"docs/admin/broadcast/sms/#sms-not-sending","title":"SMS not sending","text":"Symptoms: Server responds successfully but messages don't arrive.
Checks:
Symptoms: Server stops after some time, especially when phone screen is off.
Fix:
termux-services (if not already): bash ~/sms-server/android/setup-services.sh \u2014 this uses runit, a proper service supervisor that auto-restarts the server immediately if it crashestermux-wake-lock in Termux (included in boot script)Symptoms: Server exits immediately with a security error.
Fix: Set the API key environment variable:
# 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\"' >> ~/.bashrc\n"},{"location":"docs/admin/broadcast/sms/#rcs-chat-features-interfering-with-replies","title":"RCS / Chat Features interfering with replies","text":"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.
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 termux-sms-list, so RCS messages are invisible to it.
Fix: Disable RCS on the SMS phone:
This must be done on the phone running the SMS server
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).
Additional checks:
To pull the latest version of the server 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 && cd android && python termux-sms-api-server.py\n"},{"location":"docs/admin/map/","title":"Map & Canvassing","text":"Manage locations, organize canvassing territories, schedule volunteer shifts, and coordinate door-to-door outreach.
"},{"location":"docs/admin/map/#in-this-section","title":"In This Section","text":"Draw polygon regions on the map to define canvassing territories. Areas organize locations into manageable chunks for volunteers.
"},{"location":"docs/admin/map/areas/#key-features","title":"Key Features","text":"/app/map/cuts to draw, edit, and delete area boundariesBulk-import addresses into an area from multiple data sources:
/app/map/cuts \u2014 draw and manage canvassing areas/app/map/cuts/:id/export \u2014 printable location report for a cutCoordinate and monitor volunteer door-to-door outreach with the canvass dashboard, walk sheets, and contact export tools.
"},{"location":"docs/admin/map/canvassing/#canvass-dashboard","title":"Canvass Dashboard","text":"From /app/map/canvass:
/app/map/walk-sheet with space for recording visit outcomes; includes up to 3 configurable QR codes/app/map/cuts/:id/export for a specific canvassing areaBridge canvassing data with advocacy campaigns:
/app/map/canvass \u2014 canvass dashboard/app/map/walk-sheet \u2014 printable walk sheetMonitor geocoding accuracy and coverage from /app/map/data-quality.
/app/map/data-quality \u2014 geocoding quality dashboardImport addresses via CSV or the Canadian National Address Register (NAR), geocode them with multiple providers, and manage the location database.
"},{"location":"docs/admin/map/locations/#adding-locations","title":"Adding Locations","text":"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.
"},{"location":"docs/admin/map/locations/#bulk-operations","title":"Bulk Operations","text":"Select multiple locations for:
/app/map \u2014 location CRUD, CSV import/export, geocoding, area import wizardConfigure the default map view and QR code links from /app/map/settings.
/app/map/settings \u2014 map configurationSchedule volunteer time slots and let people sign up through a public page. Shifts can be linked to specific areas so volunteers know where they'll be canvassing.
"},{"location":"docs/admin/map/shifts/#creating-shifts","title":"Creating Shifts","text":"/app/map/shifts \u2014 shift CRUD, calendar view, signup managementUpload, organize, and share campaign videos and photos with built-in analytics and engagement features. Enable with enableMediaFeatures in Settings.
Create promotional cards that appear in the public media gallery and documentation site. Manage from /app/media/ads.
/app/media/ads \u2014 gallery ad management and analyticsTrack video engagement with GDPR-compliant analytics (IP hashing, 90-day retention).
"},{"location":"docs/admin/media/analytics/#per-video-metrics","title":"Per-Video Metrics","text":"Each video tracks:
The analytics dashboard at /app/media/analytics provides:
Public endpoints record engagement:
navigator.sendBeacon for reliable end-of-session reporting/app/media/analytics \u2014 global analytics dashboardCurate the public gallery experience with playlists, a shorts feed, and featured content.
"},{"location":"docs/admin/media/curated/#playlists","title":"Playlists","text":"From /app/media/curated:
TikTok-style vertical video feed for clips under 60 seconds:
/gallery/shorts/app/media/curated \u2014 playlist managementThe media library at /app/media/library is where you upload, organize, and publish video and photo content.
Generate 24-hour JWT-authenticated preview links for unpublished videos \u2014 useful for stakeholder review before publishing.
"},{"location":"docs/admin/media/library/#admin-routes","title":"Admin Routes","text":"/app/media/library \u2014 video and photo management/app/media/jobs \u2014 processing job queue monitoringAdmin tools for reviewing and managing comments across all media content at /app/media/moderation.
For each comment, admins can:
A configurable list of words with severity levels:
Severity Action High Auto-blocks the comment Medium Auto-hides for review Low Flags for moderator attention Custom User-defined severityThe filter list is cached with a 1-minute TTL and invalidated on changes.
"},{"location":"docs/admin/media/moderation/#admin-routes","title":"Admin Routes","text":"/app/media/moderation \u2014 comment moderation and word filter managementAccept memberships, product sales, and donations through Stripe. Enable with enablePayments in Settings.
.env (ENABLE_PAYMENTS=true)ENCRYPTION_KEY)Create custom branded donation landing pages with independent branding and goals.
"},{"location":"docs/admin/payments/donations/#donation-pages","title":"Donation Pages","text":"From /app/donation-pages:
/donate/:slugFrom /app/donations:
/app/donations \u2014 donation management/app/donation-pages \u2014 donation page CRUDCreate and manage recurring subscription plans for campaign supporters at /app/plans.
/pricing/app/plans \u2014 subscription plan managementManage campaign merchandise and one-time purchase items at /app/products.
/shop for public browsing and purchase/app/products \u2014 product managementConfigure Stripe integration from Settings > Payments.
"},{"location":"docs/admin/payments/settings/#stripe-configuration","title":"Stripe Configuration","text":"ENCRYPTION_KEY environment variable (AES encryption)Stripe webhooks are automatically configured to handle:
/app/settings (Payments tab) \u2014 Stripe key configurationManage the platform's infrastructure services, monitoring stack, and third-party integrations.
"},{"location":"docs/admin/services/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/services/crowdsec/#architecture","title":"Architecture","text":"graph LR\n User -->|HTTPS| Traefik\n Traefik -->|forwardAuth| Tinyauth\n Tinyauth -->|authenticated| Traefik\n Traefik -->|proxy| CrowdSec-Manager\n CrowdSec-Manager -->|API| CrowdSec\n Traefik -->|bouncer plugin| CrowdSec\n CrowdSec -->|captcha decision| Turnstile[Cloudflare Turnstile] All services run on the same Docker Compose stack and share the pangolin network. Traefik reaches them through Gerbil's network namespace.
Image: hhftechnology/crowdsec-manager:1.1.0
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.
Accessible at: https://crowdsec.bnkserve.org
Image: ghcr.io/steveiliop56/tinyauth:v4
A lightweight forward-auth middleware that protects the CrowdSec Manager dashboard with a login screen. Traefik's forwardAuth middleware checks every request to the manager against Tinyauth before allowing access.
Login page at: https://auth.bnkserve.org
User credentials are stored in a users file (/data/users) mounted from the host, using bcrypt-hashed passwords.
Special Characters in Passwords
Tinyauth v4 has a known issue where special characters (@, !, 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.
crowdsec-manager-router crowdsec.bnkserve.org security-headers, tinyauth Dashboard (HTTPS) crowdsec-manager-redirect crowdsec.bnkserve.org redirect-to-https HTTP \u2192 HTTPS redirect tinyauth-router auth.bnkserve.org security-headers Auth login page (HTTPS) tinyauth-redirect auth.bnkserve.org redirect-to-https HTTP \u2192 HTTPS redirect No tinyauth middleware on the tinyauth router
The tinyauth-router must not have the tinyauth forwardAuth middleware applied \u2014 this would create an infinite redirect loop.
The tinyauth forwardAuth middleware forwards every request to http://tinyauth:3000/api/auth/traefik. If the user has a valid session cookie (scoped to .bnkserve.org), the request passes through. Otherwise, the user is redirected to the Tinyauth login page.
tinyauth:\n forwardAuth:\n address: http://tinyauth:3000/api/auth/traefik\n trustForwardHeader: true\n authResponseHeaders:\n - X-Forwarded-User\n"},{"location":"docs/admin/services/crowdsec/#crowdsec-tuning","title":"CrowdSec Tuning","text":""},{"location":"docs/admin/services/crowdsec/#relaxed-crawl-detection","title":"Relaxed Crawl Detection","text":"The crowdsecurity/http-crawl-non_statics scenario was triggering on legitimate Canadian users browsing the site. The local override at /etc/crowdsec/scenarios/http-crawl-non_statics.yaml (replacing the hub symlink) has relaxed thresholds:
capacity 40 80 Twice as many distinct pages before triggering leakspeed 0.5s 0.25s Bucket drains twice as fast 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.
"},{"location":"docs/admin/services/crowdsec/#canadian-isp-whitelist","title":"Canadian ISP Whitelist","text":"A whitelist expression in /etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml exempts traffic from major Canadian ISPs from all CrowdSec scenarios:
expression:\n - evt.Meta.ASNNumber in ['812', '852', '6327', '5645', '20365', '25668', '577']\n AS Number ISP 812 Rogers Communications 852 TELUS Communications 6327 Shaw Communications 5645 TekSavvy 20365 Freedom Mobile 25668 CipherKey 577 Bell Canada Field name
The GeoIP enricher populates evt.Meta.ASNNumber (not ASNumber). This can be verified by inspecting /etc/crowdsec/parsers/s02-enrich/geoip-enrich.yaml.
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.
Configuration added to the CrowdSec bouncer plugin in dynamic_config.yml:
captchaProvider: turnstile\ncaptchaSiteKey: <site-key>\ncaptchaSecretKey: <secret-key>\ncaptchaHTMLFilePath: /etc/traefik/captcha.html\n Captcha HTML template path
The captcha.html template is copied from the plugin source to /etc/traefik/captcha.html (the mounted config volume). Do not reference the /plugins-storage/ path directly \u2014 the hash in that path changes on every Traefik restart.
Two A records pointing to 72.11.155.21:
crowdsec.bnkserve.org CrowdSec Manager dashboard auth.bnkserve.org Tinyauth login page"},{"location":"docs/admin/services/crowdsec/#verification","title":"Verification","text":"# 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>&1 | grep -i captcha\n"},{"location":"docs/admin/services/integrations/","title":"Integrations","text":"Changemaker Lite integrates with several self-hosted services. Each runs as a Docker container and can be enabled independently.
"},{"location":"docs/admin/services/integrations/#team-chat-rocketchat","title":"Team Chat (Rocket.Chat)","text":"Self-hosted team chat for volunteer coordination. Enable with enableChat in Settings.
/app/services/rocketchat, /volunteer/chatSelf-hosted video calls integrated with Rocket.Chat via JWT authentication. Enable with enableMeet in Settings.
now + 1hourToken Expiration
Set the Jitsi app's Token Expiration to now + 1hour. A raw number like 120 is interpreted as Unix timestamp 120 (Jan 1970), causing all tokens to appear expired.
Bitwarden-compatible password vault for secure team credential sharing.
https://vault.DOMAINCollaborative whiteboard for brainstorming and campaign planning.
/app/services/excalidraw/app/services/excalidrawSelf-hosted Git repository hosting for campaign code and configuration.
git.DOMAIN or embedded in admin at /app/services/giteaSelf-hosted workflow automation for connecting platform events to external services.
n8n.DOMAIN or embedded in admin at /app/services/n8nBuilt-in QR code generation for walk sheets, volunteer invites, and campaign links.
/api/qr/app/services/qrThe monitoring stack runs as a Docker Compose profile and provides metrics collection, visualization, and alerting.
"},{"location":"docs/admin/services/monitoring/#starting-the-stack","title":"Starting the Stack","text":"docker compose --profile monitoring up -d\n This starts Prometheus, Grafana, Alertmanager, cAdvisor, Node Exporter, and Redis Exporter.
"},{"location":"docs/admin/services/monitoring/#custom-metrics","title":"Custom Metrics","text":"The platform exposes 12 custom cm_* Prometheus metrics:
Three pre-configured dashboards auto-provisioned from configs/grafana/:
Alert rules in configs/prometheus/alerts.yml cover:
/app/observability \u2014 embedded Grafana dashboards, alert status, and service health (3 tabs)localhost:3001 or grafana.DOMAINlocalhost:9090localhost:9093Pangolin provides secure tunneling to expose your self-hosted services to the internet without port forwarding or a static IP.
"},{"location":"docs/admin/services/tunnel/#setup","title":"Setup","text":"From /app/pangolin:
.env with credentials, and restarts the Newt tunnel containerThe platform defines 12+ service resources in configs/pangolin/resources.yml:
api.DOMAIN, app.DOMAIN) to an internal serviceThe Newt container runs alongside nginx and tunnels traffic to your services:
PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET environment variablesnginx:80)The Pangolin server runs CrowdSec for intrusion detection with a web management UI protected by Tinyauth forward-auth. See CrowdSec & Security for details on:
crowdsec.bnkserve.org)auth.bnkserve.org)/app/pangolin \u2014 tunnel status, setup wizard, and resource managementAutomatically create and sync user accounts across integrated services when new platform users are registered. Enable with enableUserProvisioning in Settings.
From Settings > User Provisioning:
Trigger a bulk sync from /api/users/provisioning/sync to provision all existing users across enabled services. Useful after enabling a new service.
/app/users (edit drawer) \u2014 per-user service account status and actions/app/settings (User Provisioning tab) \u2014 per-service toggle and timingManage the public-facing web presence \u2014 landing pages, the dynamic homepage, navigation menu, and documentation site.
"},{"location":"docs/admin/web/#in-this-section","title":"In This Section","text":"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.
"},{"location":"docs/admin/web/documentation/","title":"Documentation","text":"Manage the MkDocs documentation site, track page engagement, and moderate visitor comments.
"},{"location":"docs/admin/web/documentation/#mkdocs-management","title":"MkDocs Management","text":"From Docs (/app/docs):
/app/docs/settings)Track how visitors interact with documentation pages using the MkDocs Material theme's custom analytics provider and navigation.tracking.
docs/overrides/Visitors can leave comments on documentation pages using a Gitea-backed comment system.
/app/docs-comments for approving, hiding, or deleting comments/app/docs \u2014 MkDocs management (file tree, config, build triggers)/app/docs/settings \u2014 documentation configuration/app/docs-comments \u2014 moderate documentation commentsA dynamic public landing page that showcases your organization and aggregates content from across the platform.
"},{"location":"docs/admin/web/homepage/#sections","title":"Sections","text":"The homepage assembles its content from enabled modules. Sections are only displayed when their corresponding module is active and has data to show.
All homepage data is fetched from a single API endpoint (/api/homepage) and cached in Redis for 2 minutes. Individual section queries use Promise.allSettled so that a failure in one module (e.g., Gancio being offline) does not prevent the rest of the page from loading.
homepageTagline field in site settings/home \u2014 public homepageBuild campaign microsites with a drag-and-drop visual editor.
"},{"location":"docs/admin/web/landing-pages/#how-it-works","title":"How It Works","text":"/p/:slug/app/pages \u2014 list and manage landing pages/app/pages/:id/edit \u2014 full-screen GrapesJS editor/p/:slug \u2014 view a published landing pageCustomize 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.
"},{"location":"docs/admin/web/navigation/#key-features","title":"Key Features","text":"enableInfluence) are automatically hidden when that feature is disabledThe platform ships with 11 builtin navigation items that cover the main public routes:
Home, Campaigns, Map, Shifts, Events, Gallery, Pricing, Shop, Donate, Website (landing page), and Docs (documentation site).
Each builtin item has a default icon and path. Some paths use special $ tokens (e.g., $landing, $docs) that are automatically resolved to the correct external URL based on the deployment environment.
Add any number of custom links via the \"Add Custom Link\" button. Custom links support:
/blog)https://example.com) -- automatically detected and opened in a new tabCustom links can be deleted from the navigation; builtin items can only be toggled off.
"},{"location":"docs/admin/web/navigation/#mobile-handling","title":"Mobile Handling","text":"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.
"},{"location":"docs/admin/web/navigation/#admin-routes","title":"Admin Routes","text":"/app/navigation -- navigation editor with per-item toggle, reorder, label editing, and custom link managementChangemaker Lite exposes two REST APIs sharing a single PostgreSQL database.
Server Framework Port Purpose Main API Express.js4000 Auth, campaigns, map, shifts, canvassing, pages, email, settings Media API Fastify 4100 Video library, analytics, playlists, reactions, comments Both APIs use JWT Bearer authentication and return JSON. All request/response bodies are application/json unless noted otherwise.
sequenceDiagram\n participant Client\n participant API\n participant DB\n\n Client->>API: POST /api/auth/login {email, password}\n API->>DB: Verify credentials\n DB-->>API: User record\n API-->>Client: {accessToken, refreshToken}\n Note over Client: Store tokens\n\n Client->>API: GET /api/campaigns (Authorization: Bearer <accessToken>)\n API-->>Client: 200 OK\n\n Note over Client: Access token expires (15 min)\n\n Client->>API: POST /api/auth/refresh {refreshToken}\n API->>DB: Rotate token (atomic transaction)\n DB-->>API: New token pair\n API-->>Client: {accessToken, refreshToken}"},{"location":"docs/api/#headers","title":"Headers","text":"All authenticated requests require:
Authorization: Bearer <accessToken>\n The Media API also accepts tokens via query parameter for SSE streams:
GET /api/public/:id/chat-stream?token=<accessToken>\n"},{"location":"docs/api/#roles","title":"Roles","text":"Role Access Level SUPER_ADMIN Full platform access INFLUENCE_ADMIN Campaign and advocacy management MAP_ADMIN Map, locations, shifts, canvassing USER Volunteer portal, public features TEMP Limited access (auto-created on public shift signup)"},{"location":"docs/api/#middleware-reference","title":"Middleware Reference","text":"Middleware Effect authenticate Requires valid JWT. Sets req.user with id, email, role. Returns 401 if missing or invalid. optionalAuth Same as authenticate but continues without user if token is absent. requireRole(...roles) Checks user role against allowed list. Returns 403 if not authorized. requireNonTemp Blocks TEMP users. Returns 403. validate(schema, source) Validates request body/query/params against a Zod schema. Returns 400 on failure."},{"location":"docs/api/#error-responses","title":"Error Responses","text":"All errors follow a consistent format:
{\n \"error\": {\n \"message\": \"Human-readable error description\",\n \"code\": \"ERROR_CODE\",\n \"statusCode\": 400\n }\n}\n Status Code Meaning 400 VALIDATION_ERROR Request body/query failed schema validation 401 UNAUTHORIZED Missing or invalid access token 403 FORBIDDEN Valid token but insufficient role 404 NOT_FOUND Resource does not exist 429 RATE_LIMITED Too many requests (see Rate Limits) 500 INTERNAL_ERROR Unexpected server error Enumeration Prevention
Auth endpoints (/login, /register, /forgot-password) return generic success messages to prevent user enumeration. A 401 from /api/auth/me does not reveal whether the user exists.
Rate limits are Redis-backed and keyed by IP address.
Endpoint Group Window Max Requests Redis Prefix Auth (login, register, refresh) 15 min 10rl:auth: Email sending 1 hour 30 rl:email: Response submission 1 hour 10 rl:response: Shift signup 1 hour 10 rl:shift-signup: Canvass visits 1 min 30 rl:canvass-visit: Canvass bulk visits 1 min 5 rl:canvass-visit-bulk: GPS tracking 1 min 6 rl:gps-tracking: Canvass geocode 1 min 10 rl:canvass-geocode: Observability 1 min 20 rl:observability: Health/metrics 1 min 30 rl:health-metrics: Global (all other) Configurable Configurable rl:global: When rate-limited, the API returns:
{\n \"error\": {\n \"message\": \"Too many requests, please try again later\",\n \"code\": \"RATE_LIMITED\",\n \"statusCode\": 429\n }\n}\n"},{"location":"docs/api/#main-api-express-port-4000","title":"Main API (Express \u2014 Port 4000)","text":""},{"location":"docs/api/#health-metrics","title":"Health & Metrics","text":"Method Path Auth Description GET /api/health Health check \u2014 PostgreSQL + Redis ping GET /api/metrics Prometheus metrics (text/plain) Health response {\n \"status\": \"healthy\",\n \"checks\": {\n \"database\": \"ok\",\n \"redis\": \"ok\"\n }\n}\n"},{"location":"docs/api/#auth","title":"Auth","text":"Prefix: /api/auth
/api/auth/login Email + password login POST /api/auth/register Create account (always USER role) POST /api/auth/verify-email Verify email with token POST /api/auth/resend-verification Resend verification email POST /api/auth/forgot-password Send password reset email POST /api/auth/reset-password Set new password with reset token POST /api/auth/refresh Rotate refresh token \u2192 new token pair POST /api/auth/logout Invalidate refresh token GET /api/auth/me Current user profile Login request & response Request:
{\n \"email\": \"admin@example.com\",\n \"password\": \"SecurePass123!\"\n}\n Response: {\n \"accessToken\": \"eyJhbG...\",\n \"refreshToken\": \"eyJhbG...\",\n \"user\": {\n \"id\": \"uuid\",\n \"email\": \"admin@example.com\",\n \"name\": \"Admin\",\n \"role\": \"SUPER_ADMIN\"\n }\n}\n Password Policy
Passwords must be at least 12 characters with at least one uppercase letter, one lowercase letter, and one digit.
"},{"location":"docs/api/#users","title":"Users","text":"Prefix: /api/users \u00b7 Auth: All routes require authentication
/api/users Admin Paginated user list with search, role, and status filters GET /api/users/:id Admin or self Single user profile POST /api/users Admin Create user PUT /api/users/:id Admin or self Update user (non-admins cannot change role/status) POST /api/users/:id/approve Admin Approve pending user; sends approval email POST /api/users/:id/reject Admin Reject pending user DELETE /api/users/:id Admin Delete user Query parameters for GET /api/users:
page number Page number (default 1) limit number Items per page (default 20) search string Search by name or email role string Filter by role status string Filter by status"},{"location":"docs/api/#dashboard","title":"Dashboard","text":"Prefix: /api/dashboard \u00b7 Auth: Admin roles required
/api/dashboard/summary Any admin Platform-wide counts (users, campaigns, locations, shifts) GET /api/dashboard/system SUPER_ADMIN Hardware + OS info (CPU, memory, disk) GET /api/dashboard/containers SUPER_ADMIN Docker container statuses GET /api/dashboard/weather Any admin Current weather at map center coordinates GET /api/dashboard/api-metrics SUPER_ADMIN Prometheus API performance metrics GET /api/dashboard/time-series SUPER_ADMIN Prometheus time-series data GET /api/dashboard/container-resources SUPER_ADMIN cAdvisor CPU/memory/network per container Query parameters for GET /api/dashboard/time-series:
metrics string Comma-separated metric keys (whitelist-validated) range string Time range (e.g., 1h, 24h, 7d) step string Sample interval (e.g., 5m, 1h)"},{"location":"docs/api/#campaigns","title":"Campaigns","text":""},{"location":"docs/api/#admin-crud","title":"Admin CRUD","text":"Prefix: /api/campaigns \u00b7 Auth: Admin roles
/api/campaigns Paginated campaign list GET /api/campaigns/:id Single campaign detail POST /api/campaigns Create campaign PUT /api/campaigns/:id Update campaign DELETE /api/campaigns/:id Delete campaign"},{"location":"docs/api/#public","title":"Public","text":"Method Path Auth Description GET /api/campaigns/public List all active campaigns GET /api/campaigns/:slug/details Campaign detail by slug (ACTIVE only)"},{"location":"docs/api/#user-submissions","title":"User Submissions","text":"Auth: Authenticated, non-TEMP users
Method Path Description POST/api/campaigns/user/submit Submit campaign for moderation (5/hour limit) GET /api/campaigns/user/my-campaigns List own submitted campaigns PUT /api/campaigns/user/:id Edit own pending campaign"},{"location":"docs/api/#moderation","title":"Moderation","text":"Auth: Admin roles
Method Path Description GET/api/campaigns/moderation/queue Campaigns pending moderation GET /api/campaigns/moderation/stats Moderation queue statistics PATCH /api/campaigns/moderation/:id Approve or reject campaign"},{"location":"docs/api/#campaign-emails","title":"Campaign Emails","text":"Method Path Auth Description POST /api/campaigns/:slug/send-email Send advocacy email to representatives (rate limited: 30/hour) POST /api/campaigns/:slug/track-mailto Track mailto link click GET /api/campaigns/:id/emails Admin Paginated emails for campaign GET /api/campaigns/:id/email-stats Admin Email statistics"},{"location":"docs/api/#responses","title":"Responses","text":"Prefix: /api/campaigns (public) and /api/responses (admin + actions)
/api/campaigns/:slug/responses List approved public responses GET /api/campaigns/:slug/response-stats Response statistics POST /api/campaigns/:slug/responses Submit response (rate limited: 10/hour) POST /api/responses/:id/upvote Optional Upvote a response DELETE /api/responses/:id/upvote Optional Remove upvote GET /api/responses/:id/verify/:token Verify response via email link"},{"location":"docs/api/#admin","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/responses All responses with filters PATCH /api/responses/:id/status Approve or reject response POST /api/responses/:id/resend-verification Resend verification email DELETE /api/responses/:id Delete response"},{"location":"docs/api/#representatives","title":"Representatives","text":"Prefix: /api/representatives
/api/representatives/by-postal/:postalCode Lookup representatives by postal code (cache-first) GET /api/representatives/test-connection Represent API health check GET /api/representatives/cache-stats Admin Cache statistics GET /api/representatives Admin Paginated cached representatives GET /api/representatives/:id Admin Single cached representative DELETE /api/representatives/by-postal/:postalCode Admin Clear cache for postal code DELETE /api/representatives/:id Admin Delete cached representative Query parameters for postal code lookup:
Param Type Descriptionrefresh boolean Force API call, bypass cache"},{"location":"docs/api/#email-queue","title":"Email Queue","text":"Prefix: /api/email-queue \u00b7 Auth: Admin roles
/api/email-queue/stats BullMQ queue statistics (waiting, active, completed, failed) POST /api/email-queue/pause Pause email processing POST /api/email-queue/resume Resume email processing POST /api/email-queue/clean Clean completed jobs"},{"location":"docs/api/#locations","title":"Locations","text":"Prefix: /api/map/locations
/api/map/locations/public All geocoded locations for map (no PII); optional ?bounds="},{"location":"docs/api/#admin_1","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/locations Paginated locations with filters GET /api/map/locations/stats Location statistics GET /api/map/locations/all All geocoded locations for admin map GET /api/map/locations/export-csv CSV export GET /api/map/locations/:id Single location GET /api/map/locations/:id/history Edit history POST /api/map/locations Create location PUT /api/map/locations/:id Update location DELETE /api/map/locations/:id Delete location POST /api/map/locations/bulk-delete Bulk delete POST /api/map/locations/geocode Geocode single address POST /api/map/locations/geocode-missing Batch geocode all ungeocoded POST /api/map/locations/reverse-geocode Reverse geocode lat/lng to address POST /api/map/locations/import-csv Import from CSV (10 MB limit) POST /api/map/locations/import-bulk Bulk NAR or standard CSV import (100 MB limit)"},{"location":"docs/api/#bulk-geocode","title":"Bulk Geocode","text":"Prefix: /api/map/locations/bulk-geocode \u00b7 Auth: Map admins
/api/map/locations/bulk-geocode Start BullMQ bulk geocoding job GET /api/map/locations/bulk-geocode/:jobId Poll job status GET /api/map/locations/bulk-geocode/stats Queue statistics"},{"location":"docs/api/#nar-import","title":"NAR Import","text":"Prefix: /api/map/nar-import \u00b7 Auth: Map admins
/api/map/nar-import/datasets Available NAR datasets by province POST /api/map/nar-import Start province import (fire-and-forget) GET /api/map/nar-import/status/:importId Poll import progress NAR Import body {\n \"provinceCode\": \"24\",\n \"filterType\": \"city\",\n \"filterCity\": \"Edmonton\",\n \"residentialOnly\": true,\n \"deduplicateRadius\": 10,\n \"batchSize\": 500\n}\n"},{"location":"docs/api/#area-import","title":"Area Import","text":"Prefix: /api/map/area-import \u00b7 Auth: Map admins
/api/map/area-import/preview Preview bounds + estimated record counts POST /api/map/area-import Start area import (fire-and-forget) GET /api/map/area-import/status/:importId Poll import progress"},{"location":"docs/api/#cuts-polygons","title":"Cuts (Polygons)","text":"Prefix: /api/map/cuts
/api/map/cuts/public All public cuts as GeoJSON GET /api/map/cuts Map admin Paginated cuts list GET /api/map/cuts/:id Map admin Single cut POST /api/map/cuts Map admin Create cut (polygon GeoJSON) PUT /api/map/cuts/:id Map admin Update cut DELETE /api/map/cuts/:id Map admin Delete cut GET /api/map/cuts/:id/locations Map admin All locations within cut polygon GET /api/map/cuts/:id/statistics Map admin Support level breakdown GET /api/map/cuts/export-geojson Map admin All cuts as GeoJSON FeatureCollection GET /api/map/cuts/:id/export-geojson Map admin Single cut as GeoJSON Feature POST /api/map/cuts/import-geojson Map admin Import cuts from GeoJSON file"},{"location":"docs/api/#shifts","title":"Shifts","text":"Prefix: /api/map/shifts
/api/map/shifts/public List upcoming public shifts POST /api/map/shifts/public/:id/signup Public signup (creates TEMP user if needed; rate limited: 10/hour)"},{"location":"docs/api/#volunteer","title":"Volunteer","text":"Auth: Any authenticated user
Method Path Description GET/api/map/shifts/volunteer/upcoming Upcoming shifts with signup status GET /api/map/shifts/volunteer/my-signups Own confirmed signups POST /api/map/shifts/volunteer/:id/signup Sign up for shift DELETE /api/map/shifts/volunteer/:id/signup Cancel signup"},{"location":"docs/api/#admin_2","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/shifts Paginated shifts with filters GET /api/map/shifts/stats Statistics GET /api/map/shifts/calendar Calendar data (?startDate=&endDate=) GET /api/map/shifts/:id Single shift with signups POST /api/map/shifts Create shift PUT /api/map/shifts/:id Update shift DELETE /api/map/shifts/:id Delete shift POST /api/map/shifts/:id/signups Admin-add volunteer DELETE /api/map/shifts/:id/signups/:signupId Remove volunteer POST /api/map/shifts/:id/email-details Email details to all volunteers"},{"location":"docs/api/#shift-series","title":"Shift Series","text":"Auth: Map admins
Method Path Description POST/api/map/shifts/series Create recurring shift series GET /api/map/shifts/series/:id Get series PUT /api/map/shifts/series/:id Update series DELETE /api/map/shifts/series/:id Delete series"},{"location":"docs/api/#canvassing","title":"Canvassing","text":"Prefix: /api/map/canvass
Auth: Any authenticated user
Method Path Description GET/api/map/canvass/my/assignments Shift assignments GET /api/map/canvass/my/stats Personal canvass statistics GET /api/map/canvass/my/visits Visit history GET /api/map/canvass/my/session Active canvass session POST /api/map/canvass/sessions Start canvass session POST /api/map/canvass/sessions/:id/end End session GET /api/map/canvass/cuts/:cutId/locations Locations in cut with visit annotations GET /api/map/canvass/cuts/:cutId/route Walking route algorithm for cut GET /api/map/canvass/locations All locations with visit annotations PUT /api/map/canvass/locations/:id Edit address (role-gated fields) POST /api/map/canvass/locations Create location POST /api/map/canvass/reverse-geocode Reverse geocode lat/lng POST /api/map/canvass/geocode-search Geocode address for map (rate limited: 10/min) POST /api/map/canvass/visits Record door knock (rate limited: 30/min) POST /api/map/canvass/visits/bulk Record visit for all unvisited units (rate limited: 5/min)"},{"location":"docs/api/#admin_3","title":"Admin","text":"Auth: SUPER_ADMIN or MAP_ADMIN
/api/map/canvass/stats Platform-wide canvass statistics GET /api/map/canvass/stats/cuts/:cutId Statistics for specific cut GET /api/map/canvass/activity Recent activity feed GET /api/map/canvass/volunteers All volunteers with canvass activity GET /api/map/canvass/volunteers/:userId Individual volunteer statistics GET /api/map/canvass/visits All visits with filters"},{"location":"docs/api/#gps-tracking","title":"GPS Tracking","text":"Prefix: /api/map/tracking
Auth: Any authenticated user
Method Path Description POST/api/map/tracking/sessions Start GPS tracking session POST /api/map/tracking/sessions/:id/end End tracking session POST /api/map/tracking/sessions/:id/points Submit GPS point batch (rate limited: 6/min) POST /api/map/tracking/sessions/:id/link-canvass Link to canvass session GET /api/map/tracking/my/session Active tracking session GET /api/map/tracking/my/sessions Own historical sessions GET /api/map/tracking/my/sessions/:id/route Full route for own session"},{"location":"docs/api/#admin_4","title":"Admin","text":"Auth: Map admins
Method Path Description GET/api/map/tracking/live Live volunteer positions + trails GET /api/map/tracking/sessions All historical tracking sessions GET /api/map/tracking/sessions/:id/route Full route for any session"},{"location":"docs/api/#map-settings","title":"Map Settings","text":"Prefix: /api/map/settings
/api/map/settings Public map settings (center, zoom, walk sheet config) PUT /api/map/settings Map admin Update map settings"},{"location":"docs/api/#geocoding","title":"Geocoding","text":"Prefix: /api/map/geocoding \u00b7 Auth: Map admins
/api/map/geocoding/search Geocode address search (?q=&limit=1-10)"},{"location":"docs/api/#landing-pages","title":"Landing Pages","text":"Prefix: /api/pages and /api/page-blocks
/api/pages/:slug/view Get published page by slug"},{"location":"docs/api/#admin_5","title":"Admin","text":"Auth: Admin roles
Method Path Description GET/api/pages Paginated landing pages GET /api/pages/:id Single page POST /api/pages Create page PUT /api/pages/:id Update page DELETE /api/pages/:id Delete page POST /api/pages/sync Sync MkDocs overrides from filesystem POST /api/pages/validate Validate and repair MkDocs exports"},{"location":"docs/api/#block-library","title":"Block Library","text":"Auth: Admin roles
Method Path Description GET/api/page-blocks List blocks GET /api/page-blocks/:id Single block POST /api/page-blocks Create block PUT /api/page-blocks/:id Update block DELETE /api/page-blocks/:id Delete block"},{"location":"docs/api/#email-templates","title":"Email Templates","text":"Prefix: /api/email-templates \u00b7 Auth: Admin roles (seed/cache require SUPER_ADMIN)
/api/email-templates List templates GET /api/email-templates/:id Single template POST /api/email-templates Create template PUT /api/email-templates/:id Update template DELETE /api/email-templates/:id Delete template GET /api/email-templates/:id/versions Version history GET /api/email-templates/:id/versions/:versionNumber Specific version POST /api/email-templates/:id/rollback Rollback to prior version POST /api/email-templates/validate Validate Handlebars syntax POST /api/email-templates/:id/test Send test email (rate limited: 10/15min) GET /api/email-templates/:id/test-logs Test send logs POST /api/email-templates/seed Seed templates from filesystem POST /api/email-templates/clear-cache Clear template cache"},{"location":"docs/api/#qr-codes","title":"QR Codes","text":"Method Path Auth Description GET /api/qr Generate QR code PNG (?text=&size=50-500) Cached for 1 hour. Returns image/png.
Prefix: /api/settings
/api/settings Public site settings (SMTP credentials stripped) GET /api/settings/admin SUPER_ADMIN Full settings including SMTP credentials PUT /api/settings SUPER_ADMIN Update settings POST /api/settings/email/test-connection SUPER_ADMIN Test SMTP connection POST /api/settings/email/test-send SUPER_ADMIN Send test email"},{"location":"docs/api/#listmonk-newsletter-sync","title":"Listmonk (Newsletter Sync)","text":"Prefix: /api/listmonk \u00b7 Auth: SUPER_ADMIN
/api/listmonk Sync status + connection check GET /api/listmonk/stats Subscriber counts from Listmonk POST /api/listmonk/test-connection Health check POST /api/listmonk/sync/participants Sync campaign participants POST /api/listmonk/sync/locations Sync locations POST /api/listmonk/sync/users Sync users POST /api/listmonk/sync/all Run all sync operations POST /api/listmonk/reinitialize Reinitialize Listmonk lists GET /api/listmonk/proxy-url Proxy port + JWT for iframe"},{"location":"docs/api/#documentation-management","title":"Documentation Management","text":"Prefix: /api/docs \u00b7 Auth: Authenticated, non-TEMP (write operations require SUPER_ADMIN)
/api/docs/status MkDocs + Code Server availability GET /api/docs/config Port numbers for iframe URLs GET /api/docs/mkdocs-config Read raw mkdocs.yml PUT /api/docs/mkdocs-config Write mkdocs.yml POST /api/docs/build Trigger MkDocs build POST /api/docs/upload Upload asset (20 MB, whitelisted extensions) GET /api/docs/files File tree (?force=true bypasses cache) POST /api/docs/files/rename Rename or move file GET /api/docs/files/* Read file content PUT /api/docs/files/* Write file content POST /api/docs/files/* Create file or folder DELETE /api/docs/files/* Delete file or empty folder"},{"location":"docs/api/#services","title":"Services","text":"Prefix: /api/services \u00b7 Auth: SUPER_ADMIN
/api/services/status Health check all managed services (NocoDB, n8n, Gitea, MailHog, Mini QR, Excalidraw, Homepage) GET /api/services/config Port numbers + subdomain info"},{"location":"docs/api/#pangolin-tunnel-management","title":"Pangolin (Tunnel Management)","text":"Prefix: /api/pangolin \u00b7 Auth: SUPER_ADMIN
/api/pangolin/status Tunnel health + connection info GET /api/pangolin/config Current env configuration GET /api/pangolin/newt-status Newt container status POST /api/pangolin/newt-restart Restart Newt container GET /api/pangolin/sites List Pangolin sites GET /api/pangolin/exit-nodes Available exit nodes GET /api/pangolin/resource-definitions Resource definitions from YAML GET /api/pangolin/resources List resources POST /api/pangolin/setup Create site + all resources (rate limited: \u2157min) POST /api/pangolin/sync Sync resources (create missing, update changed) PUT /api/pangolin/resource/:id Update resource DELETE /api/pangolin/resource/:id Delete resource GET /api/pangolin/resource/:id/clients Connected clients GET /api/pangolin/certificate/:domainId/:domain Certificate info POST /api/pangolin/certificate/:certId Update certificate"},{"location":"docs/api/#observability","title":"Observability","text":"Prefix: /api/observability \u00b7 Auth: SUPER_ADMIN \u00b7 Rate limited: 20/min
/api/observability/status Check 7 monitoring services GET /api/observability/metrics-summary Key metrics from Prometheus GET /api/observability/alerts Active alerts from Alertmanager"},{"location":"docs/api/#payments","title":"Payments","text":"Prefix: /api/payments
/api/payments/config Stripe publishable key + donation settings GET /api/payments/plans Active subscription plans GET /api/payments/products Active products (?type=) POST /api/payments/subscribe Create subscription checkout POST /api/payments/purchase Optional Product checkout (guest or logged-in) POST /api/payments/donate Donation checkout GET /api/payments/my-subscription Current subscription POST /api/payments/my-subscription/cancel Cancel subscription POST /api/payments/webhook Stripe webhook (raw body)"},{"location":"docs/api/#admin_6","title":"Admin","text":"Auth: SUPER_ADMIN
/api/payments/admin/settings Payment settings (secrets masked) PUT /api/payments/admin/settings Update payment settings POST /api/payments/admin/settings/test-connection Test Stripe connection GET /api/payments/admin/dashboard Subscription + donation statistics GET /api/payments/admin/plans All subscription plans POST /api/payments/admin/plans Create plan PUT /api/payments/admin/plans/:id Update plan DELETE /api/payments/admin/plans/:id Delete plan POST /api/payments/admin/plans/:id/sync-stripe Sync plan to Stripe GET /api/payments/admin/subscriptions All subscriptions with filters POST /api/payments/admin/subscriptions/:id/cancel Cancel subscription GET /api/payments/admin/products All products POST /api/payments/admin/products Create product PUT /api/payments/admin/products/:id Update product DELETE /api/payments/admin/products/:id Delete product POST /api/payments/admin/products/:id/sync-stripe Sync product to Stripe GET /api/payments/admin/orders List orders POST /api/payments/admin/orders/:id/refund Refund order GET /api/payments/admin/donations List donations GET /api/payments/admin/export CSV export of completed orders"},{"location":"docs/api/#media-api-fastify-port-4100","title":"Media API (Fastify \u2014 Port 4100)","text":"The Media API is a separate Fastify server sharing the same PostgreSQL database. It handles all video-related functionality.
"},{"location":"docs/api/#health","title":"Health","text":"Method Path Auth Description GET/health Media API health check"},{"location":"docs/api/#videos-admin","title":"Videos (Admin)","text":"Prefix: /api/videos \u00b7 Auth: Admin roles
/api/videos List videos (?limit=&offset=&search=&orientation=&producers=&isShort=) GET /api/videos/producers Distinct producer list GET /api/videos/health Video count health check GET /api/videos/:id Single video detail PATCH /api/videos/:id Update metadata (title, producer, tags, quality, etc.) POST /api/videos/:id/publish Publish to category POST /api/videos/:id/unpublish Unpublish POST /api/videos/bulk-publish Bulk publish POST /api/videos/bulk-unpublish Bulk unpublish POST /api/videos/:id/lock Lock published video POST /api/videos/:id/unlock Unlock video POST /api/videos/:id/generate-thumbnail Generate thumbnail via FFmpeg POST /api/videos/bulk-generate-thumbnails Bulk thumbnail generation"},{"location":"docs/api/#upload","title":"Upload","text":"Method Path Description POST /api/videos/upload Single video upload (multipart, 10 GB limit, streams to disk) POST /api/videos/upload/batch Batch upload (returns 207 multi-status)"},{"location":"docs/api/#actions","title":"Actions","text":"Method Path Description POST /api/videos/:id/duplicate Duplicate video record POST /api/videos/:id/replace Replace video file, keep metadata GET /api/videos/:id/analytics Detailed analytics (?startDate=&endDate=) POST /api/videos/:id/reset-analytics Reset all analytics GET /api/videos/:id/preview-link Generate 24-hour JWT preview link GET /api/videos/analytics/top Top videos (?metric=views|watchTime&limit=) GET /api/videos/analytics/overview Global analytics overview"},{"location":"docs/api/#scheduling","title":"Scheduling","text":"Method Path Description POST /api/videos/:id/schedule-publish Schedule future publish ({publishAt, timezone?}) POST /api/videos/:id/schedule-unpublish Schedule future unpublish DELETE /api/videos/:id/schedule/:action Cancel scheduled operation GET /api/videos/schedules/upcoming Upcoming scheduled operations GET /api/videos/:id/schedule-history Schedule history for video GET /api/videos/schedules/stats Schedule queue statistics POST /api/videos/schedules/pause Pause schedule queue POST /api/videos/schedules/resume Resume schedule queue POST /api/videos/schedules/cleanup Clean old completed jobs"},{"location":"docs/api/#video-fetch","title":"Video Fetch","text":"Method Path Description POST /api/videos/fetch Submit fetch job ({urls: string[]}, 1\u201320 URLs) GET /api/videos/fetch/jobs List recent fetch jobs GET /api/videos/fetch/jobs/:jobId Job detail + log GET /api/videos/fetch/jobs/:jobId/log SSE log stream (Redis pub/sub) DELETE /api/videos/fetch/jobs/:jobId Cancel fetch job"},{"location":"docs/api/#streaming-public","title":"Streaming (Public)","text":"Prefix: /api/videos
/api/videos/stream/health Streaming health check GET /api/videos/:id/stream Optional HTTP range-supporting video stream GET /api/videos/:id/thumbnail Optional Serve thumbnail image GET /api/videos/:id/metadata Public video metadata for embedding Note
Admins can stream unpublished videos by providing a valid JWT.
"},{"location":"docs/api/#public-gallery","title":"Public Gallery","text":"Prefix: /api/public
/api/public Optional Published videos (?limit=&offset=&search=&sort=recent|popular|oldest&category=) GET /api/public/categories Optional Categories with video counts GET /api/public/producers Optional Published producers GET /api/public/:id Optional Single published video GET /api/public/:id/thumbnail Optional Published thumbnail GET /api/public/:id/stream Optional Published video stream"},{"location":"docs/api/#tracking","title":"Tracking","text":"Prefix: /api/track \u00b7 Auth: None required
/api/track/health Tracking health check POST /api/track/view Record video view (returns {viewId}) POST /api/track/event Record play/pause/seek/complete event POST /api/track/heartbeat Update watch time (10s interval, sendBeacon) POST /api/track/batch Batch up to 50 tracking events Tracking is GDPR-compliant IP addresses are hashed with a daily-rotating salt. Raw IPs are never stored. Tracking data is retained for 90 days.
"},{"location":"docs/api/#reactions","title":"Reactions","text":"Prefix: /api/reactions
/api/reactions/config Available reaction types + emoji mappings GET /api/reactions List reactions (?mediaId=&userId=&limit=) GET /api/reactions/:mediaId/chat Reactions in chat timeline format POST /api/reactions Add reaction (30s cooldown per type) Available types: like, love, laugh, wow, sad, angry
/api/public/:id/comments List comments (?limit=&offset=) POST /api/public/:id/comments Optional Create comment (word-filtered; rate limited: 5/min) GET /api/public/:id/chat-stream SSE stream for real-time chat (30s keepalive)"},{"location":"docs/api/#comment-admin","title":"Comment Admin","text":"Prefix: /api/media/admin/comments \u00b7 Auth: Admin roles
/api/media/admin/comments/stats Counts by status GET /api/media/admin/comments All comments with filters PATCH /api/media/admin/comments/:id/approve Approve comment PATCH /api/media/admin/comments/:id/hide Hide comment PATCH /api/media/admin/comments/:id/unhide Unhide comment PUT /api/media/admin/comments/:id/notes Update moderation notes DELETE /api/media/admin/comments/:id Delete comment"},{"location":"docs/api/#word-filters","title":"Word Filters","text":"Prefix: /api/media/admin/word-filters \u00b7 Auth: Admin roles
/api/media/admin/word-filters List filter entries grouped by level POST /api/media/admin/word-filters Add word ({word, level: low|medium|high|custom}) DELETE /api/media/admin/word-filters/:id Remove word"},{"location":"docs/api/#chat-threads-notifications","title":"Chat Threads & Notifications","text":"Auth: Authenticated
Method Path Description GET/api/media/chat/threads Videos with user's comments + unread counts POST /api/media/chat/threads/:mediaId/read Mark thread as read GET /api/media/notifications/stream Per-user SSE notification stream (?token=)"},{"location":"docs/api/#shorts","title":"Shorts","text":"Method Path Auth Description GET /api/shorts Optional Shorts feed (?sort=recent|popular|random) POST /api/shorts/scan Admin Auto-classify short videos by duration"},{"location":"docs/api/#upvotes","title":"Upvotes","text":"Method Path Auth Description POST /api/public/:id/upvote Toggle upvote (session-based via X-Session-ID header) GET /api/public/:id/upvote-status Check upvote status for current session"},{"location":"docs/api/#playlists","title":"Playlists","text":""},{"location":"docs/api/#public_6","title":"Public","text":"Prefix: /api/playlists
/api/playlists/featured Optional Featured playlists GET /api/playlists/popular Optional Popular public playlists (?search=) GET /api/playlists/share/:token Optional Playlist by share token GET /api/playlists/:id Optional Playlist detail (public, owner, or share token) POST /api/playlists/:id/view Optional Record playlist view"},{"location":"docs/api/#user-playlists","title":"User Playlists","text":"Auth: Authenticated
Method Path Description GET/api/playlists/my Own playlists POST /api/playlists Create playlist PUT /api/playlists/:id Update playlist (ownership check) DELETE /api/playlists/:id Delete playlist POST /api/playlists/:id/videos Add video ({mediaId}) DELETE /api/playlists/:id/videos/:mediaId Remove video PUT /api/playlists/:id/videos/reorder Reorder videos POST /api/playlists/:id/share Generate share token DELETE /api/playlists/:id/share Revoke share token"},{"location":"docs/api/#playlist-admin","title":"Playlist Admin","text":"Prefix: /api/media/playlists \u00b7 Auth: Admin roles
/api/media/playlists All playlists GET /api/media/playlists/featured Featured playlists with admin info POST /api/media/playlists/:id/feature Feature a playlist DELETE /api/media/playlists/:id/feature Unfeature a playlist PUT /api/media/playlists/featured/reorder Reorder featured playlists PUT /api/media/playlists/:id Admin update any playlist POST /api/media/playlists/:id/duplicate Duplicate playlist DELETE /api/media/playlists/:id Admin delete any playlist"},{"location":"docs/api/#user-profile","title":"User Profile","text":"Prefix: /api/media/me \u00b7 Auth: Authenticated
/api/media/me/stats User stats + 30-day activity + achievements GET /api/media/me/watch-history Paginated watch history POST /api/media/me/stats/recalculate Recompute stats from raw data GET /api/media/me/settings Privacy settings PUT /api/media/me/settings Update privacy settings PUT /api/media/me/profile Update display name PUT /api/media/me/password Change password"},{"location":"docs/api/#route-summary","title":"Route Summary","text":"API Module Endpoint Count Express Auth 9 Users 7 Dashboard 7 Campaigns (CRUD + public + user + moderation + emails) 16 Responses 10 Email Queue 4 Representatives 7 Locations (CRUD + geocode + import) 21 Cuts 11 Shifts (CRUD + series) 19 Canvassing 20 GPS Tracking 10 Map Settings + Geocoding 3 Pages + Blocks 12 Email Templates 13 QR Codes 1 Site Settings 5 Listmonk 9 Docs Management 11 Services 2 Pangolin 16 Observability 3 Payments (public + admin) 29 Health + Metrics 3 Express Total ~248 Fastify Videos (CRUD + upload + actions + schedule + fetch) 39 Streaming 4 Public Gallery 6 Tracking 5 Reactions 4 Comments + Chat 13 Shorts + Upvotes 4 Playlists (public + user + admin) 18 User Profile 7 Health 1 Fastify Total ~101 Grand Total ~349"},{"location":"docs/architecture/","title":"Architecture","text":"Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database.
Under Construction
Detailed architecture documentation is being written. Check back soon.
"},{"location":"docs/architecture/#system-overview","title":"System Overview","text":"Browser \u2500\u2500\u25ba Nginx (reverse proxy) \u2500\u2500\u252c\u2500\u2500\u25ba Express API (port 4000) \u2500\u2500\u25ba PostgreSQL\n \u251c\u2500\u2500\u25ba Fastify Media API (port 4100) \u2500\u2500\u2518\n \u251c\u2500\u2500\u25ba React Admin GUI (port 3000)\n \u2514\u2500\u2500\u25ba MkDocs / Other Services\n"},{"location":"docs/architecture/#key-components","title":"Key Components","text":"Component Technology Role Main API Express.js + Prisma Auth, campaigns, map, shifts, pages Media API Fastify + Prisma Video library, analytics, uploads Admin GUI React + Ant Design Single-page admin application Database PostgreSQL 16 Shared by both APIs (30+ tables) Cache Redis Rate limiting, job queues, geocoding Proxy Nginx Subdomain routing, security headers"},{"location":"docs/architecture/#authentication-flow","title":"Authentication Flow","text":"This guide covers how to take Changemaker Lite from a local development setup to a publicly accessible production deployment. The main decision is how to expose your services to the internet.
"},{"location":"docs/deployment/#architecture-overview","title":"Architecture Overview","text":"Regardless of which exposure method you choose, the internal architecture is the same:
Internet \u2192 [Your exposure method] \u2192 Nginx (port 80) \u2192 Backend Services\n Nginx handles all subdomain routing internally. Every service is accessed through nginx on port 80, which proxies to the correct container based on the Host header.
app.DOMAIN Admin GUI + public pages 3000 api.DOMAIN Express API 4000 media.DOMAIN Fastify Media API 4100 DOMAIN (root) MkDocs documentation site 4004 db.DOMAIN NocoDB 8091 docs.DOMAIN MkDocs live preview 4003 code.DOMAIN Code Server 8888 git.DOMAIN Gitea 3030 n8n.DOMAIN Workflow automation 5678 home.DOMAIN Homepage dashboard 3010 listmonk.DOMAIN Newsletter manager 9001 mail.DOMAIN MailHog (dev email) 8025 qr.DOMAIN Mini QR generator 8089 draw.DOMAIN Excalidraw whiteboard 8090 vault.DOMAIN Vaultwarden password manager 8445 chat.DOMAIN Rocket.Chat team chat \u2014 events.DOMAIN Gancio event management 8092 grafana.DOMAIN Monitoring dashboards 3005"},{"location":"docs/deployment/#exposure-methods","title":"Exposure Methods","text":""},{"location":"docs/deployment/#pangolin","title":"Option 1: Pangolin + Newt Tunnel (Recommended)","text":"Admin GUI: Tunnel Management Page
The admin dashboard includes a dedicated Tunnel Management page at Admin \u2192 Settings \u2192 Tunnel. This page provides:
If you're unsure about any step above, the Tunnel page walks you through the same process interactively.
Pangolin is a self-hosted tunnel server. The Newt client container runs alongside your stack and establishes an outbound connection to your Pangolin server, which then routes public traffic back through the tunnel. No port forwarding or static IP required.
Advantages:
Requirements:
If you used config.sh, you may have already set these. Otherwise, add to your .env:
PANGOLIN_API_URL=https://api.your-pangolin-server.org/v1\nPANGOLIN_API_KEY=your_api_key_here\nPANGOLIN_ORG_ID=your_org_id\n"},{"location":"docs/deployment/#step-2-create-a-site-in-pangolin","title":"Step 2: Create a Site in Pangolin","text":"Log in to your Pangolin dashboard and create a new site:
changemaker-yourdomain.org)100.90.128.3/24)Save the credentials
The Newt Secret is only shown once during site creation. Copy it immediately.
"},{"location":"docs/deployment/#step-3-update-env-with-site-credentials","title":"Step 3: Update.env with Site Credentials","text":"PANGOLIN_SITE_ID=your_site_id\nPANGOLIN_ENDPOINT=https://your-pangolin-server.org\nPANGOLIN_NEWT_ID=your_newt_id\nPANGOLIN_NEWT_SECRET=your_newt_secret\n"},{"location":"docs/deployment/#step-4-start-the-newt-container","title":"Step 4: Start the Newt Container","text":"docker compose up -d newt\n The Newt container connects to nginx (its only dependency) and establishes the tunnel:
# From docker-compose.yml\nnewt:\n image: fosrl/newt\n container_name: newt-changemaker\n restart: unless-stopped\n environment:\n - PANGOLIN_ENDPOINT=${PANGOLIN_ENDPOINT}\n - NEWT_ID=${PANGOLIN_NEWT_ID}\n - NEWT_SECRET=${PANGOLIN_NEWT_SECRET}\n depends_on:\n - nginx\n Verify the connection:
docker compose logs newt --tail 20\n You should see a successful connection message.
"},{"location":"docs/deployment/#step-5-create-public-http-resources","title":"Step 5: Create Public HTTP Resources","text":"In the Pangolin dashboard, create an HTTP resource for each service you want exposed. All resources point to nginx:80 \u2014 nginx handles the routing internally.
Required resources (minimum for a working deployment):
Resource Name Domain Target Auth Admin GUIapp.yourdomain.org nginx:80 Not Protected API Server api.yourdomain.org nginx:80 Not Protected Public Site yourdomain.org nginx:80 Not Protected Optional resources (add as needed):
Resource Name Domain Target Media APImedia.yourdomain.org nginx:80 NocoDB db.yourdomain.org nginx:80 Documentation docs.yourdomain.org nginx:80 Code Server code.yourdomain.org nginx:80 Gitea git.yourdomain.org nginx:80 Grafana grafana.yourdomain.org nginx:80 Set resources to Not Protected
By default, Pangolin may enable authentication on new resources. This causes 302 redirects to the Pangolin login page instead of reaching your services. Set each resource to Not Protected (public access) unless you intentionally want Pangolin SSO in front of it.
"},{"location":"docs/deployment/#step-6-update-cors-for-production","title":"Step 6: Update CORS for Production","text":"Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000,http://localhost\n Then restart the API:
docker compose restart api\n"},{"location":"docs/deployment/#step-7-verify","title":"Step 7: Verify","text":"# Should return JSON (not a 302 redirect)\ncurl https://api.yourdomain.org/api/health\n\n# Admin GUI should load\ncurl -I https://app.yourdomain.org\n"},{"location":"docs/deployment/#cloudflare","title":"Option 2: Cloudflare Tunnel","text":"Cloudflare Tunnel (cloudflared) provides a similar zero-trust tunnel approach using Cloudflare's network. No port forwarding needed, and you get Cloudflare's CDN and DDoS protection.
Advantages:
Disadvantages:
Create a Cloudflare Tunnel in the Zero Trust dashboard
Add a cloudflared service to your docker-compose.yml:
cloudflared:\n image: cloudflare/cloudflared:latest\n container_name: cloudflared-changemaker\n restart: unless-stopped\n command: tunnel run\n environment:\n - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}\n depends_on:\n - nginx\n networks:\n - changemaker-lite\n Add your tunnel token to .env:
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here\n Configure public hostnames in the Cloudflare dashboard, all pointing to http://nginx:80:
app.yourdomain.org http://nginx:80 api.yourdomain.org http://nginx:80 yourdomain.org http://nginx:80 (add more as needed) http://nginx:80 Start the tunnel:
docker compose up -d cloudflared\n Note
The cloudflared service is not included in the default docker-compose.yml. Add it manually if you choose this method. The Newt service can be removed or left stopped.
If your server has a public IP address (e.g., a VPS or dedicated server), you can point DNS directly to it and use nginx with SSL certificates.
Advantages:
Disadvantages:
Point DNS for your domain and all subdomains to your server's IP:
A yourdomain.org \u2192 YOUR_SERVER_IP\nA *.yourdomain.org \u2192 YOUR_SERVER_IP\n Or use individual A records for each subdomain if your DNS provider doesn't support wildcards.
Open ports 80 and 443 on your server's firewall.
Install Certbot (or another ACME client) for SSL certificates:
# Ubuntu/Debian\nsudo apt install certbot\n\n# Get a wildcard certificate with DNS challenge\nsudo certbot certonly --manual --preferred-challenges dns \\\n -d yourdomain.org -d '*.yourdomain.org'\n Alternatively, use the Certbot Docker image or a Let's Encrypt companion container.
Update nginx to listen on 443 with your certificates. Add an SSL server block to nginx/conf.d/ssl.conf:
server {\n listen 443 ssl;\n server_name app.yourdomain.org;\n\n ssl_certificate /etc/nginx/ssl/fullchain.pem;\n ssl_certificate_key /etc/nginx/ssl/privkey.pem;\n\n location / {\n proxy_pass http://changemaker-v2-admin:3000;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n }\n}\n\n# Repeat for api.yourdomain.org, media.yourdomain.org, etc.\n# Or use a single server block with $host matching\n Mount certificates into the nginx container via docker-compose.yml:
nginx:\n volumes:\n - /etc/letsencrypt/live/yourdomain.org:/etc/nginx/ssl:ro\n Set up auto-renewal with a cron job or systemd timer:
0 3 * * * certbot renew --quiet && docker compose restart nginx\n Traefik alternative
If you prefer automatic SSL and don't want to manage nginx SSL config manually, consider replacing nginx with Traefik. Traefik can auto-discover Docker containers and provision Let's Encrypt certificates automatically. This would require adapting the container labels and removing the nginx service.
"},{"location":"docs/deployment/#tailscale","title":"Option 4: Tailscale / WireGuard (Private Access)","text":"For deployments that should only be accessible to specific people (not the general public), a mesh VPN like Tailscale or plain WireGuard gives you private networking without exposing anything to the internet.
Use cases:
http://100.x.x.x:3000)Note
With private access methods, you may not need subdomain routing at all. Access services directly by port: http://server-ip:3000 (admin), http://server-ip:4000 (API), etc.
Before going live, verify each item:
"},{"location":"docs/deployment/#security","title":"Security","text":"grep -c \"REQUIRED_STRONG\" .env should return 0)NODE_ENV=production set in .envENCRYPTION_KEY set and differs from JWT secretsEMAIL_TEST_MODE=false (unless you want MailHog in production)CORS_ORIGINS includes your production domainREDIS_PASSWORD)curl https://api.yourdomain.org/api/health returns JSONdocker compose ps shows api, admin, v2-postgres, redis, nginx healthydocker compose exec api npx prisma migrate deploydocker compose exec api npx prisma db seedhttps://app.yourdomain.org./scripts/backup.shdocker compose --profile monitoring up -dThe included backup script dumps PostgreSQL databases, archives uploads, and optionally uploads to S3.
"},{"location":"docs/deployment/#running-a-backup","title":"Running a Backup","text":"./scripts/backup.sh\n This creates a timestamped directory under ./backups/ containing:
changemaker_v2.sql.gz \u2014 Main PostgreSQL dump (compressed)listmonk.sql.gz \u2014 Listmonk database dump (if running)uploads.tar.gz \u2014 Media uploads archivemanifest.json \u2014 Backup metadata# Upload to S3 (requires AWS CLI + S3_BUCKET env var)\n./scripts/backup.sh --s3\n\n# Custom retention (delete local backups older than N days)\n./scripts/backup.sh --retention 14\n"},{"location":"docs/deployment/#automated-backups","title":"Automated Backups","text":"Add a cron job for daily backups:
# Edit crontab\ncrontab -e\n\n# Add daily backup at 3 AM\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh >> /var/log/changemaker-backup.log 2>&1\n\n# With S3 upload\n0 3 * * * /path/to/changemaker.lite/scripts/backup.sh --s3 >> /var/log/changemaker-backup.log 2>&1\n"},{"location":"docs/deployment/#restore","title":"Restore","text":"# Restore main database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/changemaker_v2.sql.gz | \\\n docker compose exec -T v2-postgres psql -U changemaker changemaker_v2\n\n# Restore Listmonk database\ngunzip -c backups/changemaker-v2-backup-TIMESTAMP/listmonk.sql.gz | \\\n docker compose exec -T listmonk-db psql -U listmonk listmonk\n\n# Restore uploads\ntar xzf backups/changemaker-v2-backup-TIMESTAMP/uploads.tar.gz -C ./\n"},{"location":"docs/deployment/#monitoring","title":"Monitoring","text":"The monitoring stack runs behind a Docker Compose profile and is not started by default.
"},{"location":"docs/deployment/#starting-the-monitoring-stack","title":"Starting the Monitoring Stack","text":"docker compose --profile monitoring up -d\n This starts:
Service Port Purpose Prometheus 9090 Metrics collection and queries Grafana 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"},{"location":"docs/deployment/#pre-configured-dashboards","title":"Pre-configured Dashboards","text":"Grafana includes 3 auto-provisioned dashboards:
The API exposes 12 custom Prometheus metrics with the cm_ prefix:
cm_api_uptime_seconds \u2014 API uptimecm_email_queue_size \u2014 BullMQ pending emailscm_active_canvass_sessions \u2014 Active canvassing sessionscm_locations_total \u2014 Total locations in databaseapi/src/utils/metrics.tsPre-configured alerts in configs/prometheus/alerts.yml:
# Pull latest code\ngit pull origin v2\n\n# Rebuild and restart containers\ndocker compose build api admin\ndocker compose up -d api admin\n\n# Run any new migrations\ndocker compose exec api npx prisma migrate deploy\n"},{"location":"docs/deployment/#database-migrations","title":"Database Migrations","text":"Always run migrations after pulling updates:
docker compose exec api npx prisma migrate deploy\n Back up first
Always run ./scripts/backup.sh before applying migrations in production. Migrations may alter table structures and are not easily reversible.
Symptom: API returns 302 redirects to the Pangolin authentication page.
Fix: In the Pangolin dashboard, edit each resource and set Authentication to Not Protected.
"},{"location":"docs/deployment/#cors-errors","title":"CORS Errors","text":"Symptom: Browser console shows CORS errors when accessing the production domain.
Fix: Add your production app. subdomain to CORS_ORIGINS in .env, then docker compose restart api.
Check in order:
PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET in .envPANGOLIN_ENDPOINT matches your Pangolin server URLdocker compose logs newt --tail 50docker compose ps nginxdocker compose ps nginxcurl http://localhost:4000/api/healthdocker compose logs nginx --tail 50dig app.yourdomain.org should point to your Pangolin serverSee Troubleshooting for more common issues.
"},{"location":"docs/getting-started/","title":"Getting Started","text":"This guide walks you through installing Changemaker Lite, running your first deployment, and logging into the admin dashboard.
"},{"location":"docs/getting-started/#prerequisites","title":"Prerequisites","text":"git clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n\nbash config.sh\n\ndocker compose up -d\n Open http://localhost:3000 and sign in with the admin email and password you configured. The API container automatically runs database migrations and seeding on first startup \u2014 no manual steps needed.
Change your password
If you used the wizard's generated password, change it immediately from the admin dashboard.
For the full setup walkthrough, see Installation.
"},{"location":"docs/getting-started/#configuration-wizard","title":"Configuration Wizard","text":"The config.sh wizard produces a fully populated .env file in 14 steps:
.env from .env.example (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 .conf.template files with domain substitution 11. Homepage Generates services.yaml 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 See Installation for detailed documentation of each step.
"},{"location":"docs/getting-started/#services","title":"Services","text":"Changemaker Lite includes 30+ Docker services organized into 8 categories:
Category Services Startup Core API, Admin, PostgreSQL, Redis, Nginxdocker compose up -d v2-postgres redis api admin nginx Media Fastify media API docker compose up -d media-api Communication Rocket.Chat, Gancio, Jitsi Meet Individual docker compose up -d commands Newsletter & Email Listmonk, MailHog docker compose up -d listmonk-app Developer Tools Code Server, MkDocs, Gitea, NocoDB, n8n Individual docker compose up -d commands Utilities Mini QR, Excalidraw, Vaultwarden, Homepage docker compose up -d mini-qr excalidraw vaultwarden homepage Monitoring Prometheus, Grafana, Alertmanager, exporters docker compose --profile monitoring up -d Infrastructure Newt tunnel, Docker socket proxy Auto-starts with tunnel configuration See Services Overview for the complete catalog with ports, feature flags, and detailed descriptions.
"},{"location":"docs/getting-started/#next-steps","title":"Next Steps","text":".env referenceThe 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.
Single instance?
If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with First Steps.
"},{"location":"docs/getting-started/control-panel/#when-to-use-ccp","title":"When to Use CCP","text":"CCP is designed for:
CCP handles the entire instance lifecycle: provisioning, configuration, health monitoring, backups, and upgrades \u2014 all from a single dashboard.
"},{"location":"docs/getting-started/control-panel/#architecture","title":"Architecture","text":"CCP runs as 4 Docker containers alongside (but independent from) your CML instances:
\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 Service Container Port Description CCP API ccp-api 5000 Express API with Docker CLI access CCP Admin ccp-admin 5100 React admin GUI CCP PostgreSQL ccp-postgres 5480 CCP metadata database CCP Redis ccp-redis 6399 Rate limiting, caching Each managed CML instance gets its own isolated set of containers and PostgreSQL database, with ports allocated from non-overlapping ranges.
"},{"location":"docs/getting-started/control-panel/#setup","title":"Setup","text":""},{"location":"docs/getting-started/control-panel/#1-run-the-setup-script","title":"1. Run the Setup Script","text":"cd changemaker-control-panel\nchmod +x setup.sh\n./setup.sh\n The setup script:
instances/ and backups/ directories.env.example to .env if not presentINSTANCES_BASE_PATH, BACKUP_STORAGE_PATH, and CML_SOURCE_PATHEdit .env and verify the key settings:
JWT_ACCESS_SECRET Auto-generated JWT signing key JWT_REFRESH_SECRET Auto-generated Refresh token signing key ENCRYPTION_KEY Auto-generated AES-256 key for instance secrets at rest INITIAL_ADMIN_EMAIL admin@example.com Bootstrap admin email INITIAL_ADMIN_PASSWORD ChangeMe2025!! Bootstrap admin password INSTANCES_BASE_PATH ./instances Where instance directories are created CML_SOURCE_PATH Auto-detected Path to CML source repo for provisioning BACKUP_STORAGE_PATH ./backups Backup archive storage PANGOLIN_API_URL \u2014 Pangolin API for tunnel management PANGOLIN_API_KEY \u2014 Pangolin authentication PANGOLIN_ORG_ID \u2014 Pangolin organization"},{"location":"docs/getting-started/control-panel/#3-start-ccp","title":"3. Start CCP","text":"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"},{"location":"docs/getting-started/control-panel/#4-log-in","title":"4. Log In","text":"Open http://localhost:5100 and sign in with the admin credentials from .env.
The Create Instance wizard walks through 5 steps:
"},{"location":"docs/getting-started/control-panel/#step-1-basic-information","title":"Step 1: Basic Information","text":"edmonton), used for directory names and compose projectedmonton.example.org)Toggle which platform features to enable for this instance:
Configure SMTP for the instance, or use MailHog for testing.
"},{"location":"docs/getting-started/control-panel/#step-4-tunnel","title":"Step 4: Tunnel","text":"Optionally configure Pangolin tunnel credentials for public access.
"},{"location":"docs/getting-started/control-panel/#step-5-review","title":"Step 5: Review","text":"Review all settings, then click Create to start provisioning.
"},{"location":"docs/getting-started/control-panel/#provisioning-flow","title":"Provisioning Flow","text":"When you create an instance, CCP runs a 13-step async provisioning process:
Step What Happens 1 Validate uniqueness (slug + domain) 2 Allocate 4 ports from ranges 3 Generate 14 secrets (passwords, JWT keys, encryption keys) 4 Create Instance record (status: PROVISIONING) 5 Create instance directory 6 Copy CML source code (rsync, excluding node_modules/.git/.env) 7 Decrypt secrets and build template context 8 Render 7 config files from Handlebars templates (docker-compose.yml, .env, nginx configs, Pangolin, Prometheus) 9 Copy static files (nginx.conf) 10docker compose pull (non-fatal if images are cached) 11 docker compose build 12 Start infrastructure (PostgreSQL + Redis), wait for healthy 13 Start API (runs migrations + seed), then start all remaining services The admin GUI polls every 3 seconds during provisioning to show progress. When complete, the instance status changes to RUNNING.
"},{"location":"docs/getting-started/control-panel/#port-allocation","title":"Port Allocation","text":"CCP allocates ports from 4 non-overlapping ranges to prevent conflicts between instances:
Range Start End Purpose API 14000 14999 Express API server Admin 13000 13999 React admin GUI PostgreSQL 15400 15499 Database Nginx 10000 10999 Reverse proxyEach new instance receives one port from each range. Ports are tracked in the database and released when instances are deleted.
"},{"location":"docs/getting-started/control-panel/#pages-overview","title":"Pages Overview","text":""},{"location":"docs/getting-started/control-panel/#dashboard","title":"Dashboard","text":"At-a-glance fleet status:
Searchable, filterable table of all instances with status, domain, health, and creation date.
"},{"location":"docs/getting-started/control-panel/#instance-detail","title":"Instance Detail","text":"5-tab view for each instance:
Tab Content Overview Status, domain, ports, features, health summary Services Per-container status grid with restart and log-view actions Logs Real-time log viewer with service filter, tail count, and time range Backups Backup list with create, download, and delete actions Tunnel Pangolin tunnel status and configuration"},{"location":"docs/getting-started/control-panel/#backups","title":"Backups","text":"Cross-instance backup management:
Filterable activity trail with 18 action types:
Each entry includes timestamp, user, action, instance, IP address, and details (expandable JSON).
"},{"location":"docs/getting-started/control-panel/#settings","title":"Settings","text":"CCP-level configuration:
Changemaker Lite uses a single .env file at the project root to configure all services. Copy the example file to get started:
cp .env.example .env\n Security Essentials
REQUIRED_STRONG_PASSWORD_CHANGE_THIS value before starting servicesopenssl rand -hex 32 (or -hex 16 where noted).env to version controlVariables are grouped by service. Each table marks whether a variable is required for a basic deployment or optional (has a sensible default or only needed for specific features).
Symbol Meaning Must be set before first run Has a working default; change for production Feature flag \u2014 opt-in"},{"location":"docs/getting-started/environment-variables/#general","title":"General","text":"Variable Default DescriptionNODE_ENV development Set to production for production deployments. Controls logging, error detail, and security checks. DOMAIN cmlite.org Root domain. Used for nginx subdomain routing (app.DOMAIN, api.DOMAIN, etc.). The root domain serves the MkDocs documentation site; all application routes live under app.DOMAIN. USER_ID 1000 UID for container file ownership. Match your host user's UID (id -u). GROUP_ID 1000 GID for container file ownership. Match your host user's GID (id -g). DOCKER_GROUP_ID 984 GID of the docker group on the host. Needed for containers that access the Docker socket. Find with getent group docker."},{"location":"docs/getting-started/environment-variables/#postgresql-main-database","title":"PostgreSQL (Main Database)","text":"The primary database for both the Express API and the Fastify Media API (shared).
Variable Default DescriptionV2_POSTGRES_USER changemaker Database username. V2_POSTGRES_PASSWORD \u2014 Must change. Database password. V2_POSTGRES_DB changemaker_v2 Database name. V2_POSTGRES_PORT 5433 Host port mapping. The container listens on 5432 internally. Connection string
The DATABASE_URL is constructed automatically inside Docker. If running locally, set:
DATABASE_URL=postgresql://changemaker:YOUR_PASSWORD@localhost:5433/changemaker_v2\n"},{"location":"docs/getting-started/environment-variables/#jwt-authentication","title":"JWT Authentication","text":"Variable Default Description JWT_ACCESS_SECRET \u2014 Secret for signing access tokens. Generate with openssl rand -hex 32. JWT_REFRESH_SECRET \u2014 Secret for signing refresh tokens. Must differ from the access secret. JWT_ACCESS_EXPIRY 15m Access token lifetime. Short-lived by design. JWT_REFRESH_EXPIRY 7d Refresh token lifetime. Tokens are rotated atomically on each refresh."},{"location":"docs/getting-started/environment-variables/#encryption-key","title":"Encryption Key","text":"Variable Default Description ENCRYPTION_KEY \u2014 AES key for encrypting secrets stored in the database (SMTP passwords, API keys, etc.). Generate with openssl rand -hex 32. Must not reuse a JWT secret. Required in production (NODE_ENV=production)."},{"location":"docs/getting-started/environment-variables/#initial-admin-account","title":"Initial Admin Account","text":"These credentials create the first super-admin user during database seeding (npx prisma db seed).
INITIAL_ADMIN_EMAIL admin@cmlite.org Email address for the initial admin. INITIAL_ADMIN_PASSWORD \u2014 Must change. Must be 12+ characters with uppercase, lowercase, and a digit. Change this password after first login."},{"location":"docs/getting-started/environment-variables/#api-server","title":"API Server","text":"Variable Default Description API_PORT 4000 Host port for the Express API. API_URL http://localhost:4000 Public URL of the API. Used for generating links in emails and QR codes. CORS_ORIGINS http://localhost:3000,http://localhost Comma-separated list of allowed CORS origins. Add your production domain (e.g., https://app.yourdomain.org) for production. Production CORS
If you deploy behind a tunnel (Pangolin, Cloudflare) and API requests fail with CORS errors, add your production app. subdomain here:
CORS_ORIGINS=https://app.betteredmonton.org,http://localhost:3000,http://localhost\n Then restart the API: docker compose restart api"},{"location":"docs/getting-started/environment-variables/#admin-gui","title":"Admin GUI","text":"Variable Default Description ADMIN_PORT 3000 Host port for the React admin dashboard. ADMIN_URL http://localhost:3000 Public URL of the admin GUI."},{"location":"docs/getting-started/environment-variables/#nginx-reverse-proxy","title":"Nginx Reverse Proxy","text":"Variable Default Description NGINX_HTTP_PORT 80 HTTP port. All subdomains route through nginx. NGINX_HTTPS_PORT 443 HTTPS port. SSL is typically handled by the tunnel provider (Pangolin/Cloudflare)."},{"location":"docs/getting-started/environment-variables/#redis","title":"Redis","text":"Shared by rate limiting, BullMQ job queues, geocoding cache, and session data.
Variable Default DescriptionREDIS_PASSWORD \u2014 Must change. Redis requires authentication. REDIS_URL redis://:${REDIS_PASSWORD}@redis-changemaker:6379 Full connection URL. Uses the password variable automatically."},{"location":"docs/getting-started/environment-variables/#payments-stripe","title":"Payments (Stripe)","text":"Variable Default Description ENABLE_PAYMENTS false Set to true to enable the payments feature (memberships, products, donations). Stripe API keys are stored encrypted in the database via the admin settings page."},{"location":"docs/getting-started/environment-variables/#email-smtp","title":"Email / SMTP","text":"Variable Default Description SMTP_HOST mailhog-changemaker SMTP server. Default points to the MailHog dev container. SMTP_PORT 1025 SMTP port. 1025 for MailHog, 587 for most production SMTP. SMTP_USER (empty) SMTP username. Not needed for MailHog. SMTP_PASS (empty) SMTP password. SMTP_FROM noreply@cmlite.org \"From\" address on outgoing emails. SMTP_FROM_NAME Changemaker Lite Display name for the \"From\" header. EMAIL_TEST_MODE true When true, all emails go to MailHog instead of real SMTP. Set to false in production. TEST_EMAIL_RECIPIENT admin@cmlite.org Catch-all recipient when test mode is on. Development email
With EMAIL_TEST_MODE=true, all outgoing email is captured in MailHog at http://localhost:8025. No real emails are sent.
Listmonk handles newsletter/marketing campaigns. Sync with the main platform is opt-in.
Variable Default DescriptionLISTMONK_PORT 9001 Listmonk web UI port. LISTMONK_DB_PORT 5434 Listmonk's own PostgreSQL port (separate from the main DB). Uses 5434 to avoid conflict with the main PostgreSQL (5432 internal / 5433 host). LISTMONK_DB_USER listmonk Listmonk database user. LISTMONK_DB_PASSWORD \u2014 Listmonk database password. LISTMONK_DB_NAME listmonk Listmonk database name. LISTMONK_WEB_ADMIN_USER admin Login for the Listmonk web dashboard. LISTMONK_WEB_ADMIN_PASSWORD \u2014 Password for the Listmonk web dashboard. LISTMONK_API_USER v2-api API user for programmatic access (auto-created by init container). LISTMONK_API_TOKEN \u2014 Token for API user. Generate with openssl rand -hex 16. LISTMONK_ADMIN_USER v2-api Same as LISTMONK_API_USER (used by the sync service). LISTMONK_ADMIN_PASSWORD \u2014 Same as LISTMONK_API_TOKEN. LISTMONK_SYNC_ENABLED false Set to true to sync participants/locations/users to Listmonk lists. LISTMONK_WEBHOOK_SECRET (empty) Shared secret for Listmonk webhook callbacks. LISTMONK_PROXY_PORT 9002 Nginx proxy port for Listmonk. Listmonk SMTP settings Listmonk has its own SMTP configuration, separate from the main platform's:
Variable Default DescriptionLISTMONK_SMTP_HOST mailhog-changemaker SMTP host for Listmonk. LISTMONK_SMTP_PORT 1025 SMTP port. LISTMONK_SMTP_USER (empty) SMTP username. LISTMONK_SMTP_PASSWORD (empty) SMTP password. LISTMONK_SMTP_TLS_TYPE none TLS mode: none, STARTTLS, or TLS. LISTMONK_SMTP_FROM Changemaker Lite <noreply@cmlite.org> From address for newsletters."},{"location":"docs/getting-started/environment-variables/#represent-api-canadian-electoral-data","title":"Represent API (Canadian Electoral Data)","text":"Variable Default Description REPRESENT_API_URL https://represent.opennorth.ca OpenNorth Represent API endpoint. Used for postal code \u2192 representative lookups. No API key required."},{"location":"docs/getting-started/environment-variables/#nocodb-data-browser","title":"NocoDB (Data Browser)","text":"Read-only database browser. Useful for inspecting data without SQL.
Variable Default DescriptionNOCODB_V2_PORT / NOCODB_PORT 8091 Host port for the NocoDB web UI. NOCODB_URL http://changemaker-v2-nocodb:8080 Internal Docker URL. NC_ADMIN_EMAIL admin@cmlite.org NocoDB admin email. NC_ADMIN_PASSWORD \u2014 NocoDB admin password."},{"location":"docs/getting-started/environment-variables/#media-manager","title":"Media Manager","text":"Video library with upload, analytics, scheduling, and a public gallery.
Variable Default DescriptionENABLE_MEDIA_FEATURES false Set to true to enable the media system. MEDIA_API_PORT 4100 Fastify media API port. MEDIA_API_PUBLIC_URL http://media-api:4100 Internal URL for the media API container. MEDIA_ROOT /media/library Path to the video library inside the container. MEDIA_UPLOADS /media/uploads Path for upload processing. MAX_UPLOAD_SIZE_GB 10 Maximum single-file upload size in gigabytes. PUBLIC_MEDIA_PORT 3100 Public media gallery server port. VIDEO_PLAYER_DEBUG false Enable verbose video player logging. Analytics & scheduling settings Variable Default Description VIDEO_ANALYTICS_RETENTION_DAYS 90 Days to retain analytics data. GDPR-compliant with IP hashing. VIDEO_ANALYTICS_IP_HASHING_ENABLED true Hash viewer IPs for privacy. VIDEO_SCHEDULE_DEFAULT_TIMEZONE UTC Default timezone for scheduled publishing. VIDEO_SCHEDULE_NOTIFICATION_ENABLED true Notify on scheduled publish/unpublish. VIDEO_PREVIEW_LINK_EXPIRY_HOURS 24 Preview link JWT expiry (hours)."},{"location":"docs/getting-started/environment-variables/#gitea-git-hosting","title":"Gitea (Git Hosting)","text":"Self-hosted Git repository. Optional service.
Variable Default DescriptionGITEA_URL http://gitea-changemaker:3000 Internal container URL for Gitea. GITEA_PORT / GITEA_WEB_PORT 3030 Gitea web UI port. GITEA_SSH_PORT 2222 Gitea SSH port for git operations. GITEA_DB_TYPE mysql Database type (Gitea uses its own MySQL). GITEA_DB_HOST gitea-db:3306 Internal database host. GITEA_DB_NAME gitea Database name. GITEA_DB_USER gitea Database user. GITEA_DB_PASSWD \u2014 Gitea database password. GITEA_DB_ROOT_PASSWORD \u2014 MySQL root password for Gitea. GITEA_ROOT_URL https://git.cmlite.org Public-facing URL for Gitea. GITEA_DOMAIN git.cmlite.org Domain used in git clone URLs. Gitea Docs Comments Enable comments on MkDocs documentation pages, backed by Gitea Issues.
Variable Default DescriptionGITEA_COMMENTS_ENABLED false Enable comments on MkDocs pages. GITEA_API_TOKEN (empty) Personal access token with repo write scope. Create in Gitea \u2192 Settings \u2192 Applications. GITEA_COMMENTS_REPO_OWNER (empty) Gitea username that owns the docs-comments repo. GITEA_COMMENTS_REPO_NAME docs-comments Repository name (auto-created via admin setup). GITEA_OAUTH_CLIENT_ID (empty) OAuth2 application client ID (create in Gitea \u2192 Settings \u2192 Applications \u2192 OAuth2). GITEA_OAUTH_CLIENT_SECRET (empty) OAuth2 application client secret."},{"location":"docs/getting-started/environment-variables/#n8n-workflow-automation","title":"n8n (Workflow Automation)","text":"Variable Default Description N8N_PORT 5678 n8n web UI port. N8N_HOST n8n.cmlite.org Public hostname for n8n. N8N_ENCRYPTION_KEY \u2014 Encryption key for n8n credentials storage. N8N_USER_EMAIL admin@example.com Initial n8n admin email. N8N_USER_PASSWORD \u2014 Initial n8n admin password. GENERIC_TIMEZONE UTC Timezone for n8n cron triggers."},{"location":"docs/getting-started/environment-variables/#mkdocs-documentation","title":"MkDocs (Documentation)","text":"Variable Default Description MKDOCS_PORT 4003 MkDocs dev server port (live preview). MKDOCS_SITE_SERVER_PORT 4004 MkDocs static site server port. BASE_DOMAIN https://cmlite.org Base URL for generated documentation links. MKDOCS_PREVIEW_URL http://mkdocs:8000 Internal container URL. MKDOCS_DOCS_PATH /mkdocs/docs Documentation source directory inside the container."},{"location":"docs/getting-started/environment-variables/#code-server-web-ide","title":"Code Server (Web IDE)","text":"Variable Default Description CODE_SERVER_PORT 8888 Code Server web UI port. CODE_SERVER_URL http://code-server:8080 Internal container URL. USER_NAME coder User account inside the Code Server container."},{"location":"docs/getting-started/environment-variables/#homepage-service-dashboard","title":"Homepage (Service Dashboard)","text":"Variable Default Description HOMEPAGE_PORT 3010 Homepage web UI port. HOMEPAGE_EMBED_PORT 8887 Port for iframe embedding in admin. HOMEPAGE_VAR_BASE_URL http://localhost Base URL used in Homepage service links."},{"location":"docs/getting-started/environment-variables/#mini-qr-qr-code-generator","title":"Mini QR (QR Code Generator)","text":"Variable Default Description MINI_QR_PORT 8089 Mini QR direct access port. MINI_QR_URL http://mini-qr:8080 Internal container URL. MINI_QR_EMBED_PORT 8885 Port for iframe embedding (walk sheets, cut exports)."},{"location":"docs/getting-started/environment-variables/#excalidraw-whiteboard","title":"Excalidraw (Whiteboard)","text":"Variable Default Description EXCALIDRAW_PORT 8090 Excalidraw web UI port. EXCALIDRAW_URL http://excalidraw-changemaker:80 Internal container URL. EXCALIDRAW_EMBED_PORT 8886 Port for iframe embedding. EXCALIDRAW_WS_URL wss://draw.cmlite.org WebSocket URL for real-time collaboration."},{"location":"docs/getting-started/environment-variables/#vaultwarden-password-manager","title":"Vaultwarden (Password Manager)","text":"Self-hosted Bitwarden-compatible password manager. Optional service.
Variable Default DescriptionVAULTWARDEN_PORT 8445 Vaultwarden web UI port. VAULTWARDEN_URL http://vaultwarden-changemaker:80 Internal container URL. VAULTWARDEN_EMBED_PORT 8890 Port for iframe embedding in admin. VAULTWARDEN_ADMIN_TOKEN (empty) Admin panel token (access at /admin). Generate with openssl rand -hex 32. VAULTWARDEN_DOMAIN https://vault.cmlite.org Public-facing URL. Must use HTTPS \u2014 Bitwarden web vault enforces HTTPS for account creation. Set to your Pangolin tunnel URL. VAULTWARDEN_SIGNUPS_ALLOWED false Allow new user self-registration. Keep false and use admin panel invites. VAULTWARDEN_WEBSOCKET_ENABLED true Enable WebSocket notifications for real-time sync. VAULTWARDEN_SMTP_SECURITY off SMTP security mode: off for MailHog, starttls or force_tls for production. Uses the main SMTP_* variables for host/credentials. Initial setup
The vaultwarden-init container automatically invites the INITIAL_ADMIN_EMAIL user when starting. Check MailHog (or your SMTP) for the invitation email.
Self-hosted team chat for volunteer coordination. Requires MongoDB (auto-configured).
Variable Default DescriptionENABLE_CHAT false Set to true to enable the Rocket.Chat integration. The initial default; once saved in admin Settings, the DB value is authoritative. ROCKETCHAT_ADMIN_USER rcadmin Rocket.Chat admin username. ROCKETCHAT_ADMIN_PASSWORD \u2014 Rocket.Chat admin password. ROCKETCHAT_URL http://rocketchat-changemaker:3000 Internal container URL. ROCKETCHAT_EMBED_PORT 8891 Port for iframe embedding in admin."},{"location":"docs/getting-started/environment-variables/#gancio-event-management","title":"Gancio (Event Management)","text":"Self-hosted event management platform. Uses the shared PostgreSQL database (auto-created by init-gancio-db.sh).
GANCIO_PORT 8092 Gancio web UI port. GANCIO_URL http://gancio-changemaker:13120 Internal container URL. GANCIO_EMBED_PORT 8892 Port for iframe embedding in admin. GANCIO_BASE_URL https://events.cmlite.org Public-facing URL for Gancio. Used in event links. GANCIO_ADMIN_USER admin Gancio admin username for shift-to-event sync (OAuth login). GANCIO_ADMIN_PASSWORD \u2014 Gancio admin password. GANCIO_SYNC_ENABLED false Set to true to enable automatic shift \u2192 Gancio event synchronization."},{"location":"docs/getting-started/environment-variables/#jitsi-meet-video-conferencing","title":"Jitsi Meet (Video Conferencing)","text":"Self-hosted video conferencing with JWT authentication. Integrates with Rocket.Chat for in-channel video calls.
Variable Default DescriptionENABLE_MEET false Set to true to enable the Jitsi Meet integration. The initial default; once saved in admin Settings, the DB value is authoritative. JITSI_APP_ID changemaker JWT application ID. Must match across Jitsi Prosody, Rocket.Chat app settings, and JWT_ACCEPTED_ISSUERS/JWT_ACCEPTED_AUDIENCES. JITSI_APP_SECRET \u2014 JWT secret for signing Jitsi tokens. Generate with openssl rand -hex 32. Shared between Jitsi Prosody, Rocket.Chat, and the API. JITSI_JICOFO_AUTH_PASSWORD \u2014 Internal XMPP password for Jicofo (conference focus). Generate with openssl rand -hex 16. JITSI_JVB_AUTH_PASSWORD \u2014 Internal XMPP password for JVB (video bridge). Generate with openssl rand -hex 16. JITSI_EMBED_PORT 8893 Port for iframe embedding in admin. JITSI_URL http://jitsi-web-changemaker:80 Internal container URL. JVB_ADVERTISE_IP (empty) Server's public IP address. Required in production for NAT traversal so remote participants can connect. JVB_PORT 10000 UDP port for media traffic. Must be open in your firewall. Production requirements
JVB_ADVERTISE_IP must be set to your server's public IP for calls to work outside the local network.10000/udp must be open in your firewall for media traffic.Send SMS messages via an Android phone running the Termux API server. The phone acts as an SMS gateway.
Variable Default DescriptionENABLE_SMS false Set to true to enable SMS campaigns. The initial default; once saved in admin Settings, the DB value is authoritative. TERMUX_API_URL http://10.0.0.193:5001 URL of the Termux API server running on the Android phone. TERMUX_API_KEY (empty) API key for authenticating with the Termux server (HMAC auth via X-API-Key header). SMS_DELAY_BETWEEN_MS 3000 Delay between sending individual SMS messages (ms). Prevents carrier throttling. SMS_MAX_RETRIES 3 Maximum retry attempts for failed SMS sends. SMS_RESPONSE_SYNC_INTERVAL_MS 30000 How often to poll the phone's inbox for responses (ms). SMS_DEVICE_MONITOR_INTERVAL_MS 30000 How often to check device health \u2014 battery, connectivity (ms). GUI configuration
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.
"},{"location":"docs/getting-started/environment-variables/#mailhog-development-email","title":"MailHog (Development Email)","text":"Variable Default DescriptionMAILHOG_SMTP_PORT 1025 SMTP port for capturing emails. MAILHOG_WEB_PORT 8025 Web UI to view captured emails."},{"location":"docs/getting-started/environment-variables/#nar-national-address-register","title":"NAR (National Address Register)","text":"Canadian address data import for geographic canvassing.
Variable Default DescriptionNAR_DATA_DIR /data Path to extracted NAR data inside the container. Expects YYYYMM/Addresses/ and YYYYMM/Locations/ subdirectories. Mount via ./data:/data:ro in Docker Compose. Download NAR data from Statistics Canada.
"},{"location":"docs/getting-started/environment-variables/#geocoding","title":"Geocoding","text":"Multi-provider geocoding for address resolution. Works out of the box with free providers; optional paid providers improve accuracy.
Variable Default DescriptionMAPBOX_API_KEY (empty) Mapbox API key for improved geocoding accuracy. Free tier: 100k requests/month. Sign up. GEOCODING_RATE_LIMIT_MS 1100 Delay between requests to free providers (ms). Respects rate limits. GEOCODING_CACHE_ENABLED true Enable Redis-backed geocoding cache. GEOCODING_CACHE_TTL_HOURS 24 Cache lifetime in hours. GOOGLE_MAPS_API_KEY (empty) Google Maps API key. Most accurate but $0.005/request after free tier. GOOGLE_MAPS_ENABLED false Enable Google Maps as a geocoding provider. GEOCODING_PARALLEL_ENABLED true Enable parallel geocoding for bulk imports (~10x speedup). GEOCODING_BATCH_SIZE 10 Number of concurrent geocoding requests during bulk operations. BULK_GEOCODE_ENABLED true Enable bulk re-geocoding from the admin UI. BULK_GEOCODE_MAX_BATCH 5000 Maximum locations per bulk geocoding run."},{"location":"docs/getting-started/environment-variables/#overpass-area-import","title":"Overpass / Area Import","text":"OpenStreetMap data import for map enrichment.
Variable Default DescriptionOVERPASS_API_URL https://overpass-api.de/api/interpreter Overpass API endpoint. Use a private instance for heavy usage. OVERPASS_MIN_DELAY_MS 30000 Minimum delay between requests (ms). The public API requires 30 seconds. AREA_IMPORT_MAX_GRID_POINTS 500 Maximum reverse-geocode grid points per area import."},{"location":"docs/getting-started/environment-variables/#pangolin-tunnel","title":"Pangolin Tunnel","text":"Expose services to the internet without port forwarding, using a self-hosted Pangolin instance.
Variable Default DescriptionPANGOLIN_API_URL https://api.bnkserve.org/v1 Pangolin server API endpoint. PANGOLIN_API_KEY (empty) API key for Pangolin management. PANGOLIN_ORG_ID (empty) Organization ID in Pangolin. PANGOLIN_SITE_ID (empty) Site ID (populated after setup via admin GUI). PANGOLIN_ENDPOINT https://pangolin.bnkserve.org Pangolin tunnel endpoint. PANGOLIN_NEWT_ID (empty) Newt client ID (populated after setup). PANGOLIN_NEWT_SECRET (empty) Newt client secret (populated after setup). Setup flow
Configure the tunnel from Admin \u2192 Settings \u2192 Pangolin. The setup wizard walks you through creating a site, copying credentials, and connecting the Newt container. See Deployment for the full guide.
"},{"location":"docs/getting-started/environment-variables/#monitoring","title":"Monitoring","text":"These services are behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Variable Default Description PROMETHEUS_PORT 9090 Prometheus web UI / query port. GRAFANA_PORT 3005 Grafana dashboard port. GRAFANA_ADMIN_PASSWORD admin Change in production. GRAFANA_ROOT_URL http://localhost:3005 Public URL for Grafana (used in links). CADVISOR_PORT 8086 cAdvisor container metrics port. NODE_EXPORTER_PORT 9100 Prometheus node exporter port. REDIS_EXPORTER_PORT 9121 Redis metrics exporter port. ALERTMANAGER_PORT 9093 Alertmanager web UI port. GOTIFY_PORT 8889 Gotify push notification port. GOTIFY_ADMIN_USER admin Gotify admin username. GOTIFY_ADMIN_PASSWORD admin Change in production. GRAFANA_EMBED_PORT 8894 Port for iframe embedding Grafana in admin. ALERTMANAGER_EMBED_PORT 8895 Port for iframe embedding Alertmanager in admin."},{"location":"docs/getting-started/environment-variables/#bunker-ops-fleet-management","title":"Bunker Ops (Fleet Management)","text":"Remote metrics push for managing multiple Changemaker Lite instances from a central monitoring server.
Variable Default DescriptionINSTANCE_LABEL (empty) Unique label for this instance (used as a Prometheus metric label). Falls back to DOMAIN if empty. BUNKER_OPS_ENABLED false Enable remote metrics push to a central VictoriaMetrics server. BUNKER_OPS_REMOTE_WRITE_URL (empty) VictoriaMetrics remote_write endpoint (e.g., https://ops.example.com/api/v1/write)."},{"location":"docs/getting-started/environment-variables/#generating-secrets","title":"Generating Secrets","text":"Use these commands to generate all required secrets at once:
# JWT secrets (two separate values)\necho \"JWT_ACCESS_SECRET=$(openssl rand -hex 32)\"\necho \"JWT_REFRESH_SECRET=$(openssl rand -hex 32)\"\n\n# Encryption key (must differ from JWT secrets)\necho \"ENCRYPTION_KEY=$(openssl rand -hex 32)\"\n\n# Database and Redis passwords\necho \"V2_POSTGRES_PASSWORD=$(openssl rand -hex 24)\"\necho \"REDIS_PASSWORD=$(openssl rand -hex 24)\"\n\n# Listmonk\necho \"LISTMONK_DB_PASSWORD=$(openssl rand -hex 24)\"\necho \"LISTMONK_WEB_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\nLISTMONK_TOKEN=$(openssl rand -hex 16)\necho \"LISTMONK_API_TOKEN=$LISTMONK_TOKEN\"\necho \"LISTMONK_ADMIN_PASSWORD=$LISTMONK_TOKEN\"\n\n# Supporting services\necho \"GITEA_DB_PASSWD=$(openssl rand -hex 24)\"\necho \"GITEA_DB_ROOT_PASSWORD=$(openssl rand -hex 24)\"\necho \"N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)\"\necho \"N8N_USER_PASSWORD=$(openssl rand -hex 16)\"\necho \"NC_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\necho \"INITIAL_ADMIN_PASSWORD=$(openssl rand -base64 18)\"\n\n# Vaultwarden\necho \"VAULTWARDEN_ADMIN_TOKEN=$(openssl rand -hex 32)\"\n\n# Rocket.Chat\necho \"ROCKETCHAT_ADMIN_PASSWORD=$(openssl rand -hex 16)\"\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 Tip
Copy the output and paste the values into your .env file. The INITIAL_ADMIN_PASSWORD uses base64 encoding to ensure it contains uppercase, lowercase, and digits (meeting the password policy).
For a basic deployment with campaigns, map, and admin:
Required variablesV2_POSTGRES_PASSWORD=...\nREDIS_PASSWORD=...\nJWT_ACCESS_SECRET=...\nJWT_REFRESH_SECRET=...\nENCRYPTION_KEY=...\nINITIAL_ADMIN_PASSWORD=...\n Start servicesdocker compose up -d v2-postgres redis api admin\n For the complete platform including media, newsletters, monitoring, and all services:
Additional variables needed# Everything above, plus:\nENABLE_MEDIA_FEATURES=true\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=...\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 Start servicesdocker compose up -d\ndocker compose --profile monitoring up -d\n"},{"location":"docs/getting-started/features/","title":"Features at a Glance","text":"Changemaker Lite bundles advocacy campaigns, geographic mapping, volunteer management, media hosting, and landing pages into a single self-hosted platform. Every feature can be toggled on or off from Settings in the admin panel.
"},{"location":"docs/getting-started/features/#core-features","title":"Core Features","text":"Advocacy Campaigns
Help supporters contact elected representatives through email campaigns with postal code lookup and a public response wall.
Campaign guide
Map & Canvassing
Manage locations, draw canvassing territories, schedule volunteer shifts, and run GPS-tracked door-to-door outreach.
Map guide
Media Manager
Upload videos and photos, curate playlists, publish a shorts feed, and track engagement with built-in analytics.
Media guide
Landing Pages
Build campaign microsites with a drag-and-drop GrapesJS visual editor and publish at custom slugs.
Landing pages guide
Payments (Stripe)
Accept memberships, product sales, and donations with encrypted Stripe integration and branded donation pages.
Payments guide
SMS Campaigns
Text message outreach via a Termux Android bridge with contact lists, templates, and response tracking.
SMS guide
Public Homepage
Customizable landing page with hero section, live stats, featured campaigns, upcoming shifts, and activity feed.
Homepage guide
Newsletter (Listmonk)
Opt-in mailing lists and newsletter campaigns with automatic subscriber sync from shifts and contacts.
Newsletter guide
Email Templates
Reusable email templates with variable substitution for campaign communications.
Email templates guide
Team Chat (Rocket.Chat)
Self-hosted team chat with iframe integration, floating widget, and native mobile app support.
Chat guide
Video Conferencing (Jitsi)
Self-hosted video calls integrated with Rocket.Chat via JWT authentication \u2014 no separate login required.
Video conferencing guide
Events (Gancio)
Self-hosted event management with automatic shift-to-event sync and an embeddable calendar widget.
Events guide
Password Manager (Vaultwarden)
Bitwarden-compatible password vault for secure team credential sharing.
Password manager guide
User Provisioning
Automatic account creation and sync across Rocket.Chat, Gitea, Vaultwarden, and Listmonk.
User provisioning guide
People / Contacts
Centralized contact management for supporters, donors, and community members with cross-module linking.
People guide
Whiteboard (Excalidraw)
Self-hosted collaborative whiteboard for brainstorming, planning, and visual collaboration.
Whiteboard guide
Social Connections
Friend system, activity feed, groups, profiles, pokes, and privacy controls for volunteer community building.
Social guide
Achievements & Leaderboard
Badge system with 11 achievements across 4 categories, progress tracking, and competitive leaderboards.
Achievements guide
Volunteer Quick Join
QR code invite links for instant volunteer onboarding \u2014 scan, fill a short form, and start canvassing.
Quick join guide
Email Automation
Automated volunteer lifecycle emails \u2014 thank-you notes, shift reminders, weekly summaries, and re-engagement campaigns.
Automation guide
Data Quality Dashboard
Geocoding quality metrics with per-provider stats, confidence tiers, and coverage analysis.
Data quality guide
Documentation Analytics
Page view tracking and engagement metrics for MkDocs documentation pages.
Docs analytics guide
Docs Comments
Gitea-backed comment system for documentation pages with anonymous posting and moderation.
Docs comments guide
Command Palette
Global Ctrl+K search across pages, campaigns, locations, users, settings, and media.
Command palette guide
Navigation Settings
Customize the public navigation menu with feature toggles, custom links, and drag-and-drop reordering.
Navigation guide
Platform Settings
Five-tab settings page covering organization details, theme colors, email configuration, feature flags, and notifications.
Settings guide
Social Sharing (OG Tags)
Open Graph meta tags for campaigns, landing pages, and gallery videos \u2014 rich link previews on social media.
OG sharing guide
Gallery Ads
Internal ad system with 5 ad types, audience targeting, scheduling, frequency caps, and CTR analytics.
Gallery ads guide
Self-Service Contact Profile
Token-based public profile pages where contacts can view and update their information and preferences.
Contact profile guide
You've installed Changemaker Lite \u2014 here's what to do next.
"},{"location":"docs/getting-started/first-steps/#1-log-in","title":"1. Log In","text":"Open the admin panel at http://localhost:3000 (or app.DOMAIN in production) and sign in with the admin email and password you configured during setup.
Change your password
If you used the wizard's generated password, change it immediately from Settings > Organization.
"},{"location":"docs/getting-started/first-steps/#2-explore-the-dashboard","title":"2. Explore the Dashboard","text":"The dashboard gives you an at-a-glance view of platform activity. Initially it will be empty \u2014 that's normal.
"},{"location":"docs/getting-started/first-steps/#3-configure-settings","title":"3. Configure Settings","text":"Visit Settings (/app/settings) to:
Go to Campaigns (/app/campaigns) and click Create Campaign:
/campaignsGo to Locations (/app/map) and add addresses:
Go to Shifts (/app/map/shifts) and create your first volunteer shift:
/shifts) with volunteersShare the shifts page link or generate QR codes for in-person events. Volunteers sign up with just an email address.
"},{"location":"docs/getting-started/first-steps/#next-steps","title":"Next Steps","text":"Changemaker Lite runs as a set of Docker containers orchestrated by Docker Compose. The config.sh wizard handles all configuration \u2014 or you can set things up manually.
# Clone the repository\ngit clone https://gitea.bnkops.com/admin/changemaker.lite\ncd changemaker.lite\ngit checkout v2\n\n# Run the configuration wizard\nbash config.sh\n\n# Start all services\ndocker compose up -d\n Open http://localhost:3000 and sign in with the admin credentials you configured. Database migrations and seeding run automatically on first startup.
Change your password
If you used the wizard's generated password, change it immediately from the admin dashboard.
"},{"location":"docs/getting-started/installation/#configuration-wizard-configsh","title":"Configuration Wizard (config.sh)","text":"The wizard performs 14 steps to produce a fully configured .env file and prepare the system for startup. Each step is interactive with sensible defaults.
Verifies that Docker, Docker Compose v2, and OpenSSL are installed. Exits immediately if any are missing, with links to installation guides.
"},{"location":"docs/getting-started/installation/#step-2-environment-file-setup","title":"Step 2: Environment File Setup","text":".env exists, copies .env.example as the starting point.env already exists, offers to back it up (timestamped copy) and create a fresh one, or update values in placePrompts for your root domain (default: cmlite.org) and derives 14 environment variables from it:
DOMAIN example.org BASE_DOMAIN https://example.org GITEA_ROOT_URL https://git.example.org GITEA_DOMAIN git.example.org N8N_HOST n8n.example.org SMTP_FROM noreply@example.org INITIAL_ADMIN_EMAIL admin@example.org NC_ADMIN_EMAIL admin@example.org EXCALIDRAW_WS_URL wss://draw.example.org LISTMONK_SMTP_FROM Changemaker Lite <noreply@example.org> HOMEPAGE_VAR_BASE_URL https://example.org VAULTWARDEN_DOMAIN https://vault.example.org GANCIO_BASE_URL https://events.example.org TEST_EMAIL_RECIPIENT admin@example.org Also updates mkdocs/mkdocs.yml with the new site_url and repo_url, and asks whether this is a production deployment (sets NODE_ENV=production).
Prompts for the initial super-admin email and password. The password is validated against the security policy:
Auto-generates 21 unique secrets \u2014 no placeholder passwords remain after this step:
Category Count Secrets JWT & Encryption 3JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY (64-char hex) Database 2 V2_POSTGRES_PASSWORD, REDIS_PASSWORD (24-char alphanumeric) Listmonk 3 LISTMONK_DB_PASSWORD, LISTMONK_WEB_ADMIN_PASSWORD, LISTMONK_API_TOKEN NocoDB 1 NC_ADMIN_PASSWORD Gitea 2 GITEA_DB_PASSWD, GITEA_DB_ROOT_PASSWORD n8n 2 N8N_ENCRYPTION_KEY, N8N_USER_PASSWORD Monitoring 2 GRAFANA_ADMIN_PASSWORD, GOTIFY_ADMIN_PASSWORD Vaultwarden 1 VAULTWARDEN_ADMIN_TOKEN (64-char hex) Rocket.Chat 1 ROCKETCHAT_ADMIN_PASSWORD Gancio 1 GANCIO_ADMIN_PASSWORD Jitsi Meet 3 JITSI_APP_SECRET (64-char hex), JITSI_JICOFO_AUTH_PASSWORD, JITSI_JVB_AUTH_PASSWORD"},{"location":"docs/getting-started/installation/#step-6-email-configuration","title":"Step 6: Email Configuration","text":"Choose between:
http://localhost:8025 for developmentEnable or disable 9 optional platform features:
Flag Environment Variable What It Enables Media ManagerENABLE_MEDIA_FEATURES=true Video library, analytics, scheduled publishing Listmonk Sync LISTMONK_SYNC_ENABLED=true Newsletter subscriber sync from platform participants Payments ENABLE_PAYMENTS=true Stripe-based products, donations, and plans Rocket.Chat ENABLE_CHAT=true Team communication platform Gancio Events GANCIO_SYNC_ENABLED=true Shift-to-event sync with Gancio Jitsi Meet ENABLE_MEET=true Video conferencing (also prompts for server public IP) SMS Campaigns ENABLE_SMS=true Termux Android bridge for SMS (also prompts for API URL) Docs Comments GITEA_COMMENTS_ENABLED=true Gitea-backed page comments on documentation Bunker Ops BUNKER_OPS_ENABLED=true Fleet metrics push to central server (also prompts for remote write URL)"},{"location":"docs/getting-started/installation/#step-8-tunnel-configuration-pangolin","title":"Step 8: Tunnel Configuration (Pangolin)","text":"Optionally configures Pangolin tunnel credentials for secure public access:
PANGOLIN_API_URL \u2014 API endpoint (default: https://api.bnkserve.org/v1)PANGOLIN_API_KEY \u2014 Authentication keyPANGOLIN_ORG_ID \u2014 Organization identifierComplete tunnel setup is done from the admin GUI at Settings > Tunnel after services are running.
"},{"location":"docs/getting-started/installation/#step-9-cors-origins","title":"Step 9: CORS Origins","text":"Automatically calculates allowed origins from your domain:
http://app.DOMAIN,https://app.DOMAIN,http://DOMAIN,https://DOMAIN,http://localhost:3000,http://localhost,http://localhost:4003\n"},{"location":"docs/getting-started/installation/#step-10-nginx-config-generation","title":"Step 10: Nginx Config Generation","text":"Renders all .conf.template files in nginx/conf.d/ by substituting ${DOMAIN} with your configured domain. This produces the nginx configuration files that handle subdomain routing.
Generates configs/homepage/services.yaml with 27 service entries (both production and local development URLs) for the Homepage service dashboard.
Creates and sets permissions (775) on 12 directories needed by containers:
Directory Purposeconfigs/code-server/.config Code Server configuration configs/code-server/.local Code Server local data mkdocs/.cache MkDocs build cache mkdocs/site MkDocs built site output assets/uploads Listmonk uploads assets/images Shared images assets/icons Homepage icons media/local/inbox Media upload inbox media/local/thumbnails Video thumbnails media/public Public media files local-files n8n local files data NAR import data"},{"location":"docs/getting-started/installation/#step-13-upgrade-watcher-optional","title":"Step 13: Upgrade Watcher (Optional)","text":"Installs a systemd path watcher that enables the admin GUI's \"Check for Updates\" and \"Start Upgrade\" buttons. This step requires sudo and is optional \u2014 you can install it later or use the CLI upgrade script directly.
The watcher installs two systemd units:
changemaker-upgrade.path \u2014 watches for data/upgrade/trigger.jsonchangemaker-upgrade.service \u2014 runs scripts/upgrade-watcher.sh when triggeredDisplays a configuration summary showing all choices made, then prints startup commands.
"},{"location":"docs/getting-started/installation/#what-gets-modified","title":"What Gets Modified","text":"After the wizard completes, the following files have been created or modified:
File Action.env Created (or updated) with all configuration values mkdocs/mkdocs.yml Updated site_url and repo_url with domain nginx/conf.d/*.conf Generated from .conf.template files configs/homepage/services.yaml Generated with all service URLs 12 directories Created with container-friendly permissions systemd units (optional) Installed to /etc/systemd/system/"},{"location":"docs/getting-started/installation/#manual-setup-alternative","title":"Manual Setup (Alternative)","text":"If you prefer to configure by hand instead of using the wizard:
cp .env.example .env\n At minimum, set these required secrets:
# 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)\nENCRYPTION_KEY=$(openssl rand -hex 32)\n Set your admin credentials (password must meet the 12+ char complexity requirement):
INITIAL_ADMIN_EMAIL=admin@yourdomain.org\nINITIAL_ADMIN_PASSWORD=YourStrongPassword1\n Then configure optional sections:
DOMAIN and all derived variables (see Step 3 table above)SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_TEST_MODE=falsePANGOLIN_API_URL, PANGOLIN_API_KEY, PANGOLIN_ORG_IDSee Environment Variables for every available option.
"},{"location":"docs/getting-started/installation/#full-stack-startup","title":"Full Stack Startup","text":"After configuration, start the entire platform:
docker compose up -d\n That's it. Docker handles the startup order automatically:
Watch the startup progress:
docker compose logs -f api --tail 20\n Once you see Starting server on port 4000, open http://localhost:3000 and log in.
The monitoring stack (Prometheus, Grafana, Alertmanager) uses a Docker Compose profile and isn't included by default:
docker compose --profile monitoring up -d\n"},{"location":"docs/getting-started/installation/#start-only-core-services","title":"Start Only Core Services","text":"If you prefer a minimal startup (lower resource usage):
docker compose up -d v2-postgres redis api admin nginx\n Manual migrations
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:
cd api && npx prisma migrate deploy && npx prisma db seed\n See Services Overview for the complete service catalog.
"},{"location":"docs/getting-started/installation/#verifying-installation","title":"Verifying Installation","text":"After starting services, verify everything is healthy:
# 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 You should see the API return {\"status\":\"ok\"} and all started containers in a \"running\" state.
.env referenceChangemaker Lite runs as 30+ Docker containers orchestrated by Docker Compose. This page catalogs every service, organized by category.
Quick reference
Use docker compose ps to see which services are currently running, or docker compose ps -a to include stopped containers.
These services form the minimum viable platform. Start them first.
Container Port Descriptionchangemaker-v2-api 4000 Express.js REST API (Prisma ORM) changemaker-v2-admin 3000 React admin GUI (Vite + Ant Design) changemaker-v2-postgres 5433 PostgreSQL 16 \u2014 primary database redis-changemaker 6379 Redis 7 \u2014 cache, rate limiting, job queues changemaker-v2-nginx 80 Nginx reverse proxy \u2014 subdomain routing # 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 The API container automatically runs database migrations and seeding on startup via its entrypoint script.
Note
Nginx is technically optional for local development (you can access services directly by port), but required for production subdomain routing.
"},{"location":"docs/getting-started/services/#media","title":"Media","text":"Container Port Description Feature Flagchangemaker-media-api 4100 Fastify media API \u2014 video library, analytics, scheduling ENABLE_MEDIA_FEATURES=true docker compose up -d media-api\n 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.
"},{"location":"docs/getting-started/services/#communication","title":"Communication","text":""},{"location":"docs/getting-started/services/#rocketchat-team-chat","title":"Rocket.Chat (Team Chat)","text":"Container Port Description Feature Flagrocketchat-changemaker 8891 Rocket.Chat server ENABLE_CHAT=true mongodb-changemaker \u2014 MongoDB (Rocket.Chat data store) \u2014 nats-changemaker \u2014 NATS (Rocket.Chat message bus) \u2014 docker compose up -d rocketchat mongodb nats\n"},{"location":"docs/getting-started/services/#gancio-events","title":"Gancio (Events)","text":"Container Port Description Feature Flag gancio-changemaker 8092 Gancio event platform GANCIO_SYNC_ENABLED=true gancio-init \u2014 Init container \u2014 creates Gancio database \u2014 docker compose up -d gancio\n Init containers
gancio-init 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.
jitsi-web-changemaker 8893 Jitsi web interface ENABLE_MEET=true jitsi-prosody-changemaker \u2014 XMPP server (Prosody) \u2014 jitsi-jicofo-changemaker \u2014 Jitsi conference focus \u2014 jitsi-jvb-changemaker 10000/udp Jitsi video bridge \u2014 docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb\n Firewall requirement
Jitsi requires UDP port 10000 open in your firewall for video/audio media traffic. Set JVB_ADVERTISE_IP in .env to your server's public IP address.
listmonk-app 9001 Listmonk newsletter platform LISTMONK_SYNC_ENABLED=true listmonk-db 5432 PostgreSQL (Listmonk's own database) \u2014 listmonk-init \u2014 Init container \u2014 creates API user \u2014 mailhog-changemaker 8025 MailHog email capture (development) EMAIL_TEST_MODE=true # Newsletter platform\ndocker compose up -d listmonk-app\n\n# Email testing (captures all outgoing emails)\ndocker compose up -d mailhog\n Listmonk has its own PostgreSQL instance separate from the main database. The listmonk-init container auto-creates the API user for platform integration.
code-server-changemaker 8888 VS Code in the browser mkdocs-changemaker 4003 MkDocs live preview (hot reload) mkdocs-site-server-changemaker 4004 MkDocs static site server gitea-changemaker 3030 Gitea \u2014 self-hosted Git repository gitea-db \u2014 PostgreSQL (Gitea's database) changemaker-v2-nocodb 8091 NocoDB \u2014 read-only database browser nocodb-init \u2014 Init container \u2014 registers database n8n-changemaker 5678 n8n \u2014 workflow automation # 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 Tip
mkdocs (port 4003) provides live editing with hot reload for documentation authors. mkdocs-site-server (port 4004) serves the built static site for production visitors.
mini-qr 8089 QR code PNG generator excalidraw-changemaker 8090 Collaborative whiteboard vaultwarden-changemaker 8445 Vaultwarden \u2014 Bitwarden-compatible password manager vaultwarden-init \u2014 Init container \u2014 configures admin settings homepage-changemaker 3010 Homepage \u2014 service dashboard docker compose up -d mini-qr excalidraw vaultwarden homepage\n Mini QR is used internally by walk sheets and cut export pages to generate printable QR codes.
"},{"location":"docs/getting-started/services/#monitoring-docker-profile","title":"Monitoring (Docker Profile)","text":"Monitoring services are behind a Docker Compose profile and are not started by default.
Container Port Descriptionprometheus-changemaker 9090 Prometheus \u2014 metrics collection grafana-changemaker 3005 Grafana \u2014 monitoring dashboards alertmanager-changemaker 9093 Alertmanager \u2014 alert routing cadvisor-changemaker 8086 cAdvisor \u2014 container metrics node-exporter-changemaker 9100 Node Exporter \u2014 host system metrics redis-exporter-changemaker 9121 Redis Exporter \u2014 Redis metrics gotify-changemaker 8889 Gotify \u2014 push notifications # Start the entire monitoring stack\ndocker compose --profile monitoring up -d\n The monitoring stack includes 3 pre-configured Grafana dashboards and 12 custom cm_* Prometheus metrics. See Monitoring for details.
newt \u2014 Pangolin tunnel connector (Newt) docker-socket-proxy \u2014 Docker socket proxy for secure container access # Newt starts automatically if PANGOLIN_NEWT_ID and PANGOLIN_NEWT_SECRET are set\ndocker compose up -d newt\n The Newt container connects to a Pangolin tunnel server for secure public access without opening inbound ports. See Tunnel for setup.
"},{"location":"docs/getting-started/services/#subdomain-routing","title":"Subdomain Routing","text":"When Nginx is running, services are accessible via subdomains. The root domain serves documentation only; all application routes are at app.DOMAIN.
app.DOMAIN Admin (3000) All application routes (admin, public pages, campaigns, map, shifts, media gallery) api.DOMAIN Express API (4000) REST API media.DOMAIN Fastify Media API (4100) Media API DOMAIN MkDocs Static (4004) Documentation / marketing site db.DOMAIN NocoDB (8091) Database browser docs.DOMAIN MkDocs Live (4003) Live documentation preview code.DOMAIN Code Server (8888) Web IDE n8n.DOMAIN n8n (5678) Workflow automation git.DOMAIN Gitea (3030) Git hosting home.DOMAIN Homepage (3010) Service dashboard grafana.DOMAIN Grafana (3005) Metrics visualization listmonk.DOMAIN Listmonk (9001) Newsletter platform qr.DOMAIN Mini QR (8089) QR code generator draw.DOMAIN Excalidraw (8090) Collaborative whiteboard vault.DOMAIN Vaultwarden (8445) Password manager events.DOMAIN Gancio (8092) Event platform chat.DOMAIN Rocket.Chat (8891) Team chat meet.DOMAIN Jitsi Meet (8893) Video conferencing mail.DOMAIN MailHog (8025) Email capture (dev)"},{"location":"docs/getting-started/services/#init-containers","title":"Init Containers","text":"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.
Init Container Purposelistmonk-init Creates the Listmonk API user for platform integration gancio-init Creates the Gancio database in the shared PostgreSQL instance vaultwarden-init Configures Vaultwarden admin settings nocodb-init Registers the main database with NocoDB for browsing Seeing these containers in a \"stopped\" or \"exited (0)\" state is completely normal.
"},{"location":"docs/getting-started/services/#starting-everything","title":"Starting Everything","text":"To start all services at once (excluding monitoring):
docker compose up -d\n To start everything including monitoring:
docker compose up -d && docker compose --profile monitoring up -d\n To see what's running:
docker compose ps\n Warning
Starting all services at once requires at least 4 GB RAM. For resource-constrained environments, start only the services you need.
"},{"location":"docs/getting-started/services/#next-steps","title":"Next Steps","text":".env referenceChangemaker 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.
There are two ways to upgrade:
./scripts/upgrade.sh directly from the command lineBoth methods execute the same 6-phase upgrade process.
"},{"location":"docs/getting-started/upgrades/#prerequisites","title":"Prerequisites","text":""},{"location":"docs/getting-started/upgrades/#upgrade-watcher-required-for-gui-method","title":"Upgrade Watcher (Required for GUI Method)","text":"The admin GUI triggers upgrades via a systemd path watcher that monitors for trigger files. This must be installed on the host system.
Install during initial setup:
The config.sh wizard offers to install the watcher automatically (Step 13). If you skipped it, install manually:
# Edit the systemd units to set your project path and user\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" scripts/systemd/changemaker-upgrade.path > /tmp/changemaker-upgrade.path\nsed -e \"s|__PROJECT_DIR__|$(pwd)|g\" -e \"s|__USER__|$(whoami)|g\" scripts/systemd/changemaker-upgrade.service > /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 Verify it's running:
sudo systemctl status changemaker-upgrade.path\n How the watcher works
The API container writes a trigger.json file to a shared data/upgrade/ volume. The systemd path watcher detects the file and runs scripts/upgrade-watcher.sh 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.
/app/settings)The system fetches from the git remote and shows:
The GUI polls for progress updates and displays the current phase, percentage, and status message in real time.
"},{"location":"docs/getting-started/upgrades/#the-6-upgrade-phases","title":"The 6 Upgrade Phases","text":"Both the GUI and CLI methods execute the same 6-phase process:
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 Runsscripts/backup.sh (pg_dump + archive), backs up user-modifiable content, saves pre-upgrade commit hash 3 30% Code Update Saves user paths, stashes local changes, git pull, pops stash with auto-conflict resolution, detects new .env variables 4 50% Container Rebuild Rebuilds api, admin, media-api; conditionally rebuilds nginx and code-server 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"},{"location":"docs/getting-started/upgrades/#what-gets-preserved","title":"What Gets Preserved","text":"The upgrade script automatically preserves user-modifiable paths that you may have customized:
Path What It Containsmkdocs/docs/ Your documentation content mkdocs/mkdocs.yml MkDocs configuration mkdocs/site/ Built documentation site configs/ Prometheus, Grafana, Alertmanager, Homepage configs nginx/conf.d/services.conf Custom nginx service proxies These files are saved before git pull and unconditionally restored afterward, even if the pull introduces changes to them. Your versions always win.
Tip
The .env file is never touched by git pull (it's in .gitignore). However, if new environment variables are added in .env.example, the upgrade script automatically appends them to your .env with their default values and warns you to review them.
Run the upgrade script directly:
./scripts/upgrade.sh\n"},{"location":"docs/getting-started/upgrades/#options","title":"Options","text":"Flag Description --skip-backup Skip the backup phase (requires --force) --pull-services Also pull new third-party Docker images --dry-run Show what would happen without executing --force Continue past non-critical warnings --branch BRANCH Git branch to pull (default: current branch) --rollback Rollback to pre-upgrade commit --api-mode Write progress/result JSON for admin GUI (used internally)"},{"location":"docs/getting-started/upgrades/#examples","title":"Examples","text":"# 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# Rollback to the last pre-upgrade state\n./scripts/upgrade.sh --rollback\n"},{"location":"docs/getting-started/upgrades/#rollback","title":"Rollback","text":""},{"location":"docs/getting-started/upgrades/#automatic-rollback","title":"Automatic Rollback","text":"If the upgrade fails at any phase, the script prints detailed rollback instructions including the pre-upgrade commit hash. Use the --rollback flag:
./scripts/upgrade.sh --rollback\n This:
git-commit.txt inside the archiveWarning
--rollback 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.
# 1. Restore code\ncd /path/to/changemaker.lite\ngit checkout <pre-upgrade-commit-hash>\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/<backup>.tar.gz -C /tmp\ngunzip -c /tmp/<backup>/v2-postgres.sql.gz | \\\n docker exec -i changemaker-v2-postgres psql -U changemaker -d changemaker_v2\n"},{"location":"docs/getting-started/upgrades/#new-environment-variables","title":"New Environment Variables","text":"When upstream code adds new environment variables to .env.example, the upgrade script automatically:
.env.example against your .env[WARN] New env vars added to .env (review defaults):\n NEW_FEATURE_FLAG\n NEW_API_KEY\n Always review new variables after an upgrade \u2014 some may need manual configuration.
"},{"location":"docs/getting-started/upgrades/#update-checker","title":"Update Checker","text":"A separate lightweight script checks for available updates without performing any changes:
./scripts/upgrade-check.sh\n This writes data/upgrade/status.json with:
The admin GUI reads this file to display update availability.
"},{"location":"docs/getting-started/upgrades/#troubleshooting","title":"Troubleshooting","text":""},{"location":"docs/getting-started/upgrades/#stale-progress-indicator","title":"Stale Progress Indicator","text":"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.
To manually clear:
rm -f data/upgrade/progress.json\n"},{"location":"docs/getting-started/upgrades/#merge-conflicts","title":"Merge Conflicts","text":"If git pull 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.
The upgrade script uses .upgrade.lock to prevent concurrent upgrades. If a previous upgrade crashed without cleaning up:
# Verify no upgrade is actually running\nps aux | grep upgrade.sh\n\n# Remove stale lock\nrm -f .upgrade.lock\n"},{"location":"docs/getting-started/upgrades/#health-check-failures","title":"Health Check Failures","text":"If Phase 6 health checks fail, services may still be starting. Wait 1-2 minutes and check manually:
# 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"},{"location":"docs/getting-started/upgrades/#systemd-watcher-not-triggering","title":"Systemd Watcher Not Triggering","text":"# 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"},{"location":"docs/services/","title":"Services","text":"Changemaker Lite orchestrates 20+ services via Docker Compose. This page is your map to every service: what it does, how to reach it, and where to find its upstream documentation.
"},{"location":"docs/services/#core-platform","title":"Core Platform","text":"The essential services that power the application.
Express API
Main V2 API server. Handles authentication, campaigns, map, shifts, pages, email, and all business logic. Prisma ORM with PostgreSQL.
Port: 4000 \u00b7 Container: changemaker-v2-api
API Reference
Fastify Media API
Video library server. Upload, metadata extraction (FFprobe), analytics, scheduled publishing, and public gallery. Shares the same PostgreSQL database.
Port: 4100 \u00b7 Container: changemaker-media-api
Media Guide
Admin GUI
React single-page application (Vite + Ant Design + Zustand). Serves the admin dashboard, public campaign pages, volunteer portal, and media gallery \u2014 all from one build.
Port: 3000 \u00b7 Container: changemaker-v2-admin
Feature Guides
PostgreSQL 16
Primary database shared by both APIs. Managed by Prisma migrations. Contains 30+ tables covering users, campaigns, locations, shifts, media, and more.
Port: 5433 (host) / 5432 (container) \u00b7 Container: changemaker-v2-postgres
PostgreSQL Docs
Redis
In-memory store for rate limiting, BullMQ job queues (email, video scheduling), geocoding cache, and session data. Requires authentication.
Port: 6379 \u00b7 Container: redis-changemaker
Redis Docs
Nginx
Reverse proxy handling all subdomain routing (app., api., media., docs., etc.). Includes security headers (HSTS, CSP, Permissions-Policy) and WebSocket support.
Port: 80 / 443 \u00b7 Container: changemaker-v2-nginx
Nginx Docs
Listmonk
Self-hosted newsletter and mailing list manager. Drop-in replacement for Mailchimp. Opt-in sync with the main platform imports participants, locations, and users as subscriber lists.
Port: 9001 \u00b7 Container: listmonk-app \u00b7 Subdomain: listmonk.DOMAIN
Listmonk Docs
MailHog
Email capture for development. All outgoing email is intercepted and displayed in a web UI when EMAIL_TEST_MODE=true. No real emails are sent.
Port: 8025 (web) / 1025 (SMTP) \u00b7 Container: mailhog-changemaker \u00b7 Subdomain: mail.DOMAIN
MailHog GitHub
MkDocs
Material-themed documentation site with full-text search, blog, social cards, and Jinja2 template overrides. Two containers: live preview (dev) and static site (production).
Port: 4003 (dev) / 4004 (static) \u00b7 Container: mkdocs-changemaker \u00b7 Subdomain: docs.DOMAIN
MkDocs Material
Code Server
Full VS Code in the browser. Edit configuration files, templates, and documentation from anywhere without SSH. Supports extensions.
Port: 8888 \u00b7 Container: code-server-changemaker \u00b7 Subdomain: code.DOMAIN
Code Server Docs
NocoDB
Airtable-alternative database browser. Provides a spreadsheet-like interface to browse, filter, sort, and export campaign data. Read-only access to the main database.
Port: 8091 \u00b7 Container: changemaker-v2-nocodb \u00b7 Subdomain: db.DOMAIN
NocoDB Docs
n8n
Visual workflow automation platform. Connect APIs, trigger actions on events, schedule tasks, and build custom integrations \u2014 all without code. 400+ built-in integrations.
Port: 5678 \u00b7 Container: n8n-changemaker \u00b7 Subdomain: n8n.DOMAIN
n8n Docs
Gitea
Self-hosted Git repository hosting. Version control for campaign code, configuration, templates, and documentation. Includes issues, pull requests, and CI/CD.
Port: 3030 (web) / 2222 (SSH) \u00b7 Container: gitea-changemaker \u00b7 Subdomain: git.DOMAIN
Gitea Docs
Mini QR
Lightweight QR code generator. Produces PNG images for walk sheets, campaign materials, and event signage. Embedded in the admin dashboard via iframe.
Port: 8089 \u00b7 Container: mini-qr \u00b7 Subdomain: qr.DOMAIN
Homepage
Service dashboard showing the status of all containers at a glance. Auto-generated services.yaml from config.sh provides both production and local links.
Port: 3010 \u00b7 Container: homepage-changemaker \u00b7 Subdomain: home.DOMAIN
Homepage Docs
Excalidraw
Collaborative whiteboard for brainstorming, diagramming, and visual planning. Real-time collaboration via WebSocket.
Port: 8090 \u00b7 Container: excalidraw-changemaker \u00b7 Subdomain: draw.DOMAIN
Excalidraw
Vaultwarden
Self-hosted Bitwarden-compatible password manager. Secure credential sharing for campaign teams. Requires HTTPS for account creation; local browsing works on HTTP.
Port: 8445 \u00b7 Container: vaultwarden-changemaker \u00b7 Subdomain: vault.DOMAIN
Vaultwarden Wiki
Rocket.Chat
Self-hosted team chat for volunteer coordination. Supports channels, direct messaging, threads, and file sharing. Embeddable in the admin dashboard via iframe. Enable with ENABLE_CHAT=true.
Port: 3000 (internal) \u00b7 Container: rocketchat-changemaker \u00b7 Subdomain: chat.DOMAIN
Rocket.Chat Docs
Jitsi Meet
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 ENABLE_MEET=true.
Containers: jitsi-web, jitsi-prosody, jitsi-jicofo, jitsi-jvb \u00b7 Subdomain: meet.DOMAIN
Setup Guide \u00b7 Jitsi Docs
Gancio
Self-hosted event management platform. Automatic shift-to-event sync (when GANCIO_SYNC_ENABLED=true) publishes shifts as public events. Uses the shared PostgreSQL database. Embeddable calendar widget available for MkDocs pages.
Port: 8092 \u00b7 Container: gancio-changemaker \u00b7 Subdomain: events.DOMAIN
Gancio Docs
Pangolin + Newt
Self-hosted tunnel server with the Newt client container. Exposes your services to the internet without port forwarding. Handles SSL/TLS, works behind CGNAT and double NAT.
Container: newt-changemaker \u00b7 Managed from Admin \u2192 Settings \u2192 Tunnel
Deployment Guide \u00b7 Pangolin GitHub
These services run behind the monitoring Docker Compose profile. Start them with:
docker compose --profile monitoring up -d\n Prometheus
Metrics collection and time-series database. Scrapes 12 custom cm_* application metrics plus container, host, and Redis metrics. Pre-configured alert rules.
Port: 9090 \u00b7 Container: prometheus-changemaker
Prometheus Docs
Grafana
Metrics visualization with 3 auto-provisioned dashboards: API Overview, Infrastructure, and Campaign Activity. Supports custom dashboards and alerting.
Port: 3005 \u00b7 Container: grafana-changemaker \u00b7 Subdomain: grafana.DOMAIN
Grafana Docs
Alertmanager
Alert routing and notification delivery. Receives alerts from Prometheus and dispatches to Gotify, email, or webhooks based on configurable rules.
Port: 9093 \u00b7 Container: alertmanager-changemaker
Alertmanager Docs
cAdvisor
Container resource metrics. Exposes CPU, memory, network, and filesystem usage per container for Prometheus to scrape.
Port: 8086 \u00b7 Container: cadvisor-changemaker
cAdvisor GitHub
Node Exporter
Host system metrics. Reports CPU, memory, disk, and network stats for the underlying server.
Port: 9100 \u00b7 Container: node-exporter-changemaker
Node Exporter
Redis Exporter
Redis metrics for Prometheus. Exposes connection counts, memory usage, command stats, and keyspace info.
Port: 9121 \u00b7 Container: redis-exporter-changemaker
Redis Exporter GitHub
Gotify
Self-hosted push notification server. Receives alerts from Alertmanager and delivers them to mobile/desktop clients.
Port: 8889 \u00b7 Container: gotify-changemaker
Gotify Docs
All services at a glance with their default ports and subdomains.
Service Port Subdomain Docker Profile Express API 4000api. default Media API 4100 media. default Admin GUI 3000 app. default PostgreSQL 5433 \u2014 default Redis 6379 \u2014 default Nginx 80/443 (all) default Listmonk 9001 listmonk. default MailHog 8025 mail. default MkDocs (dev) 4003 docs. default MkDocs (static) 4004 (root) default Code Server 8888 code. default NocoDB 8091 db. default n8n 5678 n8n. default Gitea 3030 git. default Mini QR 8089 qr. default Homepage 3010 home. default Excalidraw 8090 draw. default Vaultwarden 8445 vault. default Rocket.Chat \u2014 chat. default Jitsi Meet \u2014 meet. default Gancio 8092 events. default Newt (tunnel) \u2014 \u2014 default Prometheus 9090 \u2014 monitoring Grafana 3005 grafana. monitoring Alertmanager 9093 \u2014 monitoring cAdvisor 8086 \u2014 monitoring Node Exporter 9100 \u2014 monitoring Redis Exporter 9121 \u2014 monitoring Gotify 8889 \u2014 monitoring Starting services selectively
You don't need to run everything. Start only what you need:
# Core only\ndocker compose up -d v2-postgres redis api admin\n\n# Add nginx for subdomain routing\ndocker compose up -d nginx\n\n# Add monitoring\ndocker compose --profile monitoring up -d\n See Getting Started for the recommended startup order.
"},{"location":"docs/troubleshooting/","title":"Troubleshooting","text":"Common issues and their solutions when running Changemaker Lite.
Under Construction
This troubleshooting guide is being expanded. Check back soon for more solutions.
"},{"location":"docs/troubleshooting/#cors-errors-in-production","title":"CORS Errors in Production","text":"Symptom: Browser console shows CORS errors when accessing production domain.
Fix: Add your production domain to CORS_ORIGINS in .env:
CORS_ORIGINS=https://app.yourdomain.org,http://localhost:3000\n Then restart the API: docker compose restart api
Symptom: All API endpoints return 302 redirects to Pangolin auth page.
Fix: In the Pangolin dashboard, set each resource to \"Not Protected\" (public access).
"},{"location":"docs/troubleshooting/#database-connection-failures","title":"Database Connection Failures","text":"docker compose ps v2-postgresDATABASE_URL in .envdocker compose logs v2-postgres --tail 50docker compose ps redis-changemakerREDIS_PASSWORD and REDIS_URL format in .envdocker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD pingdocker compose logs api --tail 100.env.example)docker compose exec api npx prisma migrate deployThis guide covers everything you can do as a visitor or registered supporter \u2014 from contacting representatives to signing up for volunteer shifts.
"},{"location":"docs/user-guide/#what-you-can-do","title":"What You Can Do","text":"Campaigns
Find active advocacy campaigns, look up your representatives, and send emails.
Map
Explore the interactive map showing locations across your community.
Shifts
Sign up for volunteer shifts and join canvassing teams.
Events
Browse upcoming events and RSVP through the event calendar.
Gallery
Watch campaign videos, browse photos, and explore playlists.
Shop & Pricing
Purchase campaign merchandise or subscribe to a membership plan.
Donations
Support the cause with one-time donations on branded pages.
Your Profile
View and manage your contact profile, preferences, and activity history.
Browse active advocacy campaigns and contact your elected representatives.
"},{"location":"docs/user-guide/campaigns/#how-it-works","title":"How It Works","text":"/campaigns \u2014 see active campaigns with descriptions and email countsEach 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.
"},{"location":"docs/user-guide/campaigns/#submit-your-own-campaign","title":"Submit Your Own Campaign","text":"Registered users can draft and submit their own advocacy campaigns at /campaigns/create. Submissions go through admin review before being published.
/campaigns \u2014 browse active campaigns/campaign/:slug \u2014 take action on a specific campaign/campaign/:slug/responses \u2014 view the response wall/campaigns/create \u2014 submit a user-generated campaign (requires login)/campaigns/mine \u2014 manage your submitted campaigns (requires login)Make one-time contributions on branded donation pages.
"},{"location":"docs/user-guide/donations/#how-it-works","title":"How It Works","text":"/donateEach donation page has:
/donate \u2014 browse donation pages/donate/:slug \u2014 donate on a specific campaign pageIntegrated with Gancio for self-hosted event management. When enabled, volunteer shifts are automatically published as public events.
"},{"location":"docs/user-guide/events/#shift-to-event-sync","title":"Shift-to-Event Sync","text":"When GANCIO_SYNC_ENABLED=true, the platform:
Sync uses OAuth authentication with the Gancio admin account.
"},{"location":"docs/user-guide/events/#key-features","title":"Key Features","text":"enableEvents is enabled in settings/app/gancio \u2014 Gancio service status and iframe embed/events \u2014 public events navigation link (when enabled)events.DOMAIN \u2014 Gancio web interface for browsing and RSVPsThe public gallery at /gallery showcases campaign videos, photos, and curated playlists.
/gallery/shorts/gallery/playlist/:id/gallery \u2014 public video and photo gallery/gallery/watch/:id \u2014 watch a specific video/gallery/playlist/:id \u2014 view a playlist/gallery/shorts \u2014 browse the shorts feedThe public map at /map shows locations across your community on an interactive Leaflet map.
/map \u2014 public interactive mapGive supporters a private, token-based link to view and manage their own contact profile -- no login required.
"},{"location":"docs/user-guide/profile/#how-it-works","title":"How It Works","text":"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.
"},{"location":"docs/user-guide/profile/#public-routes","title":"Public Routes","text":"/profile/:token -- self-service contact profile pageBrowse available volunteer shifts and sign up to participate in canvassing and other campaign activities.
"},{"location":"docs/user-guide/shifts/#signing-up","title":"Signing Up","text":"/shifts to see available time slotsOrganizers may share a QR code at events for instant onboarding:
Quick Join creates a temporary 24-hour account. Your organizer can upgrade it to a permanent account afterward.
"},{"location":"docs/user-guide/shifts/#after-signing-up","title":"After Signing Up","text":"Once you have an account, log in to access the Volunteer Portal where you can:
See the Volunteer Guide for the full volunteer experience.
"},{"location":"docs/user-guide/shifts/#public-routes","title":"Public Routes","text":"/shifts \u2014 browse and sign up for volunteer shifts/join?token=... \u2014 quick join via invite link or QR codeSupport the campaign by purchasing merchandise or subscribing to a membership plan.
"},{"location":"docs/user-guide/shop/#shop","title":"Shop","text":"Browse available products at /shop:
View subscription options at /pricing:
/shop \u2014 browse products/pricing \u2014 view subscription plansWelcome! This guide walks you through everything you need as a campaign volunteer.
"},{"location":"docs/volunteer/#getting-started","title":"Getting Started","text":""},{"location":"docs/volunteer/#1-sign-up-for-a-shift","title":"1. Sign Up for a Shift","text":"Visit the Shifts page (your organizer will share the link, or find it at /shifts). Browse available time slots, pick one that works, and fill in your name and email. You'll receive a confirmation email with login credentials.
Sign in with the email and password from your confirmation at the login page.
"},{"location":"docs/volunteer/#3-explore-the-volunteer-portal","title":"3. Explore the Volunteer Portal","text":"After logging in, you'll land on the volunteer portal. Use the bottom navigation to access:
Tap your name/avatar in the header and select Browse Site to visit the public pages \u2014 campaigns, the public map, and shift signups.
"},{"location":"docs/volunteer/#faq","title":"FAQ","text":"Q: I can't find my assigned area on the map. A: Make sure your shift has an area assigned. Check with your organizer.
Q: My GPS isn't working. A: Allow location access in your browser. Try moving near a window for better signal.
Q: I recorded the wrong outcome. A: Visit the same address again and record the correct outcome. The most recent visit counts.
Q: How do I sign up for more shifts? A: Visit the public shifts page at /shifts.
Recognize volunteer contributions with unlockable achievement badges and competitive leaderboards.
"},{"location":"docs/volunteer/achievements/#how-it-works","title":"How It Works","text":"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.
"},{"location":"docs/volunteer/achievements/#badge-categories","title":"Badge Categories","text":""},{"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"},{"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"},{"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"},{"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"},{"location":"docs/volunteer/achievements/#progress-tracking","title":"Progress Tracking","text":"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).
"},{"location":"docs/volunteer/achievements/#leaderboards","title":"Leaderboards","text":"The Achievements page includes a leaderboard tab with three ranking types:
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.
"},{"location":"docs/volunteer/achievements/#volunteer-stats","title":"Volunteer Stats","text":"The Achievements page also displays aggregate stats for the current user:
/volunteer/achievements \u2014 badge gallery, progress bars, leaderboard tabs, and personal statsThe volunteer canvass map is your main tool for door-to-door outreach \u2014 a full-screen GPS-tracked experience.
"},{"location":"docs/volunteer/canvassing/#the-volunteer-map","title":"The Volunteer Map","text":""},{"location":"docs/volunteer/canvassing/#what-you-see","title":"What You See","text":"The Routes tab shows your past canvassing routes on a map, helping you see which areas you've covered and plan your next outing.
"},{"location":"docs/volunteer/canvassing/#volunteer-routes","title":"Volunteer Routes","text":"/volunteer \u2014 full-screen canvass map with GPS and visit recording/volunteer/activity \u2014 visit history and outcome breakdown/volunteer/routes \u2014 past canvassing routesView your upcoming and past volunteer shifts from the Shifts tab in the bottom navigation.
"},{"location":"docs/volunteer/shifts/#shift-details","title":"Shift Details","text":"Each shift shows:
The Activity tab shows your complete visit history:
/volunteer/shifts \u2014 view assigned shifts/volunteer/activity \u2014 visit history and outcome breakdownConnect with fellow volunteers through friend requests, activity feeds, team groups, and real-time notifications. Enable via Settings > Feature Toggles > Social Connections.
"},{"location":"docs/volunteer/social/#friends","title":"Friends","text":"The Discover page suggests potential friends using a ranked scoring algorithm based on:
The Social Feed at /volunteer/feed shows recent activity from your friends:
Groups are automatically created based on platform activity:
Each volunteer has a social profile showing volunteer stats, achievement badges, friendship status, and recent activity.
"},{"location":"docs/volunteer/social/#pokes","title":"Pokes","text":"Send a friendly nudge to any accepted friend (24-hour cooldown per pair).
"},{"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"},{"location":"docs/volunteer/social/#digest-emails","title":"Digest Emails","text":"Opt into periodic social digest emails with friend activity, unread notifications, and pending requests. Choose daily or weekly frequency.
"},{"location":"docs/volunteer/social/#volunteer-routes","title":"Volunteer Routes","text":"/volunteer/feed \u2014 social activity feed/volunteer/friends \u2014 friends, requests, blocked, and groups/volunteer/discover \u2014 ranked friend suggestions/volunteer/profile \u2014 your social profile/volunteer/profile/:userId \u2014 another volunteer's profile/volunteer/notifications \u2014 notification center and preferences/volunteer/groups/:id \u2014 group detail with member listTesting page.
A test product
+$90.00
++ Secure payment via Stripe. Browse all products +
+❤️
+Every contribution makes a difference. Choose an amount below.
+ +❤️
+Every contribution makes a difference. Choose an amount below.
+ + Custom Amount +❤️
+Your contribution helps us create lasting change in our community.
+ Donate Now +Testing page.
+