Architecture¶
Changemaker Lite uses a dual-API architecture with a shared PostgreSQL database, a React single-page application, and Nginx for subdomain routing across 30+ services.
System Diagram¶
graph LR
Browser["Browser"] --> Nginx["Nginx<br/>(reverse proxy)"]
Nginx --> Admin["React Admin GUI<br/>port 3000"]
Nginx --> API["Express API<br/>port 4000"]
Nginx --> MediaAPI["Fastify Media API<br/>port 4100"]
Nginx --> MkDocs["MkDocs<br/>port 4003/4004"]
Nginx --> Services["Other Services<br/>(Gitea, NocoDB, etc.)"]
API --> PostgreSQL[("PostgreSQL 16<br/>190+ tables")]
MediaAPI --> PostgreSQL
API --> Redis[("Redis<br/>cache + queues")]
API --> BullMQ["BullMQ<br/>(email, video jobs)"]
BullMQ --> Redis
subgraph Tunnel ["Public Access"]
Newt["Newt Client"] --> Pangolin["Pangolin Server"]
end
Newt --> Nginx
Key Components¶
| Component | Technology | Role |
|---|---|---|
| Main API | Express.js + TypeScript + Prisma | Auth, campaigns, map, shifts, pages, canvassing, email |
| Media API | Fastify + TypeScript + Prisma | Video library, analytics, uploads, scheduling |
| Admin GUI | React 19 + Vite + Ant Design + Zustand | Admin dashboard, public pages, volunteer portal, media gallery |
| Database | PostgreSQL 16 | Shared by both APIs (190+ models via Prisma) |
| Cache | Redis 7 | Rate limiting, BullMQ job queues, geocoding cache |
| Proxy | Nginx | Subdomain routing, security headers, WebSocket upgrade |
| Tunnel | Pangolin + Newt | Expose services without port forwarding |
| Monitoring | Prometheus + Grafana + Alertmanager | Metrics collection, dashboards, alerting |
Dual API Design¶
The platform runs two independent API servers sharing one PostgreSQL database:
The main API handles all core platform logic:
- Authentication — JWT access/refresh tokens, RBAC middleware
- Modules — Influence (campaigns, responses), Map (locations, cuts, shifts, canvassing), Pages, Email Templates, Settings, Users, Payments, Social, Calendar
- Services — Email queue (BullMQ), geocoding queue, Listmonk sync, Pangolin client, user provisioning
- ORM — Prisma with 190+ models and migration history
A separate server optimized for media handling:
- Video CRUD — Upload with FFprobe metadata extraction
- Scheduled Publishing — BullMQ queue with timezone support
- Analytics — View tracking, watch time, completion rates (GDPR-compliant)
- Public Gallery — Playlists, reactions, comments, SSE chat
- ORM — Prisma (migrated from Drizzle, Feb 2026)
Both servers connect to the same database and share the same Prisma schema. This separation allows the media API to handle large file uploads and streaming independently from the main API's request/response cycle.
Authentication Flow¶
sequenceDiagram
participant Client
participant API
participant DB
participant Redis
Client->>API: POST /api/auth/login {email, password}
API->>Redis: Check rate limit (10/min per IP)
Redis-->>API: OK
API->>DB: Verify bcrypt password
DB-->>API: User record
API->>DB: Create refresh token
API-->>Client: {accessToken (15min), refreshToken (7d)}
Note over Client: Authenticated requests
Client->>API: GET /api/campaigns<br/>Authorization: Bearer <accessToken>
API->>API: Verify JWT + check role (RBAC)
API-->>Client: 200 OK
Note over Client: Token expired
Client->>API: POST /api/auth/refresh {refreshToken}
API->>DB: Atomic rotation (delete old, create new)
API-->>Client: {new accessToken, new refreshToken}
Security Features¶
- Password policy — 12+ characters, uppercase, lowercase, digit (schema-enforced)
- Refresh token rotation — Atomic Prisma transaction prevents race conditions
- User enumeration prevention — Returns 401 (not 404) for missing users
- Rate limiting — 10 requests/minute on auth endpoints via Redis
- 11 roles —
SUPER_ADMIN(implicit bypass), 8 module-specific admin roles,USER,TEMP - Encryption — AES-256-GCM for sensitive DB fields (
ENCRYPTION_KEYenv var)
Request Lifecycle¶
graph TD
A["Incoming Request"] --> B["Nginx"]
B -->|"Host: api.domain"| C["Express API"]
B -->|"Host: media.domain"| D["Fastify Media API"]
B -->|"Host: app.domain"| E["React Admin GUI"]
C --> F["Rate Limiter (Redis)"]
F --> G["Auth Middleware (JWT)"]
G --> H["Role Check (RBAC)"]
H --> I["Validation (Zod)"]
I --> J["Route Handler"]
J --> K["Service Layer"]
K --> L["Prisma ORM"]
L --> M[("PostgreSQL")]
J --> N["Response + Metrics"]
Database Schema¶
The database contains 190+ Prisma models organized by module (key ones shown):
| Module | Key Models |
|---|---|
| Auth | User, RefreshToken |
| Influence | Campaign, CampaignEmail, CampaignResponse, Representative, PostalCode |
| Map | Location, Address, Cut, Shift, ShiftSignup |
| Canvass | CanvassSession, CanvassVisit, TrackingSession, TrackingPoint |
| Pages | Page, PageBlock, EmailTemplate |
| Media | Video, VideoReaction, VideoComment, VideoView, Playlist, PlaylistVideo |
| Payments | StripeProduct, StripePrice, StripeDonationPage, StripeOrder |
| Social | Friendship, SocialNotification, CalendarLayer, CalendarItem |
| SMS | SmsContactList, SmsCampaign, SmsMessage, SmsConversation |
| People | Contact, ContactAddress, ContactEmail, ContactPhone, ContactConnection |
| Settings | SiteSettings, MapSettings |
Docker Compose Architecture¶
Services are organized into categories with dependency management:
graph TD
subgraph Core ["Core (always started)"]
PG["PostgreSQL"] --> API["Express API"]
Redis --> API
PG --> Media["Fastify Media API"]
API --> Admin["React Admin"]
Admin --> Nginx
API --> Nginx
Media --> Nginx
end
subgraph Communication ["Communication (optional)"]
RC["Rocket.Chat"] --> MongoDB
Jitsi["Jitsi Meet (4 containers)"]
Gancio["Gancio Events"]
end
subgraph Monitoring ["Monitoring (profile)"]
Prometheus --> Grafana
Prometheus --> Alertmanager
cAdvisor --> Prometheus
NodeExporter --> Prometheus
end
subgraph Tunnel ["Tunnel"]
Newt --> Nginx
end
Docker healthchecks ensure proper startup order: PostgreSQL and Redis must be healthy before the API starts. The API runs migrations and seeding automatically via its entrypoint script.
Subdomain Routing¶
Nginx routes requests based on the Host header. All services run on the changemaker-lite Docker bridge network.
| Pattern | Target |
|---|---|
app.DOMAIN |
Admin GUI (admin + public + volunteer + gallery) |
api.DOMAIN |
Express API |
media.DOMAIN |
Fastify Media API |
DOMAIN (root) |
MkDocs static site |
*.DOMAIN |
15+ additional service subdomains |
See Services for the complete subdomain table.