37 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 on the v2 branch. 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)
- ✅ Security Audit Complete (13 findings addressed, Feb 2026)
- ✅ NAR 2025 Server Import (Canadian electoral data)
- ✅ Media Manager Integration (dual API architecture)
- ✅ Email Templates System
- ✅ Data Quality Dashboard
- ✅ Observability Dashboard
- ✅ Drizzle to Prisma Migration Complete (Media API consolidated to single-ORM, Feb 2026)
- ✅ Automated Pangolin Setup (One-command tunnel deployment, Feb 2026)
- ✅ Migration Drift Fixed (Baseline catch-up migration, 14 migrations cover full schema, Feb 2026)
- 🚧 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,USER,TEMP - RBAC:
requireRole(...roles),requireNonTemp,authenticatemiddleware - Security features:
- Refresh token rotation (atomic transaction)
- User enumeration prevention (401 not 404)
- Rate limiting on auth endpoints (10/min)
- Redis authentication required
- XSS/injection prevention (HTML escaping)
- Path traversal protection
- Encryption key for DB secrets (ENCRYPTION_KEY env var)
- Security audit complete (13 findings addressed, see
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 # 30+ models: User, Campaign, Location, Shift, etc.
│ │ ├── migrations/ # Prisma migration history
│ │ └── seed.ts # Admin user, settings, page blocks
│ ├── drizzle/ # Media tables (Drizzle ORM)
│ ├── 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/
│ │ ├── auth/ # JWT login, register, refresh, logout
│ │ ├── users/ # User CRUD + pagination + search
│ │ ├── settings/ # Site settings singleton
│ │ ├── 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/
│ │ │ ├── pages-admin.routes.ts # Landing page CRUD
│ │ │ ├── pages-public.routes.ts # Public page renderer
│ │ │ └── blocks.routes.ts # Block library API
│ │ ├── email-templates/ # Email template CRUD + rendering
│ │ ├── media/ # Fastify media API (videos, reactions, jobs)
│ │ ├── listmonk/ # Newsletter sync admin routes
│ │ ├── pangolin/ # Tunnel management (Newt integration)
│ │ ├── docs/ # MkDocs + Code Server health checks
│ │ ├── qr/ # QR code PNG generation (public)
│ │ └── 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/
│ │ ├── auth/ # LoginPage
│ │ ├── DashboardPage.tsx # Admin dashboard
│ │ ├── UsersPage.tsx # User CRUD
│ │ ├── SettingsPage.tsx # Global site settings
│ │ ├── influence/
│ │ │ ├── CampaignsPage.tsx # Campaign management
│ │ │ ├── ResponsesPage.tsx # Response moderation
│ │ │ ├── RepresentativesPage.tsx # Rep cache admin
│ │ │ └── EmailQueuePage.tsx # Queue monitoring
│ │ ├── map/
│ │ │ ├── LocationsPage.tsx # Location CRUD + CSV + geocoding
│ │ │ ├── CutsPage.tsx # Cut table + map drawing editor
│ │ │ ├── ShiftsPage.tsx # Shift CRUD + signups drawer
│ │ │ ├── MapSettingsPage.tsx # Map settings
│ │ │ └── DataQualityDashboardPage.tsx # Geocoding quality metrics
│ │ ├── CanvassDashboardPage.tsx # Admin canvass overview
│ │ ├── WalkSheetPage.tsx # Printable walk sheet
│ │ ├── CutExportPage.tsx # Printable location report
│ │ ├── volunteer/
│ │ │ ├── VolunteerMapPage.tsx # Full-screen GPS canvass map
│ │ │ ├── VolunteerShiftsPage.tsx # Assigned shifts
│ │ │ ├── MyActivityPage.tsx # Visit history + outcomes
│ │ │ └── MyRoutesPage.tsx # Route history
│ │ ├── public/
│ │ │ ├── CampaignsListPage.tsx # Public campaign listing
│ │ │ ├── CampaignPage.tsx # Campaign detail + email form
│ │ │ ├── ResponseWallPage.tsx # Public response wall
│ │ │ ├── MapPage.tsx # Public Leaflet map
│ │ │ ├── ShiftsPage.tsx # Public shift signup
│ │ │ ├── LandingPage.tsx # Rendered landing page (/p/:slug)
│ │ │ ├── MediaGalleryPage.tsx # Public video gallery
│ │ │ └── MediaViewerPage.tsx # Video detail page
│ │ ├── media/
│ │ │ ├── LibraryPage.tsx # Video library management
│ │ │ ├── SharedMediaPage.tsx # Public gallery admin
│ │ │ └── MediaJobsPage.tsx # Job queue monitoring
│ │ ├── LandingPagesPage.tsx # Landing page manager
│ │ ├── PageEditorPage.tsx # Full-screen GrapesJS editor
│ │ ├── EmailTemplatesPage.tsx # Email template CRUD
│ │ ├── EmailTemplateEditorPage.tsx # Email template editor
│ │ ├── ListmonkPage.tsx # Newsletter sync management
│ │ ├── PangolinPage.tsx # Tunnel setup wizard
│ │ ├── DocsPage.tsx # MkDocs export management
│ │ ├── MkDocsSettingsPage.tsx # Documentation config
│ │ ├── ObservabilityPage.tsx # Monitoring dashboard
│ │ └── services/
│ │ ├── MiniQRPage.tsx # Mini QR iframe
│ │ ├── MailHogPage.tsx # Email capture UI
│ │ ├── CodeEditorPage.tsx # Code Server management
│ │ ├── N8nPage.tsx # Workflow automation
│ │ ├── GiteaPage.tsx # Git repository hosting
│ │ └── NocoDBPage.tsx # Data browser management
│ ├── stores/ # auth.store.ts, canvass.store.ts (Zustand)
│ ├── lib/ # api.ts, media-api.ts, media-public-api.ts (axios)
│ ├── hooks/ # useDebounce, useLocalStorage
│ └── types/ # api.ts, canvass.ts, media.ts (TypeScript interfaces)
│
├── media-manager/ # Legacy media manager (reference)
├── nginx/ # Reverse proxy config (subdomain routing + CSP)
├── configs/ # Prometheus, Grafana, Alertmanager configs
├── scripts/ # backup.sh, legacy Cloudflare scripts
├── docker-compose.yml # V2 orchestration (20+ services)
├── docker-compose.v1.yml # V1 backup (reference)
├── .env.example # All required environment variables
└── V2_PLAN.md # Full 14-phase roadmap
Quick Start Guide
Initial Setup (First Time)
-
Clone repository and checkout v2 branch:
git clone <repo-url> changemaker.lite cd changemaker.lite git checkout v2 -
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 | - |
| NocoDB | http://localhost:8091 | See NC_ADMIN_EMAIL/NC_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 |
Feature Flags
Enable optional features in .env:
# Media Manager
ENABLE_MEDIA_FEATURES=true
# Listmonk Newsletter Sync
LISTMONK_SYNC_ENABLED=true
# Email Test Mode (sends to MailHog instead of SMTP)
EMAIL_TEST_MODE=true
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
cd api && npx drizzle-kit push # Push Drizzle schema changes (media)
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
docker compose exec api npx drizzle-kit push
# Stop services
docker compose down
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: Upload with FFprobe metadata extraction (duration, dimensions, orientation, quality), bulk operations
- Quick Actions (Feb 2026): Edit, preview, analytics, schedule, duplicate, preview links (24h JWT), reset analytics
- Scheduled Publishing (Feb 2026): BullMQ job queue, timezone support (11 zones), calendar view, publish/unpublish automation
- Analytics (Feb 2026): Views, watch time, completion rate, traffic sources, registered viewers, GDPR-compliant (IP hashing, 90-day retention)
- Tracking: Public endpoints for view/event recording, 10s heartbeat, navigator.sendBeacon for reliability
- UI Features: Keyboard shortcuts (E/P/A/S), hover overlays, skeleton loading, error handling, mobile responsive
Routes:
- Admin:
/app/media/library,/app/media/analytics,/app/media/shared,/app/media/jobs - Public:
/gallery(public video gallery),/gallery/watch/:id(video viewer),/media/:id(backwards compatible viewer route) - Tracking (public):
/track/view,/track/event,/track/heartbeat
Note: The public gallery is served at /gallery via the admin app using MediaPublicLayout. This provides a unified purple interface for both authenticated and unauthenticated users. The gallery supports optional authentication (session-based upvoting/commenting for anonymous users).
Documentation:
- Media Admin Features Guide — Complete feature documentation
- Video Analytics Guide — Analytics setup and interpretation
- Media API README — Architecture overview
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/pangolin-setup.sh— CLI wrapper for automated setupconfigs/pangolin/resources.yml— Central resource definitions (12 services)- Newt container integration (Cloudflare alternative)
- Automated setup: One-command deployment (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 (Drizzle) |
| 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 |
| 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 |
| 8091 | NocoDB | Data browser |
| 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 |
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 |
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: See MEMORY.md for comprehensive development patterns, gotchas, and lessons learned. Below are V2-specific patterns only.
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/(AppLayout) - Public pages:
admin/src/pages/public/(PublicLayout, dark theme) - Volunteer pages:
admin/src/pages/volunteer/(VolunteerLayout) - Zustand stores:
auth.store.ts,canvass.store.ts - API clients:
{ api }fromlib/api.ts,mediaApifromlib/media-api.ts
Database ORMs
- Prisma (main API): Use
UncheckedCreateInput/UncheckedUpdateInputfor foreign keys,Prisma.InputJsonValuefor JSON arrays - Drizzle (media API): Separate schema file, push with
npx drizzle-kit push, no migrations generated
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: 14 migrations in
api/prisma/migrations/fully cover the schema (baseline catch-up applied Feb 2026) - Fixing drift: If
db pushwas used and migrations are out of sync:- Drop any stray indexes/objects in DB not in schema:
DROP INDEX IF EXISTS <name>; - Create a temp shadow DB:
docker compose exec -T v2-postgres createdb -U changemaker prisma_shadow_diff - Generate catch-up SQL:
docker compose exec -T api npx prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url "postgresql://..." --script - Save to
api/prisma/migrations/<timestamp>_<name>/migration.sql - Mark as applied:
docker compose exec -T api npx prisma migrate resolve --applied <migration_name> - Verify:
docker compose exec -T api npx prisma migrate status→ "Database schema is up to date!" - Clean up:
docker compose exec -T v2-postgres dropdb -U changemaker prisma_shadow_diff
- Drop any stray indexes/objects in DB not in schema:
- Gotcha:
--from-migrationsreplays all migration files on a shadow DB. If a migration references tables created bydb push(no migration file), it will fail. Fix: temporarily move the dependent migration aside, generate the catch-up (which includes the missing tables), then remove the old migration - Production deploys: Use
prisma migrate deploy(notmigrate dev) — it applies pending migrations without creating a shadow DB
V2-Specific Gotchas
- Prisma migrations: Never use
db pushon the v2 branch — always usemigrate devto keep migration history in sync. The baseline catch-up migration (20260224100000_baseline_catchup) covers all schema changes from Feb 18–24 that were previously applied viadb push - Fastify media API on port 4100, separate from Express on 4000 (same DB, different ORM)
- Volunteer page naming:
VolunteerShiftsPage.tsx(not "MyAssignmentsPage") - Tracking module:
api/src/modules/map/tracking/(volunteer + admin routes) - Pages module: 3 route files (pages-admin, pages-public, blocks)
- Vite proxy:
VITE_API_URL,VITE_MKDOCS_URLenv vars (Docker sets to container hostnames) - Nginx media API block must come BEFORE general API block
- MkDocs port 4003 (was 4000, conflicted with API)
- Media upload: requires separate RW volume mount for inbox directory (
:rwon/media/local/inbox), library remains read-only - FFmpeg/FFprobe: installed in media-api container (Alpine
apk add --no-cache ffmpeg), used for metadata extraction
Security & Configuration
Security Audit
Comprehensive security audit completed 2025-02-11, addressing 13 findings. See SECURITY_AUDIT_2025-02-11.md for full report.
Key improvements:
- Password policy: 12+ chars, uppercase, lowercase, digit (schema-enforced)
- Rate limits on auth endpoints (10/min per IP)
- Refresh token rotation (atomic transaction)
- User enumeration prevention (401 not 404)
- Redis authentication required
- XSS/injection prevention (HTML escaping)
- Path traversal protection
- Encryption key for DB secrets (
ENCRYPTION_KEYrequired in production) - Nginx security headers (HSTS, Permissions-Policy, CSP)
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 betteredmonton.org
CORS_ORIGINS=http://app.betteredmonton.org,https://app.betteredmonton.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.betteredmonton.org- Main API (all endpoints fail without this)app.betteredmonton.org- Admin GUI + public pagesmedia.betteredmonton.org- Media API
Verification:
# Should return JSON, NOT a 302 redirect
curl https://api.betteredmonton.org/api/health
See Also: PRODUCTION_403_FIX.md for detailed step-by-step instructions.
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 Connection Failures
Symptom: API logs show database connection errors.
Fix:
- Check PostgreSQL container:
docker compose ps v2-postgres - Verify
DATABASE_URLin.envmatches container name and port - Check PostgreSQL logs:
docker compose logs v2-postgres --tail 50 - Test connection:
docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"
Redis Connection Failures
Symptom: API logs show Redis connection errors, rate limiting doesn't work.
Fix:
- Check Redis container:
docker compose ps redis-changemaker - Verify
REDIS_PASSWORDmatches in.envandREDIS_URLformat - Check Redis logs:
docker compose logs redis-changemaker --tail 50 - Test connection:
docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping
V1 Reference (Legacy)
V1 code archived in influence/, map/, and docker-compose.v1.yml. Two independent Express apps using NocoDB REST API. See individual README files for V1 documentation:
influence/README.MD— Features, config, campaign managementmap/README.md— Features, config, setup instructions- Both use session-based auth, bcryptjs passwords, Bull job queues
Key Configuration Files
Infrastructure
docker-compose.yml— V2 orchestration (20+ services, monitoring profile).env/.env.example— Environment variables (100+ vars)
Database
api/prisma/schema.prisma— Main schema (30+ Prisma models)api/prisma/migrations/— 14 migration files (fully cover schema as of Feb 2026)api/drizzle.config.ts— Drizzle config for media tablesapi/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— Security audit reportMEMORY.md— Development patterns and gotchas