---
title: Architecture
description: System architecture, dual API design, database schema, and authentication flow.
icon: material/sitemap
tags:
  - reference
  - developer
  - architecture
---

# 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

```mermaid
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)"

    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

=== "Fastify Media API (port 4100)"

    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

```mermaid
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

```mermaid
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:

```mermaid
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](../services/index.md) for the complete subdomain table.
