40 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Changemaker Lite is a self-hosted political campaign platform built with Docker Compose. It consolidates advocacy email campaigns, geographic mapping, volunteer management, and administration into a single TypeScript stack. The primary domain is cmlite.org.
Current state: V2 rebuild substantially complete (merged to main). Core platform operational with Phases 1-14 complete. See V2_PLAN.md for the full roadmap.
Status Summary:
- ✅ Phases 1-14 Complete (Foundation through Monitoring + DevOps)
- ✅ Drizzle to Prisma Migration Complete (single-ORM, Feb 2026)
- ✅ Automated Pangolin Setup (one-command tunnel deployment)
- ✅ 3 Security Audits Complete (Feb 2025 + Mar 22/27/30 2026)
- ✅ Social Connections + Calendar (friendship, shared views, availability finder)
- ✅ Payments + Ticketed Events (Stripe integration, check-in scanner)
- ✅ Meeting Planner + Straw Polls (scheduling, voting)
- ✅ SMS Campaign Connector (Termux Android bridge)
- ✅ Docs CMS (blog authoring, access policies, collaboration, version history)
- ✅ User Provisioning Framework (Gitea, Vaultwarden, Listmonk)
- ✅ Granular Admin Roles (9 admin roles + module-specific RBAC)
- ✅ Collaborative Docs Editing (Y.js CRDT + Hocuspocus)
- ✅ Engagement Scoring + EventBus + Gitea SSO
- ✅ MCP Server (Claude Code integration, 27 core + 6 on-demand packs (~65 tools))
- 🚧 Phase 15 (Testing + Polish) - Next
V2 Architecture
Stack
- Dual API Architecture
- Express.js API (TypeScript, port 4000) — Main V2 features with Prisma ORM + PostgreSQL 16
- Fastify Media API (TypeScript, port 4100) — Video library with Prisma ORM (shared DB) ✅ Migrated from Drizzle (Feb 2026)
- React Admin GUI — Vite + Ant Design + Zustand, port 3000
- Nginx reverse proxy — subdomain routing (
*.cmlite.org) - NocoDB v2 — read-only data browser on port 8091
- Redis — caching, rate limiting, BullMQ backend, geocoding queue (authenticated)
- Monitoring Stack (Docker profile:
monitoring) — Prometheus, Grafana, Alertmanager, cAdvisor, exporters
Authentication & Security
- JWT-based auth: access tokens (15min) + refresh tokens (7 days, stored in DB)
- Password policy: 12+ characters, uppercase, lowercase, digit (enforced at schema level)
- Initial admin: Configured via
INITIAL_ADMIN_EMAILandINITIAL_ADMIN_PASSWORDenv vars (auto-created during database seeding) - Roles:
SUPER_ADMIN,INFLUENCE_ADMIN,MAP_ADMIN,BROADCAST_ADMIN,CONTENT_ADMIN,MEDIA_ADMIN,PAYMENTS_ADMIN,EVENTS_ADMIN,SOCIAL_ADMIN,USER,TEMP - RBAC:
requireRole(...roles),requireNonTemp,authenticatemiddleware;SUPER_ADMINimplicitly bypasses all role checks - Module-specific role groups (defined in
api/src/utils/roles.ts):INFLUENCE_ROLES,MAP_ROLES,BROADCAST_ROLES,CONTENT_ROLES,MEDIA_ROLES,PAYMENTS_ROLES,EVENTS_ROLES,SOCIAL_ROLES,SYSTEM_ROLES,SCHEDULING_ROLES - User management:
SUPER_ADMINalways; other admins needpermissions.canManageUsers: truefor write operations - Security: See Security & Configuration section below +
SECURITY_AUDIT_2025-02-11.md
Email Systems
- BullMQ — async advocacy email job queue with SMTP
- Listmonk — newsletter/marketing campaigns (opt-in sync via
LISTMONK_SYNC_ENABLED) - MailHog — dev email capture (
EMAIL_TEST_MODE=true)
Directory Structure (Annotated)
changemaker.lite/
├── api/ # Dual API servers (Express + Fastify)
│ ├── prisma/
│ │ ├── schema.prisma # 192 models: User, Campaign, Location, Shift, Payment, Social, etc.
│ │ ├── migrations/ # 50 Prisma migrations (full schema history)
│ │ └── seed.ts # Admin user, settings, page blocks
│ ├── Dockerfile.media # Fastify media server container
│ └── src/
│ ├── server.ts # Express API entry point (port 4000)
│ ├── media-server.ts # Fastify media API entry point (port 4100)
│ ├── config/
│ │ └── env.ts # Zod-validated environment config (100+ vars)
│ ├── middleware/ # auth, rbac, rate-limit, validate, error-handler
│ ├── modules/ # 44 modules total
│ │ ├── auth/ # JWT login, register, refresh, logout
│ │ ├── users/ # User CRUD + pagination + search
│ │ ├── settings/ # Site settings singleton (20+ feature flags)
│ │ ├── services/ # Service health checks
│ │ ├── influence/
│ │ │ ├── campaigns/ # Campaign CRUD + public routes
│ │ │ ├── representatives/ # Represent API integration + cache
│ │ │ ├── responses/ # Response wall + moderation + upvoting
│ │ │ ├── postal-codes/ # Postal code cache service
│ │ │ ├── campaign-emails/ # Email tracking + stats
│ │ │ └── email-queue/ # BullMQ queue admin
│ │ ├── map/
│ │ │ ├── locations/ # Location CRUD + geocoding + NAR import
│ │ │ ├── geocoding/ # Multi-provider geocoding (6 providers)
│ │ │ ├── cuts/ # Polygon CRUD + spatial queries
│ │ │ ├── shifts/ # Shift CRUD + signups
│ │ │ ├── canvass/ # Canvassing sessions + visits + routes
│ │ │ ├── tracking/ # GPS tracking sessions (volunteer + admin routes)
│ │ │ └── settings/ # Map settings singleton
│ │ ├── pages/ # Landing page CRUD + block library + public renderer
│ │ ├── email-templates/ # Email template CRUD + rendering
│ │ ├── media/ # Fastify media API (videos, reactions, jobs, analytics)
│ │ ├── social/ # Friendships, challenges, spotlights, referrals
│ │ ├── calendar/ # Calendar layers, items, shared views, availability
│ │ ├── payments/ # Stripe products, donations, subscriptions
│ │ ├── ticketed-events/ # Event ticketing, tiers, check-in
│ │ ├── sms/ # SMS campaigns via Termux Android bridge
│ │ ├── meeting-planner/ # Meeting scheduling with polls
│ │ ├── meetings/ # Meeting agendas, minutes, action items
│ │ ├── polls/ # Straw polls with comments + voting
│ │ ├── docs/ # MkDocs health checks + export routes
│ │ ├── docs-analytics/ # Docs page view tracking
│ │ ├── docs-comments/ # Gitea-backed comments on docs
│ │ ├── people/ # CRM people module
│ │ ├── events/ # Gancio event integration
│ │ ├── newsletter/ # Newsletter management
│ │ ├── listmonk/ # Newsletter sync admin routes
│ │ ├── pangolin/ # Tunnel management (Newt integration)
│ │ ├── rocketchat/ # Rocket.Chat integration
│ │ ├── jitsi/ # Jitsi video conferencing auth
│ │ ├── registry/ # Docker image registry management
│ │ ├── upgrade/ # Auto-upgrade checks + deployment
│ │ ├── gitea-setup/ # Gitea SSO + API token management
│ │ ├── volunteer-invite/ # Invite codes + setup workflows
│ │ ├── gallery-ads/ # Media gallery ads
│ │ ├── homepage/ # Homepage stats + dashboard
│ │ ├── search/ # Cross-module search
│ │ ├── reports/ # Analytics + reporting
│ │ ├── og/ # Open Graph metadata
│ │ ├── qr/ # QR code PNG generation (public)
│ │ ├── dashboard/ # Admin dashboard data
│ │ ├── activity/ # Activity feed
│ │ └── observability/ # Prometheus/Grafana/Alertmanager integration
│ ├── services/ # email, email-queue, geocode-queue, listmonk, pangolin, docker
│ ├── types/ # express.d.ts (Request augmentation)
│ └── utils/ # logger (Winston), metrics (prom-client), spatial
│
├── admin/ # React Admin (Vite + Ant Design + Zustand)
│ └── src/
│ ├── App.tsx # Main router + route definitions
│ ├── components/
│ │ ├── AppLayout.tsx # Admin sidebar layout
│ │ ├── PublicLayout.tsx # Public dark theme layout
│ │ ├── VolunteerLayout.tsx # Volunteer portal layout
│ │ ├── MediaPublicLayout.tsx # Public media gallery layout
│ │ ├── GrapesJSEditor.tsx # Landing page editor wrapper (forwardRef, Ctrl+S)
│ │ ├── map/ # Leaflet map components + controls + drawing modes
│ │ ├── canvass/ # GPS tracking, markers, route, visit recording
│ │ ├── media/ # VideoCard, BulkActions, gallery components
│ │ ├── email-templates/ # Email template components
│ │ └── observability/ # Monitoring components
│ ├── pages/ # 52 root pages + 8 subdirectories
│ │ ├── influence/ # Campaign moderation, effectiveness, impact stories, straw polls
│ │ ├── map/ # LocationsPage, CutsPage, ShiftsPage, MapSettingsPage, DataQualityDashboard
│ │ ├── media/ # Library, Playlists, Analytics, Gallery Ads, Comment Moderation
│ │ ├── payments/ # Dashboard, Products, Plans, Donations, Subscribers, Settings
│ │ ├── social/ # Dashboard, Graph, Moderation, Referrals, Spotlights, Challenges
│ │ ├── sms/ # Dashboard, Contacts, Campaigns, Conversations, Templates, Setup
│ │ ├── events/ # Ticketed Events, Event Detail, Check-in Scanner
│ │ ├── volunteer/ # Map, Shifts, Routes, Calendar, Friends, Profile, Groups, Achievements
│ │ ├── public/ # Homepage, Campaigns, Map, Events, Media Gallery, Pricing, Donations, Meet
│ │ └── (root) # Dashboard, Users, Settings, Docs*, MeetingPlanner, Observability, etc.
│ ├── stores/ # 9 Zustand stores (auth, canvass, chat-widget, command-palette, favorites, settings, social, tour, tracking)
│ ├── lib/ # api.ts, media-api.ts, media-public-api.ts, nav-defaults.ts, service-url.ts, y-textarea.ts
│ ├── hooks/ # useDebounce, useLocalStorage
│ └── types/ # api.ts, canvass.ts, media.ts (TypeScript interfaces)
│
├── mcp-server/ # Claude Code MCP server (27 core + 6 on-demand packs (~65 tools))
├── nginx/ # Reverse proxy config (subdomain routing + CSP)
├── configs/ # Prometheus, Grafana, Alertmanager, Pangolin configs
├── scripts/ # Deployment, backup, upgrade, registry scripts
│ ├── install.sh # Curl-friendly installer (downloads tarball + runs config.sh)
│ ├── uninstall.sh # Remove containers, volumes, and install dir
│ ├── build-and-push.sh # Build production images → push to Gitea registry
│ ├── build-release.sh # Package runtime files into release tarball
│ ├── mirror-images.sh # Mirror third-party images to Gitea
│ ├── upgrade.sh # 6-phase upgrade (git or release-tarball mode)
│ ├── upgrade-check.sh # Check for updates (git or Gitea API)
│ ├── upgrade-watcher.sh # Systemd bridge for admin GUI upgrades
│ ├── update-env.sh # Merge new variables from .env.example into existing .env
│ ├── backup.sh / restore.sh # PostgreSQL + Listmonk + uploads backup/restore
│ ├── validate-env.sh # Required env variable validation
│ ├── validate-compose-parity.sh # Check docker-compose.yml ↔ docker-compose.prod.yml parity
│ ├── test-deployment.sh # Post-deploy smoke tests (auth, services, health)
│ ├── register-with-ccp.sh # Register instance with a Control Panel via invite code
│ ├── ccp-deregister.sh # Deregister instance from its CCP
│ ├── pangolin-teardown.sh # Delete Pangolin resources/sites (dry-run by default)
│ ├── gitea-init.sh # Bootstrap Gitea admin user + SSO app
│ ├── nocodb-init.sh # Bootstrap NocoDB project + base connection
│ ├── mkdocs-entrypoint.sh # MkDocs container entrypoint (live + built modes)
│ ├── mkdocs-build-trigger.py # Trigger MkDocs rebuild from API hooks
│ ├── legacy/ # Archived Cloudflare tunnel configs (pre-Pangolin)
│ └── systemd/ # Systemd unit files (backup timer, upgrade watcher)
├── docker-compose.yml # V2 orchestration (40+ services)
├── docker-compose.prod.yml # Production (image-only, no source mounts)
├── .env.example # All required environment variables
└── V2_PLAN.md # Full 14-phase roadmap
Quick Start Guide
Pre-built Install (Production — Recommended)
The fastest way to deploy. No source code, no compilation:
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
This downloads a ~9MB release tarball, runs the config wizard, and sets IMAGE_TAG=latest. Then:
cd ~/changemaker.lite && docker compose up -d
Pre-built images are pulled from gitea.bnkops.com/admin (~2 min). Database migrations and seeding run automatically via the API entrypoint. Access the admin GUI at http://localhost:3000.
Source Install (Development)
-
Clone repository:
git clone <repo-url> changemaker.lite cd changemaker.lite -
Create environment file:
cp .env.example .env # Edit .env and set: # - V2_POSTGRES_PASSWORD (strong password) # - REDIS_PASSWORD (strong password) # - JWT_ACCESS_SECRET (openssl rand -hex 32) # - JWT_REFRESH_SECRET (openssl rand -hex 32) # - ENCRYPTION_KEY (openssl rand -hex 32, must differ from JWT secrets) -
Start core services:
docker compose up -d v2-postgres redis api admin -
Run database migrations:
docker compose exec api npx prisma migrate deploy docker compose exec api npx prisma db seed -
Access the application:
- Admin GUI: http://localhost:3000 (see INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD in .env)
- API: http://localhost:4000
- Change default password immediately
Development Workflow
Starting services:
# Core services
docker compose up -d v2-postgres redis api admin
# Include monitoring stack
docker compose --profile monitoring up -d
# Include media API
docker compose up -d media-api
Local development (without Docker):
# Terminal 1: API
cd api && npm install && npm run dev
# Terminal 2: Admin
cd admin && npm install && npm run dev
# Terminal 3 (optional): Media API
cd api && npm run dev:media
Accessing Services
| Service | URL | Default Credentials |
|---|---|---|
| Admin GUI | http://localhost:3000 | See INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD in .env |
| API | http://localhost:4000 | - |
| Media API | http://localhost:4100 | - |
| NocoDB | http://localhost:8091 | See NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD in .env |
| Gitea | http://localhost:3030 | See GITEA_ADMIN_USER/GITEA_ADMIN_PASSWORD in .env |
| MailHog | http://localhost:8025 | - |
| Grafana | http://localhost:3001 | admin / admin |
| Prometheus | http://localhost:9090 | - |
| Listmonk | http://localhost:9001 | See LISTMONK_WEB_ADMIN_USER/PASSWORD in .env |
| Rocket.Chat | http://localhost:3100 | See RC env vars in .env |
| Excalidraw | http://localhost:8090 | - |
| Vaultwarden | http://localhost:8093 | See VAULTWARDEN_ADMIN_TOKEN in .env |
Feature Flags
Most features are toggled via SiteSettings in the database (admin Settings page). Some also have .env overrides:
# .env feature flags (env-level)
ENABLE_MEDIA_FEATURES=true # Media manager
ENABLE_PAYMENTS=true # Stripe integration
ENABLE_SMS=true # SMS campaigns
ENABLE_CHAT=true # Rocket.Chat
ENABLE_MEET=true # Jitsi meetings
LISTMONK_SYNC_ENABLED=true # Newsletter sync
EMAIL_TEST_MODE=true # MailHog vs SMTP
Database feature flags (SiteSettings): enableInfluence, enableMap, enableNewsletter, enableLandingPages, enableMediaFeatures, enablePayments, enableGalleryAds, enableChat, enableEvents, enableDocsComments, enableSms, enablePeople, enableSocial, enableMeet, enableMeetingPlanner, enableTicketedEvents, enableSocialCalendar, enablePolls, enableDocsCollaboration, enableUserProvisioning
Development Commands
The user likes to use Docker - recereating services as if in production.
API Development
cd api && npm run dev # Express dev server (port 4000)
cd api && npm run dev:media # Fastify media dev server (port 4100)
cd api && npx tsc --noEmit # Type-check
cd api && npx prisma migrate dev # Run/create Prisma migrations
cd api && npx prisma studio # Browse database
Admin Development
cd admin && npm run dev # Vite dev server (port 3000)
cd admin && npx tsc --noEmit # Type-check
cd admin && npm run build # Production build
Docker Operations
# Start services
docker compose up -d v2-postgres redis api admin
docker compose up -d media-api
docker compose --profile monitoring up -d
# View logs
docker compose logs -f api
docker compose logs -f media-api
# Database operations
docker compose exec api npx prisma migrate dev
# Stop services
docker compose down
Registry & Release Operations
# Build production images and push to Gitea registry
./scripts/build-and-push.sh --services api,admin,media-api,nginx
./scripts/build-and-push.sh --no-push # Build only, no push (verify)
# Mirror third-party images to Gitea
./scripts/mirror-images.sh # Core images (postgres, redis, etc.)
./scripts/mirror-images.sh --all # Include heavy images (RC, Jitsi, n8n)
# Build release tarball (for pre-built installs — run AFTER build-and-push)
./scripts/build-release.sh --tag v2.1.0 # Creates releases/changemaker-lite-v2.1.0.tar.gz
./scripts/build-release.sh --tag v2.1.0 --upload # Also upload to Gitea Releases API
./scripts/build-release.sh --dry-run # Preview tarball contents
# Use registry images in upgrade (source installs)
./scripts/upgrade.sh --use-registry --force --skip-backup
# Install from tarball (end-user one-liner)
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
Two compose files:
docker-compose.yml— Development: includesbuild:blocks and./api:/appsource mountsdocker-compose.prod.yml— Production:image:only, no source mounts,IMAGE_TAG:-latest
Release tarballs ship docker-compose.prod.yml as the compose file. Source installs use docker-compose.yml.
Note: gitea.bnkops.com must use Pangolin tunnel (not Cloudflare proxy) for large image layers (>100MB). See docs/REGISTRY_GUIDE.md.
Testing & Backup
# Media API tests
cd api && ./test-media-api.sh
# Backup (PostgreSQL + Listmonk + uploads)
./scripts/backup.sh
# Type-check all projects
cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit
API Testing Credentials & Login
Test admin account: admin@bnkops.ca / ChangeMe2025! (SUPER_ADMIN role)
Reliable login method (avoids shell ! escaping issues):
- Write the JSON body to a file using the Write tool (NOT echo/printf — the
!gets backslash-escaped by bash):Write /tmp/login.json → {"email":"admin@bnkops.ca","password":"ChangeMe2025!"} - Use
curl -d @/tmp/login.json:curl -s -X POST http://localhost:4002/api/auth/login \ -H "Content-Type: application/json" -d @/tmp/login.json - Extract token and use for authenticated requests:
TOKEN=$(curl -s -X POST http://localhost:4002/api/auth/login \ -H "Content-Type: application/json" -d @/tmp/login.json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])") curl -s http://localhost:4002/api/some-endpoint -H "Authorization: Bearer $TOKEN"
Port mapping: API container port 4000 → host port 4002, Admin port 3000 → host port 3002
Important: The ! character in ChangeMe2025! triggers bash history expansion. NEVER pass this password directly in bash command strings. Always use the Write-tool-to-file approach above.
Core Modules Reference
Auth & Users
Files:
api/src/modules/auth/— JWT login, register, refresh, logoutapi/src/modules/users/— User CRUD + pagination + searchapi/src/middleware/auth.ts— JWT verification + RBACadmin/src/stores/auth.store.ts— Zustand auth state + token persistenceadmin/src/lib/api.ts— Axios with 401 refresh interceptor
Features: JWT access/refresh tokens, bcrypt passwords (12+ chars), role-based access control, user enumeration prevention, rate limiting
Influence Module (Advocacy Campaigns)
Files:
api/src/modules/influence/campaigns/— Campaign CRUD + public routesapi/src/modules/influence/representatives/— Represent API client + cacheapi/src/modules/influence/responses/— Response wall + moderation + upvotingapi/src/services/email-queue.service.ts— BullMQ queue + workeradmin/src/pages/CampaignsPage.tsx— Campaign managementadmin/src/pages/public/CampaignPage.tsx— Public campaign page
Features: Postal code → representative lookup, email campaigns, response wall with moderation, BullMQ async email queue
Routes:
- Admin:
/app/influence/campaigns,/app/influence/responses,/app/influence/email-queue - Public:
/campaigns,/campaigns/:id,/responses/:campaignId
Map Module (Locations & Canvassing)
Files:
api/src/modules/map/locations/— Location CRUD + geocoding + NAR importapi/src/modules/map/geocoding/geocoding.service.ts— Multi-provider geocoding (6 providers)api/src/modules/map/cuts/— Polygon CRUD + spatial queriesapi/src/modules/map/shifts/— Shift CRUD + signupsapi/src/modules/map/canvass/— Canvassing sessions + visits + routesapi/src/modules/map/tracking/— GPS tracking sessions (volunteer + admin routes)api/src/utils/spatial.ts— Point-in-polygon, haversine, bounds, centroidsadmin/src/pages/LocationsPage.tsx— Location CRUD + CSV + geocodingadmin/src/pages/CutsPage.tsx— Cut table + map drawing editoradmin/src/pages/CanvassDashboardPage.tsx— Admin canvass overviewadmin/src/pages/volunteer/VolunteerMapPage.tsx— Full-screen GPS canvass map
Features: Multi-provider geocoding, NAR 2025 import (Canadian electoral data), polygon cuts, volunteer shifts, canvassing system with GPS tracking, walking route algorithm, printable walk sheets
Routes:
- Admin:
/app/map/locations,/app/map/cuts,/app/map/shifts,/app/canvass/dashboard - Public:
/map,/shifts - Volunteer:
/volunteer/canvass/:cutId,/volunteer/assignments,/volunteer/activity
Landing Pages & Email Templates
Files:
api/src/modules/pages/— Landing page CRUD + block library (3 route files)api/src/modules/email-templates/— Email template CRUD + renderingadmin/src/components/GrapesJSEditor.tsx— GrapesJS wrapper (forwardRef, Ctrl+S)admin/src/pages/PageEditorPage.tsx— Full-screen page editoradmin/src/pages/EmailTemplateEditorPage.tsx— Email template editor
Features: GrapesJS WYSIWYG editor, page/template CRUD, MkDocs export (Jinja2 Material overrides), public renderer, desktop-only editor warning
Routes:
- Admin:
/app/pages,/app/pages/:id/edit,/app/email-templates - Public:
/p/:slug
Media Manager (Dual API)
Files:
api/src/modules/media/— Fastify media API (videos, reactions, jobs, analytics)api/src/modules/media/services/— FFprobe, video analytics serviceapi/src/modules/media/routes/— Video CRUD, actions, schedule, analytics, tracking, uploadapi/src/services/video-schedule-queue.service.ts— BullMQ queue for scheduled publishingadmin/src/lib/media-api.ts— Dedicated axios instance for Media APIadmin/src/pages/media/LibraryPage.tsx— Video library with quick actions + calendaradmin/src/pages/media/AnalyticsDashboardPage.tsx— Global analytics dashboardadmin/src/pages/media/SharedMediaPage.tsx— Public gallery adminadmin/src/pages/public/MediaGalleryPage.tsx— Public video galleryadmin/src/components/media/— VideoCard, VideoActions, modals, charts
Features: Video CRUD with FFprobe metadata, quick actions, scheduled publishing (BullMQ + timezones), analytics (GDPR-compliant), public tracking endpoints, keyboard shortcuts
Routes:
- Admin:
/app/media/library,/app/media/analytics,/app/media/shared,/app/media/jobs - Public:
/gallery,/gallery/watch/:id,/media/:id(legacy) - Public gallery uses
MediaPublicLayout(purple theme, optional auth)
Services & Integrations
Listmonk Newsletter Sync:
api/src/services/listmonk.client.ts— Listmonk REST API client (native fetch)api/src/services/listmonk-sync.service.ts— Sync participants/locations → listsadmin/src/pages/ListmonkPage.tsx— Newsletter sync management- Opt-in sync:
LISTMONK_SYNC_ENABLED=true
Pangolin Tunnel Management:
api/src/services/pangolin.client.ts— Pangolin Integration API clientapi/src/modules/pangolin/pangolin.routes.ts— Tunnel management routes (includes/setup-automated)admin/src/pages/PangolinPage.tsx— Setup wizard + status dashboard + automated setup buttonscripts/register-with-ccp.sh— Register this instance with a Control Panel (CCP) using an invite codescripts/pangolin-teardown.sh— Delete all Pangolin resources/sites for an org (dry-run by default, idempotent)scripts/ccp-deregister.sh— Deregister instance from its CCPconfigs/pangolin/resources.yml— Central resource definitions (12 services)- Newt container integration (Cloudflare alternative)
- Automated setup: One-command deployment via CCP registration (creates site, updates .env, restarts Newt)
- Continuous sync: Hourly resource sync via nginx cron job
MkDocs + Code Server:
api/src/modules/docs/docs.routes.ts— Health checks + export routesadmin/src/pages/DocsPage.tsx— MkDocs export managementadmin/src/pages/CodeEditorPage.tsx— Code Server management- Embedded iframes in admin (CSP
frame-ancestorsfor embedding)
Mini QR Service:
api/src/modules/qr/qr.routes.ts— QR code PNG generation (public, no auth)admin/src/pages/MiniQRPage.tsx— Mini QR iframe- Used by walk sheets + cut exports
Observability & Monitoring
Files:
api/src/modules/observability/observability.routes.ts— Prometheus/Grafana/Alertmanager integrationapi/src/utils/metrics.ts— 12 customcm_*Prometheus metricsadmin/src/pages/ObservabilityPage.tsx— Monitoring dashboard (3 tabs)admin/src/pages/DataQualityDashboardPage.tsx— Geocoding quality metricsconfigs/prometheus/— Scrape targets, alert rulesconfigs/grafana/— 3 pre-configured dashboards
Features: 12 custom cm_* metrics (API uptime, queue size, sessions, etc.), HTTP request metrics, external service health gauges, 3 Grafana dashboards, alert rules, auto-start banner
Routes:
- Admin:
/app/observability,/app/map/data-quality - Direct:
localhost:9090(Prometheus),localhost:3001(Grafana)
Port Reference
| Port | Service | Notes |
|---|---|---|
| Core Services | ||
| 3000 | Admin GUI | Vite dev / React production |
| 4000 | Express API | Main V2 API (Prisma) |
| 4100 | Fastify Media API | Video library (Prisma) |
| 5433 | V2 PostgreSQL | Localhost (container: 5432) |
| 6379 | Redis | Cache, rate limit, BullMQ |
| Supporting Services | ||
| 3001 | Grafana | Metrics visualization |
| 3010 | Homepage | Service dashboard |
| 3030 | Gitea | Git hosting + SSO |
| 3100 | Rocket.Chat | Team chat (embed proxy) |
| 4001 | MkDocs Site | Served docs |
| 4003 | MkDocs Dev | Live preview |
| 5432 | Listmonk PostgreSQL | Listmonk DB |
| 5678 | n8n | Workflow automation |
| 8025 | MailHog | Email capture (dev) |
| 8089 | Mini QR | QR generator |
| 8090 | Excalidraw | Collaborative whiteboard |
| 8091 | NocoDB | Data browser |
| 8092 | Gancio | Event management |
| 8093 | Vaultwarden | Password manager |
| 8443 | Jitsi Web | Video conferencing |
| 8885 | Mini QR Proxy | Iframe-friendly |
| 8888 | Code Server | Web IDE |
| 9001 | Listmonk | Newsletter platform |
Monitoring (profile: monitoring) |
||
| 8080 | cAdvisor | Container metrics |
| 8889 | Gotify | Notifications |
| 9090 | Prometheus | Metrics collection |
| 9093 | Alertmanager | Alert routing |
| 9100 | Node Exporter | Host metrics |
| 9121 | Redis Exporter | Redis metrics |
Nginx Routing
| Subdomain | Target | Purpose |
|---|---|---|
app.cmlite.org |
Admin (3000) | All application routes (admin + public pages, campaigns, map, shifts, media) |
api.cmlite.org |
Express (4000) | Main API |
media.cmlite.org |
Fastify (4100) | Media API |
db.cmlite.org |
NocoDB (8091) | Data browser |
docs.cmlite.org |
MkDocs (4003) | Docs site |
code.cmlite.org |
Code Server (8888) | Web IDE |
n8n.cmlite.org |
n8n (5678) | Workflow automation |
git.cmlite.org |
Gitea (3030) | Git hosting + SSO |
home.cmlite.org |
Homepage (3010) | Dashboard |
grafana.cmlite.org |
Grafana (3001) | Metrics viz |
listmonk.cmlite.org |
Listmonk (9001) | Newsletters |
qr.cmlite.org |
Mini QR (8089) | QR generator |
chat.cmlite.org |
Rocket.Chat (3100) | Team chat |
meet.cmlite.org |
Jitsi (8443) | Video conferencing |
events.cmlite.org |
Gancio (8092) | Event management |
draw.cmlite.org |
Excalidraw (8090) | Collaborative whiteboard |
vault.cmlite.org |
Vaultwarden (8093) | Password manager |
mail.cmlite.org |
MailHog (8025) | Email capture (dev) |
cmlite.org |
MkDocs Static (4004) | Documentation/marketing site only |
Clean separation: Root domain (${DOMAIN}) serves MkDocs documentation site. All application functionality (admin GUI, public campaigns, map, shifts, media gallery) is accessible via app.${DOMAIN} subdomain. This provides clear separation between public documentation and the application.
Common Patterns
Note: Below are the key development patterns for this project.
API Router Structure
- Service layer (
*.service.ts) — business logic, database queries - Routes (
*.routes.ts) — Express router, middleware, validation - Schemas (
*.schemas.ts) — Zod validation schemas - Split admin/public routes when needed (e.g.,
campaigns.routes.ts+campaigns-public.routes.ts)
Authentication Middleware
authenticate— requires any logged-in userrequireRole(...roles)— requires specific role(s)requireNonTemp— blocks TEMP users- Login redirects: ADMIN_ROLES →
/app, USER/TEMP →/volunteer
Frontend Architecture
- Admin pages:
admin/src/pages/+ subdirs (AppLayout) - Public pages:
admin/src/pages/public/(PublicLayout, dark theme) - Volunteer pages:
admin/src/pages/volunteer/(VolunteerLayout) - Zustand stores (9): auth, canvass, chat-widget, command-palette, favorites, settings, social, tour, tracking
- API clients:
{ api }fromlib/api.ts,mediaApifromlib/media-api.ts
Database ORM
- Prisma (both APIs): 192 models in single
schema.prisma. UseUncheckedCreateInput/UncheckedUpdateInputfor foreign keys,Prisma.InputJsonValuefor JSON arrays
Prisma Migration Workflow
- Always use
prisma migrate devfor schema changes (notprisma db push) —db pushapplies changes directly but doesn't create migration files, causing drift - Migration history: 50 migrations in
api/prisma/migrations/fully cover the schema - Production deploys: Use
prisma migrate deploy(notmigrate dev)
Key Gotchas
- Prisma migrations: Never use
db push— alwaysmigrate devto keep history in sync - Nginx media API block must come BEFORE general API block
IMAGE_TAG=local(default) never pulls from registry; set to SHA orlatestfor pre-built images- Release vs source installs: Detected by
VERSIONfile + absence of.git/; release usesdocker-compose.prod.yml, source usesdocker-compose.yml api/dist/is gitignored — never commit; if root-owned from container builds, fix withchown!in passwords triggers bash history expansion — use Write tool to write JSON to file, thencurl -d @file- Port mappings: API container 4000 → host 4002, Admin container 3000 → host 3002
- BullMQ needs its own Redis connections (pass URL string, not shared ioredis instance)
- Public pages use
axiosdirectly (no auth interceptor), admin pages use{ api }from lib - Prisma JSON fields: typed arrays need
as unknown as Prisma.InputJsonValuecast - nginx conf.d files have
.templatecounterparts used by envsubst at startup
Security & Configuration
Security Audits
Four security audits completed. See audit reports for full details:
- Feb 2025: 13 findings (password policy, rate limits, token rotation, XSS prevention).
SECURITY_AUDIT_2025-02-11.md - Mar 22 2026: JWT algorithm lockdown, invite secret separation, webhook hardening, CSV injection, QR DoS
- Mar 27 2026: 33 findings (30 fixed) — IDOR, XSS, path traversal, MongoDB auth, SSTI, open redirect
- Mar 30 2026: 19 findings — IDOR action items/ticketed events, nginx rate limit, JWT secret reuse
Key security features:
- Password policy: 12+ chars, uppercase, lowercase, digit (schema-enforced)
- Rate limits on auth endpoints (10/min per IP) + nginx rate limiting
- Refresh token rotation (atomic Prisma transaction)
- JWT algorithm locked to HS256, separate invite secret
- User enumeration prevention (401 not 404)
- Redis authentication required
- XSS/injection prevention (HTML escaping, DOMPurify, SSTI protection)
- Path traversal protection (resolve + startsWith checks)
- Encryption key for DB secrets (
ENCRYPTION_KEYrequired in all environments) - Nginx security headers (HSTS, Permissions-Policy, CSP, X-Forwarded-For)
- MongoDB keyfile authentication
- httpOnly cookies for refresh tokens
Required Environment Variables
See .env.example for all 100+ variables. Critical ones:
V2_POSTGRES_PASSWORD,REDIS_PASSWORDJWT_ACCESS_SECRET,JWT_REFRESH_SECRETENCRYPTION_KEY(must differ from JWT secrets)LISTMONK_SYNC_ENABLED(opt-in newsletter sync)EMAIL_TEST_MODE(MailHog vs SMTP)ENABLE_MEDIA_FEATURES(media manager)
Production Deployment
- Tunneling: Pangolin with Newt container (Cloudflare alternative)
- SSL/TLS: Handled by tunnel provider (Pangolin/Cloudflare)
- Docker Networking: All containers share
changemaker-litebridge network, reference by container name - Monitoring: Enable with
docker compose --profile monitoring up -d - Backups: Run
./scripts/backup.sh(PostgreSQL + Listmonk + uploads, optional S3 upload)
Production CORS Configuration
When deploying to a production domain via Pangolin tunnel, you MUST update the .env file to include the production domain in CORS_ORIGINS:
# Example for cmlite.org
CORS_ORIGINS=http://app.cmlite.org,https://app.cmlite.org,http://localhost:3000,http://localhost
# Also set production mode
NODE_ENV=production
Without this, API requests from the production domain will fail CORS validation. After updating .env, restart the API container:
docker compose restart api
Troubleshooting
Production 403/302 Errors - Pangolin Resources
Symptom: All API endpoints return 302 redirects to Pangolin authentication page, or 403 Forbidden errors.
Root Cause: Pangolin tunnel resources are configured with authentication enabled (default behavior).
Fix: Log in to your Pangolin dashboard and edit each resource:
- Navigate to Resources → Public
- For each resource (app, api, media, docs, etc.), click Edit
- Change Authentication setting to "Not Protected" (or "Public Access"/"No Authentication")
- Save changes
Critical resources to fix first:
api.${DOMAIN}- Main API (all endpoints fail without this)app.${DOMAIN}- Admin GUI + public pagesmedia.${DOMAIN}- Media API
Verification:
# Should return JSON, NOT a 302 redirect
curl https://api.cmlite.org/api/health
CORS Errors in Production
Symptom: Browser console shows CORS errors when accessing production domain.
Fix: Add production domain to CORS_ORIGINS in .env file (see Production CORS Configuration above).
API Works Locally But Not Via Tunnel
Check in order:
- Newt container running:
docker compose ps newt - Newt connected:
docker compose logs newt --tail 50(should show successful connection) - Environment variables set:
PANGOLIN_SITE_ID,PANGOLIN_NEWT_ID,PANGOLIN_NEWT_SECRETin.env - Pangolin resources configured: All resources set to "Not Protected"
- Nginx running:
docker compose ps nginx
Database/Redis Connection Failures
Check container status (docker compose ps), verify credentials in .env, check logs (docker compose logs <service> --tail 50). Test DB: docker compose exec api npx prisma db execute --stdin <<< "SELECT 1". Test Redis: docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping.
V1 Reference (Legacy)
V1 code has been removed from the repo. History preserved as v1-archive git tag. docker-compose.v1.yml remains as reference only.
Key Configuration Files
Infrastructure
docker-compose.yml— Development orchestration (build blocks + source mounts, 40+ services)docker-compose.prod.yml— Production orchestration (image-only, no source mounts,IMAGE_TAG:-latest).env/.env.example— Environment variables (100+ vars)config.sh— Interactive setup wizard (14 steps, release-mode aware)
Database
api/prisma/schema.prisma— Main schema (192 Prisma models)api/prisma/migrations/— 50 migration files (full schema history)api/prisma/seed.ts— Database seeding
Nginx
nginx/nginx.conf— Global config + security headersnginx/conf.d/default.conf— Subdomain routing (12+ subdomains)nginx/conf.d/api.conf— API reverse proxy (Express + Fastify)nginx/conf.d/services.conf— Service proxies
Monitoring
configs/prometheus/prometheus.yml— Scrape targets + global configconfigs/prometheus/alerts.yml— Alert rules (12 rules)configs/grafana/— 3 pre-configured dashboardsconfigs/alertmanager/alertmanager.yml— Alert routing
Documentation
CLAUDE.md— Project-wide instructions (this file)V2_PLAN.md— Full 14-phase roadmapSECURITY_AUDIT_2025-02-11.md— Initial security audit report.mcp.json— MCP server configuration for Claude Code