11-Feb-2026 — NGX Ramblers Architecture Overview


This article provides a visual tour of the NGX Ramblers platform architecture — every system, service, and integration that makes the application work. Use this as a reference when onboarding, debugging, or planning new features.

High-Level Architecture

NGX Ramblers is an Angular 21 single-page application backed by a Node.js/Express API, hosted on Fly.io, with MongoDB Atlas as the primary data store. Cloudflare handles DNS, SSL, and CDN — all user traffic passes through it before reaching Fly.io.

flowchart LR
    USER@{ icon: "ngx:user", label: "Users", pos: "b", h: 48 }
    CF@{ icon: "ngx:cloudflare", label: "Cloudflare DNS/CDN", pos: "b", h: 48 }

    USER --> CF

    subgraph fly["Fly.io"]
        FLY@{ icon: "logos:fly-icon", label: "Docker Container", pos: "b", h: 48 }
        ANGULAR@{ icon: "logos:angular-icon", label: "Angular 21 SPA", pos: "b", h: 48 }
        EXPRESS@{ icon: "ngx:express", label: "Express API", pos: "b", h: 48 }
        FLY --> ANGULAR
        ANGULAR --> EXPRESS
        FLY --> EXPRESS
    end

    CF --> FLY
    EXPRESS --> MONGO@{ icon: "ngx:mongodb", label: "MongoDB Atlas", pos: "b", h: 48 }
    EXPRESS --> S3@{ icon: "ngx:aws", label: "AWS S3", pos: "b", h: 48 }
    EXPRESS --> BREVO@{ icon: "ngx:brevo", label: "Brevo", pos: "b", h: 48 }
    EXPRESS --> RAMBLERS@{ icon: "ngx:ramblers-hq", label: "Ramblers API", pos: "b", h: 48 }

    style fly fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

Frontend Stack

The Angular frontend uses a rich set of libraries for maps, charts, galleries, and UI components.

flowchart LR
    SPA@{ icon: "logos:angular-icon", label: "Angular 21", pos: "b", h: 48 }

    subgraph ui["UI & Components"]
        BS@{ icon: "logos:bootstrap", label: "Bootstrap 5", pos: "b", h: 48 }
        FA["FontAwesome"]
        PRIME["PrimeNG"]
    end

    subgraph maps["Maps"]
        LEAF@{ icon: "logos:leaflet", label: "Leaflet", pos: "b", h: 48 }
        GMAP@{ icon: "logos:google-maps", label: "Google Maps", pos: "b", h: 48 }
        OS@{ icon: "ngx:os-maps", label: "OS Maps", pos: "b", h: 48 }
    end

    subgraph media["Media & Content"]
        GALLERY["ng-gallery"]
        CHARTS["Chart.js"]
        MD["ngx-markdown"]
        MERMAID["Mermaid"]
    end

    subgraph analytics["Analytics & Security"]
        GA@{ icon: "logos:google-analytics", label: "Google Analytics", pos: "b", h: 48 }
        RECAP@{ icon: "logos:recaptcha", label: "reCAPTCHA", pos: "b", h: 48 }
    end

    SPA --> ui
    SPA --> maps
    SPA --> media
    SPA --> analytics

    style ui fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style maps fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style media fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style analytics fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

Backend Integrations

The Express API server connects to external services for data storage, email, DNS, event management, and walk data.

flowchart LR
    API@{ icon: "ngx:express", label: "Express API", pos: "b", h: 48 }

    subgraph storage["Storage"]
        MONGO@{ icon: "ngx:mongodb", label: "MongoDB Atlas", pos: "b", h: 48 }
        S3@{ icon: "ngx:aws", label: "AWS S3", pos: "b", h: 48 }
    end

    subgraph email["Email"]
        BREVO@{ icon: "ngx:brevo", label: "Brevo (send)", pos: "b", h: 48 }
        GMAIL@{ icon: "logos:google-gmail", label: "Gmail API (inbox)", pos: "b", h: 48 }
        PUBSUB@{ icon: "logos:google-cloud", label: "Google Cloud Pub/Sub", pos: "b", h: 48 }
        MAILCHIMP@{ icon: "ngx:mailchimp", label: "Mailchimp (legacy)", pos: "b", h: 48 }
    end

    subgraph dns["DNS & Hosting"]
        CF@{ icon: "ngx:cloudflare", label: "Cloudflare", pos: "b", h: 48 }
        FLY@{ icon: "logos:fly-icon", label: "Fly.io", pos: "b", h: 48 }
    end

    subgraph walks["Walks & Events"]
        RAMBLERS@{ icon: "ngx:ramblers-hq", label: "Ramblers API", pos: "b", h: 48 }
        MEETUP@{ icon: "ngx:meetup", label: "Meetup API", pos: "b", h: 48 }
    end

    subgraph geo["Geolocation"]
        GMAP@{ icon: "logos:google-maps", label: "Google Places", pos: "b", h: 48 }
        POSTCODE["Postcode Lookup"]
        OSMAP@{ icon: "ngx:os-maps", label: "OS Maps Proxy", pos: "b", h: 48 }
    end

    API --> storage
    API --> email
    API --> dns
    API --> walks
    API --> geo

    style storage fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style email fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style dns fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style walks fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style geo fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

CI/CD Pipeline

Code flows from GitHub through automated builds and tests, then deploys to Fly.io with secrets imported from a secure store.

flowchart LR
    DEV@{ icon: "logos:github-icon", label: "Push to main", pos: "b", h: 48 }
    GHA@{ icon: "logos:github-actions", label: "GitHub Actions", pos: "b", h: 48 }
    BUILD@{ icon: "logos:docker-icon", label: "Docker Build", pos: "b", h: 48 }
    TEST@{ icon: "logos:typescript-icon", label: "Lint & Test", pos: "b", h: 48 }
    SECRETS@{ icon: "ngx:mongodb", label: "Import Secrets", pos: "b", h: 48 }
    DEPLOY@{ icon: "logos:fly-icon", label: "Deploy to Fly.io", pos: "b", h: 48 }

    DEV --> GHA
    GHA --> TEST
    TEST --> BUILD
    BUILD --> SECRETS
    SECRETS --> DEPLOY

Data Flow: Walk Publishing

When a walk leader creates a walk, data flows through multiple systems to reach all audiences.

flowchart LR
    LEADER@{ icon: "ngx:user", label: "Walk Leader", pos: "b", h: 48 }
    APP@{ icon: "ngx:ramblers", label: "NGX Ramblers", pos: "b", h: 48 }
    DB@{ icon: "ngx:mongodb", label: "MongoDB", pos: "b", h: 48 }
    RAPI@{ icon: "ngx:ramblers-hq", label: "Ramblers API", pos: "b", h: 48 }
    MEETUP@{ icon: "ngx:meetup", label: "Meetup", pos: "b", h: 48 }
    NOTIFY@{ icon: "ngx:brevo", label: "Email Notification", pos: "b", h: 48 }

    LEADER -->|"Create walk"| APP
    APP -->|"Save"| DB
    APP -->|"Upload"| RAPI
    APP -->|"Publish"| MEETUP
    DB -->|"Trigger"| NOTIFY

Email Architecture

NGX Ramblers now handles email in both directions. Brevo and Cloudflare cover outbound sending and domain routing, and a built-in Gmail inbox brings inbound committee mail into the application itself — with browser push notifications the moment something arrives.

Outbound: sending

Outbound mail goes through Brevo. The unified email composer builds a message — WYSIWYG body, merge fields, recipient selection — and sends it either as a Brevo campaign (bulk) or, increasingly, as a server-side transactional batch job. Cloudflare email-routing rules and DNS records (SPF / DKIM / DMARC) keep delivery trustworthy.

flowchart LR
    COMPOSER@{ icon: "ngx:ramblers", label: "Email Composer", pos: "b", h: 48 }
    BREVO@{ icon: "ngx:brevo", label: "Brevo API", pos: "b", h: 48 }
    DELIVER@{ icon: "ngx:user", label: "Recipient Inbox", pos: "b", h: 48 }

    COMPOSER -->|"campaign or transactional batch"| BREVO
    BREVO --> DELIVER

    CF@{ icon: "ngx:cloudflare", label: "Cloudflare", pos: "b", h: 48 }
    CF -->|"DNS: SPF / DKIM / DMARC"| DELIVER

Inbound: the Gmail inbox

Committee role addresses (secretary@, membership@, and so on) are routed by Cloudflare into a dedicated Gmail mailbox, which NGX reads through the Gmail API over OAuth — so committee mail is handled inside the app rather than forwarded to someone's personal mail client. Messages reach the app two ways:

Either way, each message is fetched, grouped into a thread, matched to the committee role it was addressed to, and stored in MongoDB (inboxMailboxConnections, inboxThreads, inboxMessages). The member assigned to that role reads and replies inside the app; replies go back out through the Brevo transactional path and are written back into the Gmail thread, so the mailbox stays consistent whether it is read in NGX or in Gmail.

flowchart TB
    SENDER@{ icon: "ngx:user", label: "External sender", pos: "b", h: 48 }
    CF@{ icon: "ngx:cloudflare", label: "Cloudflare routing", pos: "b", h: 48 }
    GMAIL@{ icon: "logos:google-gmail", label: "Dedicated Gmail mailbox", pos: "b", h: 48 }
    PUBSUB@{ icon: "logos:google-cloud", label: "Google Cloud Pub/Sub", pos: "b", h: 48 }
    APP@{ icon: "ngx:ramblers", label: "NGX Ramblers", pos: "b", h: 48 }
    DB@{ icon: "ngx:mongodb", label: "threads + messages", pos: "b", h: 48 }
    ROLE@{ icon: "ngx:user", label: "Committee role", pos: "b", h: 48 }

    SENDER -->|"role address"| CF
    CF -->|"forward"| GMAIL
    GMAIL -->|"watch"| PUBSUB
    PUBSUB -->|"push"| APP
    GMAIL -->|"poll: history delta"| APP
    APP -->|"fetch · thread · route"| DB
    DB --> ROLE
    ROLE -->|"reply via Brevo + writeback"| GMAIL

Live notifications

When an inbound message is stored, every member assigned to that role gets a browser push notification through a service worker and the Web Push (VAPID) protocol — no mail client open, no polling. The same signal drives the live unread badge in the admin UI.

flowchart LR
    NEW["New inbound message"]
    APP@{ icon: "ngx:ramblers", label: "NGX Ramblers", pos: "b", h: 48 }
    SW["Service Worker<br/>Web Push · VAPID"]
    MEMBER@{ icon: "ngx:user", label: "Assigned member", pos: "b", h: 48 }

    NEW --> APP
    APP -->|"push to subscribers"| SW
    SW -->|"notification + unread badge"| MEMBER

One inbox, many providers

The reader sits behind a provider interface (InboxReaderProvider). Gmail (gmail-api) is the implementation running in production today; the same interface leaves room for IMAP and other inbound sources (brevo-inbound-parse, cloudflare-ingress) without changing how threads, routing or notifications work.

File & Image Storage

User-uploaded images pass through server-side processing before reaching S3.

flowchart LR
    UPLOAD@{ icon: "logos:angular-icon", label: "Upload from UI", pos: "b", h: 48 }
    PROCESS@{ icon: "logos:nodejs-icon", label: "Resize (Sharp)", pos: "b", h: 48 }
    STORE@{ icon: "ngx:aws", label: "AWS S3", pos: "b", h: 48 }
    SERVE@{ icon: "ngx:user", label: "Serve to browser", pos: "b", h: 48 }

    UPLOAD --> PROCESS --> STORE --> SERVE

Maps & Geolocation

Multiple mapping services work together to provide rich location features.

flowchart TD
    LEAF@{ icon: "logos:leaflet", label: "Leaflet", pos: "b", h: 48 }

    subgraph tiles["Tile Layers"]
        OSM@{ icon: "logos:openstreetmap", label: "OpenStreetMap", pos: "b", h: 48 }
        OSMAP@{ icon: "ngx:os-maps", label: "OS Maps (UK)", pos: "b", h: 48 }
    end

    subgraph search["Location Search"]
        GMAP@{ icon: "logos:google-maps", label: "Google Places", pos: "b", h: 48 }
        POST["Postcode Lookup"]
        ORS["OpenRouteService"]
    end

    LEAF --> tiles
    LEAF --> search

    style tiles fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
    style search fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143

Authentication & Security

The application uses JWT for API authentication, with bcrypt for password storage and reCAPTCHA for public forms.

flowchart LR
    LOGIN@{ icon: "ngx:user", label: "Login", pos: "b", h: 48 }
    AUTH["Passport.js + JWT"]
    HASH["bcrypt"]
    DB@{ icon: "ngx:mongodb", label: "Member Record", pos: "b", h: 48 }
    RECAP@{ icon: "logos:recaptcha", label: "reCAPTCHA v3", pos: "b", h: 48 }
    CONTACT@{ icon: "ngx:user", label: "Contact Form", pos: "b", h: 48 }

    LOGIN --> AUTH --> DB
    AUTH --> HASH
    CONTACT --> RECAP

System Configuration

All external service credentials and feature flags are stored centrally in MongoDB under the config collection.

flowchart TD
    CONFIG@{ icon: "ngx:mongodb", label: "Config Collection", pos: "b", h: 48 }

    CONFIG --> K1@{ icon: "ngx:aws", label: "AWS", pos: "b", h: 48 }
    CONFIG --> K2@{ icon: "ngx:cloudflare", label: "Cloudflare", pos: "b", h: 48 }
    CONFIG --> K3@{ icon: "ngx:brevo", label: "Brevo", pos: "b", h: 48 }
    CONFIG --> K4@{ icon: "logos:google-maps", label: "Google Maps", pos: "b", h: 48 }
    CONFIG --> K5@{ icon: "logos:recaptcha", label: "reCAPTCHA", pos: "b", h: 48 }
    CONFIG --> K6@{ icon: "ngx:meetup", label: "Meetup", pos: "b", h: 48 }
    CONFIG --> K7@{ icon: "ngx:ramblers-hq", label: "Ramblers API", pos: "b", h: 48 }
    CONFIG --> K8@{ icon: "logos:google-analytics", label: "Google Analytics", pos: "b", h: 48 }
    CONFIG --> K9@{ icon: "logos:google-gmail", label: "Gmail OAuth (inbox)", pos: "b", h: 48 }
    CONFIG --> K10@{ icon: "logos:google-cloud", label: "Google Cloud Pub/Sub", pos: "b", h: 48 }
    CONFIG --> K11["Web Push (VAPID) keys"]

The Web Push (VAPID) keys are explained in Web Push notifications and VAPID keys.

Technology Summary

Layer Technology Purpose
Frontend Angular 21, Bootstrap 5, PrimeNG SPA framework, UI components
Backend Node.js 24, Express, TypeScript API server
Database MongoDB Atlas, Mongoose Data persistence
Hosting Fly.io, Docker Container hosting
CDN/DNS Cloudflare DNS, SSL, email routing, CDN
Storage AWS S3, Sharp File storage, image processing
Email (outbound) Brevo, Cloudflare Transactional + bulk send, domain routing, SPF/DKIM/DMARC
Email (inbound) Gmail API, Google Cloud Pub/Sub, Web Push (VAPID) In-app committee inbox, push notifications, live unread badge
Maps Leaflet, Google Maps, OS Maps Map rendering, geocoding
Walks Ramblers API, Meetup API Walk data sync, event publishing
CI/CD GitHub Actions, Docker Build, test, deploy
Analytics Google Analytics 4 User tracking
Security JWT, Passport.js, bcrypt, reCAPTCHA Authentication, bot protection