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:

Express API (port 4000)Fastify Media API (port 4100)

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_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.