Skip to content

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 rolesSUPER_ADMIN (implicit bypass), 8 module-specific admin roles, USER, TEMP
  • Encryption — AES-256-GCM for sensitive DB fields (ENCRYPTION_KEY env 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.