{"id":"698c6fe8b2aeaff1f1af0d6b","title":"2026 02 11 Ngx Ramblers Architecture","path":"how-to/technical-articles/2026-02-11-ngx-ramblers-architecture","contentMarkdown":"# 11-Feb-2026 — NGX Ramblers Architecture Overview\n\n_____\n\nThis 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.\n\n## High-Level Architecture\n\nNGX 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.\n\n```mermaid\nflowchart LR\n    USER@{ icon: \"ngx:user\", label: \"Users\", pos: \"b\", h: 48 }\n    CF@{ icon: \"ngx:cloudflare\", label: \"Cloudflare DNS/CDN\", pos: \"b\", h: 48 }\n\n    USER --> CF\n\n    subgraph fly[\"Fly.io\"]\n        FLY@{ icon: \"logos:fly-icon\", label: \"Docker Container\", pos: \"b\", h: 48 }\n        ANGULAR@{ icon: \"logos:angular-icon\", label: \"Angular 21 SPA\", pos: \"b\", h: 48 }\n        EXPRESS@{ icon: \"ngx:express\", label: \"Express API\", pos: \"b\", h: 48 }\n        FLY --> ANGULAR\n        ANGULAR --> EXPRESS\n        FLY --> EXPRESS\n    end\n\n    CF --> FLY\n    EXPRESS --> MONGO@{ icon: \"ngx:mongodb\", label: \"MongoDB Atlas\", pos: \"b\", h: 48 }\n    EXPRESS --> S3@{ icon: \"ngx:aws\", label: \"AWS S3\", pos: \"b\", h: 48 }\n    EXPRESS --> BREVO@{ icon: \"ngx:brevo\", label: \"Brevo\", pos: \"b\", h: 48 }\n    EXPRESS --> RAMBLERS@{ icon: \"ngx:ramblers-hq\", label: \"Ramblers API\", pos: \"b\", h: 48 }\n\n    style fly fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n```\n\n## Frontend Stack\n\nThe Angular frontend uses a rich set of libraries for maps, charts, galleries, and UI components.\n\n```mermaid\nflowchart LR\n    SPA@{ icon: \"logos:angular-icon\", label: \"Angular 21\", pos: \"b\", h: 48 }\n\n    subgraph ui[\"UI & Components\"]\n        BS@{ icon: \"logos:bootstrap\", label: \"Bootstrap 5\", pos: \"b\", h: 48 }\n        FA[\"FontAwesome\"]\n        PRIME[\"PrimeNG\"]\n    end\n\n    subgraph maps[\"Maps\"]\n        LEAF@{ icon: \"logos:leaflet\", label: \"Leaflet\", pos: \"b\", h: 48 }\n        GMAP@{ icon: \"logos:google-maps\", label: \"Google Maps\", pos: \"b\", h: 48 }\n        OS@{ icon: \"ngx:os-maps\", label: \"OS Maps\", pos: \"b\", h: 48 }\n    end\n\n    subgraph media[\"Media & Content\"]\n        GALLERY[\"ng-gallery\"]\n        CHARTS[\"Chart.js\"]\n        MD[\"ngx-markdown\"]\n        MERMAID[\"Mermaid\"]\n    end\n\n    subgraph analytics[\"Analytics & Security\"]\n        GA@{ icon: \"logos:google-analytics\", label: \"Google Analytics\", pos: \"b\", h: 48 }\n        RECAP@{ icon: \"logos:recaptcha\", label: \"reCAPTCHA\", pos: \"b\", h: 48 }\n    end\n\n    SPA --> ui\n    SPA --> maps\n    SPA --> media\n    SPA --> analytics\n\n    style ui fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style maps fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style media fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style analytics fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n```\n\n## Backend Integrations\n\nThe Express API server connects to external services for data storage, email, DNS, event management, and walk data.\n\n```mermaid\nflowchart LR\n    API@{ icon: \"ngx:express\", label: \"Express API\", pos: \"b\", h: 48 }\n\n    subgraph storage[\"Storage\"]\n        MONGO@{ icon: \"ngx:mongodb\", label: \"MongoDB Atlas\", pos: \"b\", h: 48 }\n        S3@{ icon: \"ngx:aws\", label: \"AWS S3\", pos: \"b\", h: 48 }\n    end\n\n    subgraph email[\"Email\"]\n        BREVO@{ icon: \"ngx:brevo\", label: \"Brevo (send)\", pos: \"b\", h: 48 }\n        GMAIL@{ icon: \"logos:google-gmail\", label: \"Gmail API (inbox)\", pos: \"b\", h: 48 }\n        PUBSUB@{ icon: \"logos:google-cloud\", label: \"Google Cloud Pub/Sub\", pos: \"b\", h: 48 }\n        MAILCHIMP@{ icon: \"ngx:mailchimp\", label: \"Mailchimp (legacy)\", pos: \"b\", h: 48 }\n    end\n\n    subgraph dns[\"DNS & Hosting\"]\n        CF@{ icon: \"ngx:cloudflare\", label: \"Cloudflare\", pos: \"b\", h: 48 }\n        FLY@{ icon: \"logos:fly-icon\", label: \"Fly.io\", pos: \"b\", h: 48 }\n    end\n\n    subgraph walks[\"Walks & Events\"]\n        RAMBLERS@{ icon: \"ngx:ramblers-hq\", label: \"Ramblers API\", pos: \"b\", h: 48 }\n        MEETUP@{ icon: \"ngx:meetup\", label: \"Meetup API\", pos: \"b\", h: 48 }\n    end\n\n    subgraph geo[\"Geolocation\"]\n        GMAP@{ icon: \"logos:google-maps\", label: \"Google Places\", pos: \"b\", h: 48 }\n        POSTCODE[\"Postcode Lookup\"]\n        OSMAP@{ icon: \"ngx:os-maps\", label: \"OS Maps Proxy\", pos: \"b\", h: 48 }\n    end\n\n    API --> storage\n    API --> email\n    API --> dns\n    API --> walks\n    API --> geo\n\n    style storage fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style email fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style dns fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style walks fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style geo fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n```\n\n## CI/CD Pipeline\n\nCode flows from GitHub through automated builds and tests, then deploys to Fly.io with secrets imported from a secure store.\n\n```mermaid\nflowchart LR\n    DEV@{ icon: \"logos:github-icon\", label: \"Push to main\", pos: \"b\", h: 48 }\n    GHA@{ icon: \"logos:github-actions\", label: \"GitHub Actions\", pos: \"b\", h: 48 }\n    BUILD@{ icon: \"logos:docker-icon\", label: \"Docker Build\", pos: \"b\", h: 48 }\n    TEST@{ icon: \"logos:typescript-icon\", label: \"Lint & Test\", pos: \"b\", h: 48 }\n    SECRETS@{ icon: \"ngx:mongodb\", label: \"Import Secrets\", pos: \"b\", h: 48 }\n    DEPLOY@{ icon: \"logos:fly-icon\", label: \"Deploy to Fly.io\", pos: \"b\", h: 48 }\n\n    DEV --> GHA\n    GHA --> TEST\n    TEST --> BUILD\n    BUILD --> SECRETS\n    SECRETS --> DEPLOY\n```\n\n\n\n## Data Flow: Walk Publishing\n\nWhen a walk leader creates a walk, data flows through multiple systems to reach all audiences.\n\n```mermaid\nflowchart LR\n    LEADER@{ icon: \"ngx:user\", label: \"Walk Leader\", pos: \"b\", h: 48 }\n    APP@{ icon: \"ngx:ramblers\", label: \"NGX Ramblers\", pos: \"b\", h: 48 }\n    DB@{ icon: \"ngx:mongodb\", label: \"MongoDB\", pos: \"b\", h: 48 }\n    RAPI@{ icon: \"ngx:ramblers-hq\", label: \"Ramblers API\", pos: \"b\", h: 48 }\n    MEETUP@{ icon: \"ngx:meetup\", label: \"Meetup\", pos: \"b\", h: 48 }\n    NOTIFY@{ icon: \"ngx:brevo\", label: \"Email Notification\", pos: \"b\", h: 48 }\n\n    LEADER -->|\"Create walk\"| APP\n    APP -->|\"Save\"| DB\n    APP -->|\"Upload\"| RAPI\n    APP -->|\"Publish\"| MEETUP\n    DB -->|\"Trigger\"| NOTIFY\n```\n\n## Email Architecture\n\nNGX 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.\n\n### Outbound: sending\n\nOutbound 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.\n\n```mermaid\nflowchart LR\n    COMPOSER@{ icon: \"ngx:ramblers\", label: \"Email Composer\", pos: \"b\", h: 48 }\n    BREVO@{ icon: \"ngx:brevo\", label: \"Brevo API\", pos: \"b\", h: 48 }\n    DELIVER@{ icon: \"ngx:user\", label: \"Recipient Inbox\", pos: \"b\", h: 48 }\n\n    COMPOSER -->|\"campaign or transactional batch\"| BREVO\n    BREVO --> DELIVER\n\n    CF@{ icon: \"ngx:cloudflare\", label: \"Cloudflare\", pos: \"b\", h: 48 }\n    CF -->|\"DNS: SPF / DKIM / DMARC\"| DELIVER\n```\n\n### Inbound: the Gmail inbox\n\nCommittee 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:\n\n- **Watch mode** — Gmail publishes a change notification to **Google Cloud Pub/Sub**, which pushes it straight to the application. Near real-time.\n- **Poll mode** — the application periodically asks Gmail for the history delta since it last looked. A dependable fallback where Pub/Sub is not set up.\n\nEither 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.\n\n```mermaid\nflowchart TB\n    SENDER@{ icon: \"ngx:user\", label: \"External sender\", pos: \"b\", h: 48 }\n    CF@{ icon: \"ngx:cloudflare\", label: \"Cloudflare routing\", pos: \"b\", h: 48 }\n    GMAIL@{ icon: \"logos:google-gmail\", label: \"Dedicated Gmail mailbox\", pos: \"b\", h: 48 }\n    PUBSUB@{ icon: \"logos:google-cloud\", label: \"Google Cloud Pub/Sub\", pos: \"b\", h: 48 }\n    APP@{ icon: \"ngx:ramblers\", label: \"NGX Ramblers\", pos: \"b\", h: 48 }\n    DB@{ icon: \"ngx:mongodb\", label: \"threads + messages\", pos: \"b\", h: 48 }\n    ROLE@{ icon: \"ngx:user\", label: \"Committee role\", pos: \"b\", h: 48 }\n\n    SENDER -->|\"role address\"| CF\n    CF -->|\"forward\"| GMAIL\n    GMAIL -->|\"watch\"| PUBSUB\n    PUBSUB -->|\"push\"| APP\n    GMAIL -->|\"poll: history delta\"| APP\n    APP -->|\"fetch · thread · route\"| DB\n    DB --> ROLE\n    ROLE -->|\"reply via Brevo + writeback\"| GMAIL\n```\n\n\n\n### Live notifications\n\nWhen 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.\n\n```mermaid\nflowchart LR\n    NEW[\"New inbound message\"]\n    APP@{ icon: \"ngx:ramblers\", label: \"NGX Ramblers\", pos: \"b\", h: 48 }\n    SW[\"Service Worker<br/>Web Push · VAPID\"]\n    MEMBER@{ icon: \"ngx:user\", label: \"Assigned member\", pos: \"b\", h: 48 }\n\n    NEW --> APP\n    APP -->|\"push to subscribers\"| SW\n    SW -->|\"notification + unread badge\"| MEMBER\n```\n\n\n\n### One inbox, many providers\n\nThe 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.\n\n## File & Image Storage\n\nUser-uploaded images pass through server-side processing before reaching S3.\n\n```mermaid\nflowchart LR\n    UPLOAD@{ icon: \"logos:angular-icon\", label: \"Upload from UI\", pos: \"b\", h: 48 }\n    PROCESS@{ icon: \"logos:nodejs-icon\", label: \"Resize (Sharp)\", pos: \"b\", h: 48 }\n    STORE@{ icon: \"ngx:aws\", label: \"AWS S3\", pos: \"b\", h: 48 }\n    SERVE@{ icon: \"ngx:user\", label: \"Serve to browser\", pos: \"b\", h: 48 }\n\n    UPLOAD --> PROCESS --> STORE --> SERVE\n```\n\n\n\n## Maps & Geolocation\n\nMultiple mapping services work together to provide rich location features.\n\n```mermaid\nflowchart TD\n    LEAF@{ icon: \"logos:leaflet\", label: \"Leaflet\", pos: \"b\", h: 48 }\n\n    subgraph tiles[\"Tile Layers\"]\n        OSM@{ icon: \"logos:openstreetmap\", label: \"OpenStreetMap\", pos: \"b\", h: 48 }\n        OSMAP@{ icon: \"ngx:os-maps\", label: \"OS Maps (UK)\", pos: \"b\", h: 48 }\n    end\n\n    subgraph search[\"Location Search\"]\n        GMAP@{ icon: \"logos:google-maps\", label: \"Google Places\", pos: \"b\", h: 48 }\n        POST[\"Postcode Lookup\"]\n        ORS[\"OpenRouteService\"]\n    end\n\n    LEAF --> tiles\n    LEAF --> search\n\n    style tiles fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style search fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n```\n\n## Authentication & Security\n\nThe application uses JWT for API authentication, with bcrypt for password storage and reCAPTCHA for public forms.\n\n```mermaid\nflowchart LR\n    LOGIN@{ icon: \"ngx:user\", label: \"Login\", pos: \"b\", h: 48 }\n    AUTH[\"Passport.js + JWT\"]\n    HASH[\"bcrypt\"]\n    DB@{ icon: \"ngx:mongodb\", label: \"Member Record\", pos: \"b\", h: 48 }\n    RECAP@{ icon: \"logos:recaptcha\", label: \"reCAPTCHA v3\", pos: \"b\", h: 48 }\n    CONTACT@{ icon: \"ngx:user\", label: \"Contact Form\", pos: \"b\", h: 48 }\n\n    LOGIN --> AUTH --> DB\n    AUTH --> HASH\n    CONTACT --> RECAP\n```\n\n\n\n## System Configuration\n\nAll external service credentials and feature flags are stored centrally in MongoDB under the `config` collection.\n\n```mermaid\nflowchart TD\n    CONFIG@{ icon: \"ngx:mongodb\", label: \"Config Collection\", pos: \"b\", h: 48 }\n\n    CONFIG --> K1@{ icon: \"ngx:aws\", label: \"AWS\", pos: \"b\", h: 48 }\n    CONFIG --> K2@{ icon: \"ngx:cloudflare\", label: \"Cloudflare\", pos: \"b\", h: 48 }\n    CONFIG --> K3@{ icon: \"ngx:brevo\", label: \"Brevo\", pos: \"b\", h: 48 }\n    CONFIG --> K4@{ icon: \"logos:google-maps\", label: \"Google Maps\", pos: \"b\", h: 48 }\n    CONFIG --> K5@{ icon: \"logos:recaptcha\", label: \"reCAPTCHA\", pos: \"b\", h: 48 }\n    CONFIG --> K6@{ icon: \"ngx:meetup\", label: \"Meetup\", pos: \"b\", h: 48 }\n    CONFIG --> K7@{ icon: \"ngx:ramblers-hq\", label: \"Ramblers API\", pos: \"b\", h: 48 }\n    CONFIG --> K8@{ icon: \"logos:google-analytics\", label: \"Google Analytics\", pos: \"b\", h: 48 }\n    CONFIG --> K9@{ icon: \"logos:google-gmail\", label: \"Gmail OAuth (inbox)\", pos: \"b\", h: 48 }\n    CONFIG --> K10@{ icon: \"logos:google-cloud\", label: \"Google Cloud Pub/Sub\", pos: \"b\", h: 48 }\n    CONFIG --> K11[\"Web Push (VAPID) keys\"]\n```\n\nThe Web Push (VAPID) keys are explained in [Web Push notifications and VAPID keys](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-02-11-ngx-ramblers-architecture/web-push-vapid).\n\n\n\n## Technology Summary\n\n| Layer | Technology | Purpose |\n|-------|-----------|---------|\n| **Frontend** | Angular 21, Bootstrap 5, PrimeNG | SPA framework, UI components |\n| **Backend** | Node.js 24, Express, TypeScript | API server |\n| **Database** | MongoDB Atlas, Mongoose | Data persistence |\n| **Hosting** | Fly.io, Docker | Container hosting |\n| **CDN/DNS** | Cloudflare | DNS, SSL, email routing, CDN |\n| **Storage** | AWS S3, Sharp | File storage, image processing |\n| **Email (outbound)** | Brevo, Cloudflare | Transactional + bulk send, domain routing, SPF/DKIM/DMARC |\n| **Email (inbound)** | Gmail API, Google Cloud Pub/Sub, Web Push (VAPID) | In-app committee inbox, push notifications, live unread badge |\n| **Maps** | Leaflet, Google Maps, OS Maps | Map rendering, geocoding |\n| **Walks** | Ramblers API, Meetup API | Walk data sync, event publishing |\n| **CI/CD** | GitHub Actions, Docker | Build, test, deploy |\n| **Analytics** | Google Analytics 4 | User tracking |\n| **Security** | JWT, Passport.js, bcrypt, reCAPTCHA | Authentication, bot protection |","contentHtml":"<h1>11-Feb-2026 — NGX Ramblers Architecture Overview</h1>\n<hr>\n<p>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.</p>\n<h2>High-Level Architecture</h2>\n<p>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.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    USER@{ icon: &quot;ngx:user&quot;, label: &quot;Users&quot;, pos: &quot;b&quot;, h: 48 }\n    CF@{ icon: &quot;ngx:cloudflare&quot;, label: &quot;Cloudflare DNS/CDN&quot;, pos: &quot;b&quot;, h: 48 }\n\n    USER --&gt; CF\n\n    subgraph fly[&quot;Fly.io&quot;]\n        FLY@{ icon: &quot;logos:fly-icon&quot;, label: &quot;Docker Container&quot;, pos: &quot;b&quot;, h: 48 }\n        ANGULAR@{ icon: &quot;logos:angular-icon&quot;, label: &quot;Angular 21 SPA&quot;, pos: &quot;b&quot;, h: 48 }\n        EXPRESS@{ icon: &quot;ngx:express&quot;, label: &quot;Express API&quot;, pos: &quot;b&quot;, h: 48 }\n        FLY --&gt; ANGULAR\n        ANGULAR --&gt; EXPRESS\n        FLY --&gt; EXPRESS\n    end\n\n    CF --&gt; FLY\n    EXPRESS --&gt; MONGO@{ icon: &quot;ngx:mongodb&quot;, label: &quot;MongoDB Atlas&quot;, pos: &quot;b&quot;, h: 48 }\n    EXPRESS --&gt; S3@{ icon: &quot;ngx:aws&quot;, label: &quot;AWS S3&quot;, pos: &quot;b&quot;, h: 48 }\n    EXPRESS --&gt; BREVO@{ icon: &quot;ngx:brevo&quot;, label: &quot;Brevo&quot;, pos: &quot;b&quot;, h: 48 }\n    EXPRESS --&gt; RAMBLERS@{ icon: &quot;ngx:ramblers-hq&quot;, label: &quot;Ramblers API&quot;, pos: &quot;b&quot;, h: 48 }\n\n    style fly fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n</code></pre>\n<h2>Frontend Stack</h2>\n<p>The Angular frontend uses a rich set of libraries for maps, charts, galleries, and UI components.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    SPA@{ icon: &quot;logos:angular-icon&quot;, label: &quot;Angular 21&quot;, pos: &quot;b&quot;, h: 48 }\n\n    subgraph ui[&quot;UI &amp; Components&quot;]\n        BS@{ icon: &quot;logos:bootstrap&quot;, label: &quot;Bootstrap 5&quot;, pos: &quot;b&quot;, h: 48 }\n        FA[&quot;FontAwesome&quot;]\n        PRIME[&quot;PrimeNG&quot;]\n    end\n\n    subgraph maps[&quot;Maps&quot;]\n        LEAF@{ icon: &quot;logos:leaflet&quot;, label: &quot;Leaflet&quot;, pos: &quot;b&quot;, h: 48 }\n        GMAP@{ icon: &quot;logos:google-maps&quot;, label: &quot;Google Maps&quot;, pos: &quot;b&quot;, h: 48 }\n        OS@{ icon: &quot;ngx:os-maps&quot;, label: &quot;OS Maps&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph media[&quot;Media &amp; Content&quot;]\n        GALLERY[&quot;ng-gallery&quot;]\n        CHARTS[&quot;Chart.js&quot;]\n        MD[&quot;ngx-markdown&quot;]\n        MERMAID[&quot;Mermaid&quot;]\n    end\n\n    subgraph analytics[&quot;Analytics &amp; Security&quot;]\n        GA@{ icon: &quot;logos:google-analytics&quot;, label: &quot;Google Analytics&quot;, pos: &quot;b&quot;, h: 48 }\n        RECAP@{ icon: &quot;logos:recaptcha&quot;, label: &quot;reCAPTCHA&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    SPA --&gt; ui\n    SPA --&gt; maps\n    SPA --&gt; media\n    SPA --&gt; analytics\n\n    style ui fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style maps fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style media fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style analytics fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n</code></pre>\n<h2>Backend Integrations</h2>\n<p>The Express API server connects to external services for data storage, email, DNS, event management, and walk data.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    API@{ icon: &quot;ngx:express&quot;, label: &quot;Express API&quot;, pos: &quot;b&quot;, h: 48 }\n\n    subgraph storage[&quot;Storage&quot;]\n        MONGO@{ icon: &quot;ngx:mongodb&quot;, label: &quot;MongoDB Atlas&quot;, pos: &quot;b&quot;, h: 48 }\n        S3@{ icon: &quot;ngx:aws&quot;, label: &quot;AWS S3&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph email[&quot;Email&quot;]\n        BREVO@{ icon: &quot;ngx:brevo&quot;, label: &quot;Brevo (send)&quot;, pos: &quot;b&quot;, h: 48 }\n        GMAIL@{ icon: &quot;logos:google-gmail&quot;, label: &quot;Gmail API (inbox)&quot;, pos: &quot;b&quot;, h: 48 }\n        PUBSUB@{ icon: &quot;logos:google-cloud&quot;, label: &quot;Google Cloud Pub/Sub&quot;, pos: &quot;b&quot;, h: 48 }\n        MAILCHIMP@{ icon: &quot;ngx:mailchimp&quot;, label: &quot;Mailchimp (legacy)&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph dns[&quot;DNS &amp; Hosting&quot;]\n        CF@{ icon: &quot;ngx:cloudflare&quot;, label: &quot;Cloudflare&quot;, pos: &quot;b&quot;, h: 48 }\n        FLY@{ icon: &quot;logos:fly-icon&quot;, label: &quot;Fly.io&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph walks[&quot;Walks &amp; Events&quot;]\n        RAMBLERS@{ icon: &quot;ngx:ramblers-hq&quot;, label: &quot;Ramblers API&quot;, pos: &quot;b&quot;, h: 48 }\n        MEETUP@{ icon: &quot;ngx:meetup&quot;, label: &quot;Meetup API&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph geo[&quot;Geolocation&quot;]\n        GMAP@{ icon: &quot;logos:google-maps&quot;, label: &quot;Google Places&quot;, pos: &quot;b&quot;, h: 48 }\n        POSTCODE[&quot;Postcode Lookup&quot;]\n        OSMAP@{ icon: &quot;ngx:os-maps&quot;, label: &quot;OS Maps Proxy&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    API --&gt; storage\n    API --&gt; email\n    API --&gt; dns\n    API --&gt; walks\n    API --&gt; geo\n\n    style storage fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style email fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style dns fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style walks fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style geo fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n</code></pre>\n<h2>CI/CD Pipeline</h2>\n<p>Code flows from GitHub through automated builds and tests, then deploys to Fly.io with secrets imported from a secure store.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    DEV@{ icon: &quot;logos:github-icon&quot;, label: &quot;Push to main&quot;, pos: &quot;b&quot;, h: 48 }\n    GHA@{ icon: &quot;logos:github-actions&quot;, label: &quot;GitHub Actions&quot;, pos: &quot;b&quot;, h: 48 }\n    BUILD@{ icon: &quot;logos:docker-icon&quot;, label: &quot;Docker Build&quot;, pos: &quot;b&quot;, h: 48 }\n    TEST@{ icon: &quot;logos:typescript-icon&quot;, label: &quot;Lint &amp; Test&quot;, pos: &quot;b&quot;, h: 48 }\n    SECRETS@{ icon: &quot;ngx:mongodb&quot;, label: &quot;Import Secrets&quot;, pos: &quot;b&quot;, h: 48 }\n    DEPLOY@{ icon: &quot;logos:fly-icon&quot;, label: &quot;Deploy to Fly.io&quot;, pos: &quot;b&quot;, h: 48 }\n\n    DEV --&gt; GHA\n    GHA --&gt; TEST\n    TEST --&gt; BUILD\n    BUILD --&gt; SECRETS\n    SECRETS --&gt; DEPLOY\n</code></pre>\n<h2>Data Flow: Walk Publishing</h2>\n<p>When a walk leader creates a walk, data flows through multiple systems to reach all audiences.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    LEADER@{ icon: &quot;ngx:user&quot;, label: &quot;Walk Leader&quot;, pos: &quot;b&quot;, h: 48 }\n    APP@{ icon: &quot;ngx:ramblers&quot;, label: &quot;NGX Ramblers&quot;, pos: &quot;b&quot;, h: 48 }\n    DB@{ icon: &quot;ngx:mongodb&quot;, label: &quot;MongoDB&quot;, pos: &quot;b&quot;, h: 48 }\n    RAPI@{ icon: &quot;ngx:ramblers-hq&quot;, label: &quot;Ramblers API&quot;, pos: &quot;b&quot;, h: 48 }\n    MEETUP@{ icon: &quot;ngx:meetup&quot;, label: &quot;Meetup&quot;, pos: &quot;b&quot;, h: 48 }\n    NOTIFY@{ icon: &quot;ngx:brevo&quot;, label: &quot;Email Notification&quot;, pos: &quot;b&quot;, h: 48 }\n\n    LEADER --&gt;|&quot;Create walk&quot;| APP\n    APP --&gt;|&quot;Save&quot;| DB\n    APP --&gt;|&quot;Upload&quot;| RAPI\n    APP --&gt;|&quot;Publish&quot;| MEETUP\n    DB --&gt;|&quot;Trigger&quot;| NOTIFY\n</code></pre>\n<h2>Email Architecture</h2>\n<p>NGX Ramblers now handles email in <strong>both directions</strong>. Brevo and Cloudflare cover outbound sending and domain routing, and a built-in <strong>Gmail inbox</strong> brings inbound committee mail into the application itself — with browser push notifications the moment something arrives.</p>\n<h3>Outbound: sending</h3>\n<p>Outbound mail goes through <strong>Brevo</strong>. 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 <strong>transactional batch</strong> job. Cloudflare email-routing rules and DNS records (SPF / DKIM / DMARC) keep delivery trustworthy.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    COMPOSER@{ icon: &quot;ngx:ramblers&quot;, label: &quot;Email Composer&quot;, pos: &quot;b&quot;, h: 48 }\n    BREVO@{ icon: &quot;ngx:brevo&quot;, label: &quot;Brevo API&quot;, pos: &quot;b&quot;, h: 48 }\n    DELIVER@{ icon: &quot;ngx:user&quot;, label: &quot;Recipient Inbox&quot;, pos: &quot;b&quot;, h: 48 }\n\n    COMPOSER --&gt;|&quot;campaign or transactional batch&quot;| BREVO\n    BREVO --&gt; DELIVER\n\n    CF@{ icon: &quot;ngx:cloudflare&quot;, label: &quot;Cloudflare&quot;, pos: &quot;b&quot;, h: 48 }\n    CF --&gt;|&quot;DNS: SPF / DKIM / DMARC&quot;| DELIVER\n</code></pre>\n<h3>Inbound: the Gmail inbox</h3>\n<p>Committee role addresses (secretary@, membership@, and so on) are routed by <strong>Cloudflare</strong> into a dedicated <strong>Gmail</strong> mailbox, which NGX reads through the <strong>Gmail API</strong> over OAuth — so committee mail is handled inside the app rather than forwarded to someone&#39;s personal mail client. Messages reach the app two ways:</p>\n<ul>\n<li><strong>Watch mode</strong> — Gmail publishes a change notification to <strong>Google Cloud Pub/Sub</strong>, which pushes it straight to the application. Near real-time.</li>\n<li><strong>Poll mode</strong> — the application periodically asks Gmail for the history delta since it last looked. A dependable fallback where Pub/Sub is not set up.</li>\n</ul>\n<p>Either way, each message is fetched, grouped into a thread, matched to the committee <strong>role</strong> it was addressed to, and stored in MongoDB (<code>inboxMailboxConnections</code>, <code>inboxThreads</code>, <code>inboxMessages</code>). 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.</p>\n<pre><code class=\"language-mermaid\">flowchart TB\n    SENDER@{ icon: &quot;ngx:user&quot;, label: &quot;External sender&quot;, pos: &quot;b&quot;, h: 48 }\n    CF@{ icon: &quot;ngx:cloudflare&quot;, label: &quot;Cloudflare routing&quot;, pos: &quot;b&quot;, h: 48 }\n    GMAIL@{ icon: &quot;logos:google-gmail&quot;, label: &quot;Dedicated Gmail mailbox&quot;, pos: &quot;b&quot;, h: 48 }\n    PUBSUB@{ icon: &quot;logos:google-cloud&quot;, label: &quot;Google Cloud Pub/Sub&quot;, pos: &quot;b&quot;, h: 48 }\n    APP@{ icon: &quot;ngx:ramblers&quot;, label: &quot;NGX Ramblers&quot;, pos: &quot;b&quot;, h: 48 }\n    DB@{ icon: &quot;ngx:mongodb&quot;, label: &quot;threads + messages&quot;, pos: &quot;b&quot;, h: 48 }\n    ROLE@{ icon: &quot;ngx:user&quot;, label: &quot;Committee role&quot;, pos: &quot;b&quot;, h: 48 }\n\n    SENDER --&gt;|&quot;role address&quot;| CF\n    CF --&gt;|&quot;forward&quot;| GMAIL\n    GMAIL --&gt;|&quot;watch&quot;| PUBSUB\n    PUBSUB --&gt;|&quot;push&quot;| APP\n    GMAIL --&gt;|&quot;poll: history delta&quot;| APP\n    APP --&gt;|&quot;fetch · thread · route&quot;| DB\n    DB --&gt; ROLE\n    ROLE --&gt;|&quot;reply via Brevo + writeback&quot;| GMAIL\n</code></pre>\n<h3>Live notifications</h3>\n<p>When an inbound message is stored, every member assigned to that role gets a <strong>browser push notification</strong> through a <strong>service worker</strong> and the <strong>Web Push (VAPID)</strong> protocol — no mail client open, no polling. The same signal drives the live unread badge in the admin UI.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    NEW[&quot;New inbound message&quot;]\n    APP@{ icon: &quot;ngx:ramblers&quot;, label: &quot;NGX Ramblers&quot;, pos: &quot;b&quot;, h: 48 }\n    SW[&quot;Service Worker&lt;br/&gt;Web Push · VAPID&quot;]\n    MEMBER@{ icon: &quot;ngx:user&quot;, label: &quot;Assigned member&quot;, pos: &quot;b&quot;, h: 48 }\n\n    NEW --&gt; APP\n    APP --&gt;|&quot;push to subscribers&quot;| SW\n    SW --&gt;|&quot;notification + unread badge&quot;| MEMBER\n</code></pre>\n<h3>One inbox, many providers</h3>\n<p>The reader sits behind a provider interface (<code>InboxReaderProvider</code>). Gmail (<code>gmail-api</code>) is the implementation running in production today; the same interface leaves room for IMAP and other inbound sources (<code>brevo-inbound-parse</code>, <code>cloudflare-ingress</code>) without changing how threads, routing or notifications work.</p>\n<h2>File &amp; Image Storage</h2>\n<p>User-uploaded images pass through server-side processing before reaching S3.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    UPLOAD@{ icon: &quot;logos:angular-icon&quot;, label: &quot;Upload from UI&quot;, pos: &quot;b&quot;, h: 48 }\n    PROCESS@{ icon: &quot;logos:nodejs-icon&quot;, label: &quot;Resize (Sharp)&quot;, pos: &quot;b&quot;, h: 48 }\n    STORE@{ icon: &quot;ngx:aws&quot;, label: &quot;AWS S3&quot;, pos: &quot;b&quot;, h: 48 }\n    SERVE@{ icon: &quot;ngx:user&quot;, label: &quot;Serve to browser&quot;, pos: &quot;b&quot;, h: 48 }\n\n    UPLOAD --&gt; PROCESS --&gt; STORE --&gt; SERVE\n</code></pre>\n<h2>Maps &amp; Geolocation</h2>\n<p>Multiple mapping services work together to provide rich location features.</p>\n<pre><code class=\"language-mermaid\">flowchart TD\n    LEAF@{ icon: &quot;logos:leaflet&quot;, label: &quot;Leaflet&quot;, pos: &quot;b&quot;, h: 48 }\n\n    subgraph tiles[&quot;Tile Layers&quot;]\n        OSM@{ icon: &quot;logos:openstreetmap&quot;, label: &quot;OpenStreetMap&quot;, pos: &quot;b&quot;, h: 48 }\n        OSMAP@{ icon: &quot;ngx:os-maps&quot;, label: &quot;OS Maps (UK)&quot;, pos: &quot;b&quot;, h: 48 }\n    end\n\n    subgraph search[&quot;Location Search&quot;]\n        GMAP@{ icon: &quot;logos:google-maps&quot;, label: &quot;Google Places&quot;, pos: &quot;b&quot;, h: 48 }\n        POST[&quot;Postcode Lookup&quot;]\n        ORS[&quot;OpenRouteService&quot;]\n    end\n\n    LEAF --&gt; tiles\n    LEAF --&gt; search\n\n    style tiles fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n    style search fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143\n</code></pre>\n<h2>Authentication &amp; Security</h2>\n<p>The application uses JWT for API authentication, with bcrypt for password storage and reCAPTCHA for public forms.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\n    LOGIN@{ icon: &quot;ngx:user&quot;, label: &quot;Login&quot;, pos: &quot;b&quot;, h: 48 }\n    AUTH[&quot;Passport.js + JWT&quot;]\n    HASH[&quot;bcrypt&quot;]\n    DB@{ icon: &quot;ngx:mongodb&quot;, label: &quot;Member Record&quot;, pos: &quot;b&quot;, h: 48 }\n    RECAP@{ icon: &quot;logos:recaptcha&quot;, label: &quot;reCAPTCHA v3&quot;, pos: &quot;b&quot;, h: 48 }\n    CONTACT@{ icon: &quot;ngx:user&quot;, label: &quot;Contact Form&quot;, pos: &quot;b&quot;, h: 48 }\n\n    LOGIN --&gt; AUTH --&gt; DB\n    AUTH --&gt; HASH\n    CONTACT --&gt; RECAP\n</code></pre>\n<h2>System Configuration</h2>\n<p>All external service credentials and feature flags are stored centrally in MongoDB under the <code>config</code> collection.</p>\n<pre><code class=\"language-mermaid\">flowchart TD\n    CONFIG@{ icon: &quot;ngx:mongodb&quot;, label: &quot;Config Collection&quot;, pos: &quot;b&quot;, h: 48 }\n\n    CONFIG --&gt; K1@{ icon: &quot;ngx:aws&quot;, label: &quot;AWS&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K2@{ icon: &quot;ngx:cloudflare&quot;, label: &quot;Cloudflare&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K3@{ icon: &quot;ngx:brevo&quot;, label: &quot;Brevo&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K4@{ icon: &quot;logos:google-maps&quot;, label: &quot;Google Maps&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K5@{ icon: &quot;logos:recaptcha&quot;, label: &quot;reCAPTCHA&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K6@{ icon: &quot;ngx:meetup&quot;, label: &quot;Meetup&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K7@{ icon: &quot;ngx:ramblers-hq&quot;, label: &quot;Ramblers API&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K8@{ icon: &quot;logos:google-analytics&quot;, label: &quot;Google Analytics&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K9@{ icon: &quot;logos:google-gmail&quot;, label: &quot;Gmail OAuth (inbox)&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K10@{ icon: &quot;logos:google-cloud&quot;, label: &quot;Google Cloud Pub/Sub&quot;, pos: &quot;b&quot;, h: 48 }\n    CONFIG --&gt; K11[&quot;Web Push (VAPID) keys&quot;]\n</code></pre>\n<p>The Web Push (VAPID) keys are explained in <a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-02-11-ngx-ramblers-architecture/web-push-vapid\">Web Push notifications and VAPID keys</a>.</p>\n<h2>Technology Summary</h2>\n<table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>Technology</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><strong>Frontend</strong></td>\n<td>Angular 21, Bootstrap 5, PrimeNG</td>\n<td>SPA framework, UI components</td>\n</tr>\n<tr>\n<td><strong>Backend</strong></td>\n<td>Node.js 24, Express, TypeScript</td>\n<td>API server</td>\n</tr>\n<tr>\n<td><strong>Database</strong></td>\n<td>MongoDB Atlas, Mongoose</td>\n<td>Data persistence</td>\n</tr>\n<tr>\n<td><strong>Hosting</strong></td>\n<td>Fly.io, Docker</td>\n<td>Container hosting</td>\n</tr>\n<tr>\n<td><strong>CDN/DNS</strong></td>\n<td>Cloudflare</td>\n<td>DNS, SSL, email routing, CDN</td>\n</tr>\n<tr>\n<td><strong>Storage</strong></td>\n<td>AWS S3, Sharp</td>\n<td>File storage, image processing</td>\n</tr>\n<tr>\n<td><strong>Email (outbound)</strong></td>\n<td>Brevo, Cloudflare</td>\n<td>Transactional + bulk send, domain routing, SPF/DKIM/DMARC</td>\n</tr>\n<tr>\n<td><strong>Email (inbound)</strong></td>\n<td>Gmail API, Google Cloud Pub/Sub, Web Push (VAPID)</td>\n<td>In-app committee inbox, push notifications, live unread badge</td>\n</tr>\n<tr>\n<td><strong>Maps</strong></td>\n<td>Leaflet, Google Maps, OS Maps</td>\n<td>Map rendering, geocoding</td>\n</tr>\n<tr>\n<td><strong>Walks</strong></td>\n<td>Ramblers API, Meetup API</td>\n<td>Walk data sync, event publishing</td>\n</tr>\n<tr>\n<td><strong>CI/CD</strong></td>\n<td>GitHub Actions, Docker</td>\n<td>Build, test, deploy</td>\n</tr>\n<tr>\n<td><strong>Analytics</strong></td>\n<td>Google Analytics 4</td>\n<td>User tracking</td>\n</tr>\n<tr>\n<td><strong>Security</strong></td>\n<td>JWT, Passport.js, bcrypt, reCAPTCHA</td>\n<td>Authentication, bot protection</td>\n</tr>\n</tbody></table>\n"}