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