Client: John Keells Foundation, part of John Keells Holdings PLC
Prepared by: CloudNavision Private Limited
Author: Azmie Sally, Founder & CEO
Version: 3.0 | Date: March 2026 | Classification: Confidential

This Architecture Design Document describes the technical architecture for the CSR Volunteer Management Platform deployed for John Keells Foundation, part of John Keells Holdings PLC. The platform enables enterprise-wide volunteer management, tracking, ratings, and impact reporting across all JKH business units.
The architecture is a fully managed, containerised deployment on Microsoft Azure with zero self-managed servers. Both the React web application and the n8n automation platform run as Azure Container Apps. All external traffic is routed exclusively through Cloudflare tunnels — no Azure resource has a public-facing origin endpoint. Outbound connectivity to Supabase is locked to a single fixed IP via NAT Gateway.
For both frontend (React/Express.js) and automation (n8n) — no VMs, no OS patching
Internal ingress only; both services use outbound tunnels, eliminating exposed origin endpoints
Within the React container, required to serve the built SPA from a Container App
For managed PostgreSQL, authentication, edge functions, and Realtime
For file storage via n8n and Microsoft Graph API
For n8n admin access only — web app uses WAF + Entra ID SSO (no double authentication for 15,000 users)
jkhmgmt.onmicrosoft.com for Azure subscription hosting only; keells.onmicrosoft.com for all users, app registrations, and employee SSO login
For non-Entra ID users — secondary authentication
With static IP for fixed egress identity — required for Supabase network allowlist
For server-side Excel report generation within the Express.js layer
1.0 vCPU / 2.0 Gi per web app replica, min 2 / max 10 replicas, scaling on HTTP concurrent connections

Figure 1: High-Level Architecture Diagram illustrating the five-layer deployment model — Users, Cloudflare Edge, Azure Container Apps, NAT Gateway, and External Services — for the CSR Volunteer Management Platform hosted on Azure (Southeast Asia / Singapore).
The two tenants serve entirely separate purposes. jkhmgmt.onmicrosoft.com is used solely for Azure subscription hosting — it contains no users and no app registrations. keells.onmicrosoft.com holds all JKH employees and is the tenant where all app registrations are created, including both the n8n automation registration and the Supabase Auth SSO registration. They do not interact.
All subdomains are exclusively under johnkeellsfoundation.com. DNS is managed by Cloudflare. The api subdomain is reserved — no DNS record should be created until required.
All Azure resources follow the pattern: JKH-{ENV}-FND-{TYPE}-{INSTANCE}

Inbound DenyAll is the primary perimeter — both Container Apps have internal-only ingress so no legitimate inbound traffic arrives directly. All external traffic arrives via outbound Cloudflare tunnels.
TUNNEL_TOKEN = <JKH-PRD-FND-TUNNEL-CSR token>VITE_SUPABASE_URL = https://<ref>.supabase.co
VITE_SUPABASE_ANON_KEY = <anon key>
VITE_ENTRA_CLIENT_ID = <client ID — keells.onmicrosoft.com tenant>
VITE_ENTRA_TENANT_ID = <tenant ID — keells.onmicrosoft.com>
VITE_ENTRA_REDIRECT_URI = https://csr.johnkeellsfoundation.com/auth/callback
VITE_LINKEDIN_CLIENT_ID = <LinkedIn OAuth client ID>
VITE_N8N_WEBHOOK_BASE_URL = https://n8n.johnkeellsfoundation.com
VITE_N8N_WEBHOOK_SECRET = <webhook auth token>The AI translation service (Google Translate API) is stored as a Supabase Edge Function secret, not a frontend variable. It is not a build argument and does not appear in the Docker build.
TUNNEL_TOKEN = <JKH-PRD-FND-TUNNEL-N8N token>
N8N_ENCRYPTION_KEY = <random 32-char string>
DB_POSTGRESDB_PASSWORD = <Supabase DB password>
SUPABASE_SERVICE_KEY = <Supabase service role key — never in frontend>N8N_HOST = n8n.johnkeellsfoundation.com
WEBHOOK_URL = https://n8n.johnkeellsfoundation.com
N8N_PROTOCOL = https
N8N_PORT = 5678
DB_TYPE = postgresdb
DB_POSTGRESDB_HOST = db.JKHFOUNDATION.supabase.co
DB_POSTGRESDB_PORT = 5432
DB_POSTGRESDB_DATABASE = postgres
DB_POSTGRESDB_SCHEMA = n8n_data
DB_POSTGRESDB_USER = postgres
EXECUTIONS_DATA_PRUNE = true
EXECUTIONS_DATA_MAX_AGE = 336Cloudflare CDN caches all static assets at the edge. For a React SPA, this means the vast majority of bytes transferred never reach the origin container. Effective concurrent connections at origin for 600 concurrent users is estimated at 120–180 (20–30% of total).
Identity provider: Microsoft Entra ID (keells.onmicrosoft.com)
All credentials are managed through two secure stores. No secret is stored in source code, Docker images, or configuration files committed to GitHub.
Supabase encryption — AES-256 managed by Supabase
TLS 1.2+ — All connections encrypted
Supabase RLS — Per-table, per-user policies on all csr_app tables
JWT — Stateless authentication via Supabase Auth tokens
This platform involves two distinct Entra ID tenants serving entirely different purposes. They must never be confused:
Both app registrations — the n8n automation registration and the Supabase Auth SSO registration — are created in keells.onmicrosoft.com. The jkhmgmt.onmicrosoft.com tenant is used exclusively for the Azure subscription and contains no users or app registrations.
Review items from original document: Contacts.Read vs Contacts.ReadWrite — recommend Contacts.Read only (least privilege). Mail.ReadWrite.Shared and Mail.Send.Shared — not required for current workflow scope; add only if workflows send from shared mailboxes.
This is a new, separate app registration required to enable Microsoft Entra ID as the SSO identity provider for volunteer and employee login via Supabase Auth. It is entirely separate from the n8n registration but resides in the same keells.onmicrosoft.com tenant.
This registration must be created in the keells.onmicrosoft.com tenant. JKH employees authenticate with their @keells.com Microsoft accounts, which live in the keells tenant. All app registrations for this platform reside in this tenant.
Feature B.19 specifies that the volunteer's SAP number is automatically populated from the Microsoft identity platform during sign-in. This requires the SAP employee ID to be stored as a custom or extension attribute on the user's Entra ID profile in keells.onmicrosoft.com.
Once confirmed, the Supabase Auth hook reads this attribute at login and populates the SAP Number field on the volunteer profile. It will not overwrite manual entries per B.19 specification.
The following optional claims must be added to the token configuration in the Azure Portal:
In Azure Portal → App Registration → Token configuration → Add optional claim → ID token:
For the groups claim: Token configuration → Add groups claim → Security groups → ID token.
Group IDs are returned as GUIDs in the token. A mapping table in the platform (configured by Central Admin per SC.6) maps these GUIDs to Business Units. JKH IT must provide the Entra ID group GUIDs that correspond to each Business Unit.
The client secret is entered directly into the Supabase Auth provider configuration. It is never stored in code, GitHub, or any config file.
Once the app registration is created, configure in Supabase Dashboard → Authentication → Providers → Azure:
In Entra ID app registration → Authentication → Redirect URIs:
https://JKHFOUNDATION.supabase.co/auth/v1/callbackIn Supabase Dashboard → Authentication → URL Configuration:
Site URL : https://volunteers.johnkeellsfoundation.com
Redirect URLs : https://volunteers.johnkeellsfoundation.com/auth/callback
https://volunteers.johnkeellsfoundation.com/**LinkedIn OAuth is provided as a secondary sign-in path for non-Entra ID users (e.g. external partners, contractors, or alumni who do not hold a @keells.com Microsoft account).
LinkedIn OAuth users do not receive SAP employee ID auto-population (B.19) or automatic Business Unit assignment (SC.6), as these rely on Entra ID group and profile attributes. Manual data entry applies.
All Supabase access is either browser-side (protected by RLS and anon key scope) or via the NAT Gateway (Container Apps server-side). Supabase dashboard access for CloudNavision and JKH IT is always permitted by Supabase independently of network restrictions.
Site URL : https://volunteers.johnkeellsfoundation.com
Redirect URLs : https://volunteers.johnkeellsfoundation.com/auth/callback
https://volunteers.johnkeellsfoundation.com/**RLS must be enabled and verified on all tables in the csr_app schema before go-live. The anon key used by the frontend grants zero access beyond what RLS policies explicitly permit.
The React app uses Supabase Realtime for live in-app notifications (Feature B.15). Each active user maintains a persistent WebSocket connection. At 600 concurrent users this consumes 600 of the available 600 pooled connections — leaving no headroom for REST API calls and direct n8n PostgreSQL connections.
//Trigger: Webhook from Web App
Send event reminders, status updates, and alerts to Microsoft Teams
Trigger: Webhook on project creation
Dispatch full project details to downstream systems
Trigger: Webhook from Web App
Upload evaluation photos, documents, and event images to SharePoint
Trigger: Teams Webhook
Process user interactions with Teams Adaptive Cards
Trigger: Scheduled / On-demand
Generate advanced reports and save to SharePoint
User navigates to volunteers.johnkeellsfoundation.com
Cloudflare routes request to ca-jkh-csr-prd via Cloudflare tunnel
Web app presents sign-in options: Microsoft Entra ID (keells.onmicrosoft.com) or LinkedIn
User authenticates with selected provider (+ MFA if configured for Entra ID)
Identity provider returns OAuth tokens to web app
Web app exchanges tokens with Supabase Auth
On first Entra ID login: SAP employee ID auto-populated from Entra ID profile (B.19)
On first Entra ID login: Group GUIDs mapped to Business Unit (SC.6)
User session established with role-based access
Volunteer browses available projects in web app
Web app fetches project data from Supabase (REST API, anon key + RLS)
Volunteer submits application
Web app creates application record in Supabase
Web app sends webhook to n8n (n8n.johnkeellsfoundation.com/webhook/...)
n8n sends Teams notification to volunteer and administrator via Microsoft Graph
n8n updates notification log in Supabase (n8n_data schema)
User uploads photo or document via web app
Web app sends file data to n8n webhook
n8n authenticates with Microsoft Graph API (using JKHMIT_FOUNDATION_n8nAuto_PRD credentials from keells.onmicrosoft.com)
n8n uploads file to SharePoint (John Keells Foundation library)
n8n returns SharePoint URL to web app
Web app stores SharePoint URL reference in Supabase
Central Admin requests report via web app
SheetJS generates Excel workbook within the Express.js server layer
Report available for immediate download from the Reports module
Optionally: n8n uploads report to SharePoint and sends Teams notification with report link
Teams integration is handled entirely through n8n workflows using the Microsoft Graph API:
For project notifications and reminders
For team announcements
For individual notifications
Action responses via webhooks
SharePoint integration is handled through n8n workflows. The web app never connects directly to SharePoint. Flow: Web app sends file data to n8n webhook → n8n authenticates with SharePoint using JKHMIT_FOUNDATION_n8nAuto_PRD service principal (Sites.Selected permission) from keells.onmicrosoft.com → n8n uploads file to designated folder in John Keells Foundation library → n8n returns SharePoint URL to web app for storage in Supabase.
Azure Application Insights (JKH-PRD-FND-APPI-001)
Request tracking, performance metrics, exceptions
Azure Log Analytics (JKH-PRD-FND-LOG-001)
Container logs, diagnostic data, custom queries
Azure Monitor Alerts
Threshold-based alerting for critical metrics
git push → main
│
▼
GitHub Actions
├── npm ci
├── npm run build (Vite — VITE_ vars injected as build args)
├── docker build (two-stage — builder + serve)
├── docker push → acrjkhjkfprd.azurecr.io/jkh-csr-app:<sha>
└── az containerapp update → ca-jkh-csr-prd (rolling redeploy)n8n is not in the CI/CD pipeline — it uses the official n8nio/n8n image updated manually via a pinned version tag.
# Stage 1 — Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY
ARG VITE_ENTRA_CLIENT_ID
ARG VITE_ENTRA_TENANT_ID
ARG VITE_ENTRA_REDIRECT_URI
ARG VITE_LINKEDIN_CLIENT_ID
ARG VITE_N8N_WEBHOOK_BASE_URL
ARG VITE_N8N_WEBHOOK_SECRET
RUN npm run build
# Stage 2 — Serve
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
COPY --from=builder /app/dist ./dist
EXPOSE 8080
CMD ["node", "server.js"]name: Build and Deploy
on:
push:
branches: [main]
env:
ACR_NAME: acrjkhjkfprd
IMAGE_NAME: jkh-csr-app
CONTAINER_APP: ca-jkh-csr-prd
RESOURCE_GROUP: JKH-PRD-FND-RG-001
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Log in to ACR
run: az acr login --name $ACR_NAME
- name: Build and push image
run: |
docker build \
--build-arg VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }} \
--build-arg VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }} \
--build-arg VITE_ENTRA_CLIENT_ID=${{ secrets.VITE_ENTRA_CLIENT_ID }} \
--build-arg VITE_ENTRA_TENANT_ID=${{ secrets.VITE_ENTRA_TENANT_ID }} \
--build-arg VITE_ENTRA_REDIRECT_URI=${{ secrets.VITE_ENTRA_REDIRECT_URI }} \
--build-arg VITE_LINKEDIN_CLIENT_ID=${{ secrets.VITE_LINKEDIN_CLIENT_ID }} \
--build-arg VITE_N8N_WEBHOOK_BASE_URL=${{ secrets.VITE_N8N_WEBHOOK_BASE_URL }} \
--build-arg VITE_N8N_WEBHOOK_SECRET=${{ secrets.VITE_N8N_WEBHOOK_SECRET }} \
-t $ACR_NAME.azurecr.io/$IMAGE_NAME:${{ github.sha }} \
-t $ACR_NAME.azurecr.io/$IMAGE_NAME:latest \
.
docker push $ACR_NAME.azurecr.io/$IMAGE_NAME:${{ github.sha }}
docker push $ACR_NAME.azurecr.io/$IMAGE_NAME:latest
- name: Deploy to Container Apps
run: |
az containerapp update \
--name $CONTAINER_APP \
--resource-group $RESOURCE_GROUP \
--image $ACR_NAME.azurecr.io/$IMAGE_NAME:${{ github.sha }}All JKH employees — potential volunteers
~4% of base — typical for web-based employee portal
Revised sizing target
Per active user — Supabase Realtime for live notifications (B.15)
Cloudflare CDN caches all static assets at the edge. Only HTML requests and API calls reach the origin container.
Estimated effective concurrent connections at origin for 600 concurrent users: 120–180 (20–30% of total).
Scale metric : http_requests (concurrent)
Target per replica: 150
Min replicas : 2
Max replicas : 10
Scale-out cooldown: 30 seconds
Scale-in cooldown : 120 secondsMinimum 2 replicas eliminates cold-start delay for the first users. Scale-in cooldown is 120s to prevent thrashing during sustained load.
Before go-live, a load test against the staging environment should validate the revised sizing.
Recommended tool: k6 (open source, integrates with Azure and GitHub Actions)
Supabase Pro Plan includes automated daily backups with point-in-time recovery (PITR).
The following sequence must be followed exactly. Steps are ordered to satisfy dependencies — each step assumes all prior steps are complete.
JKH-PRD-FND-RG-001
JKH-PRD-FND-LOG-001
JKH-PRD-FND-VNET-001
JKH-PRD-FND-NSG-ACA, JKH-PRD-FND-NSG-ACR
Attach each NSG to its subnet
JKH-PRD-FND-PIP-NAT — record static IP immediately
JKH-PRD-FND-NAT-001 → associate to JKH-PRD-FND-SNET-ACA
acrjkhjkfprd → private endpoint in JKH-PRD-FND-SNET-ACR
Add JKH-PRD-FND-PIP-NAT + JKH office IPs to allowlist
Update redirect URLs to johnkeellsfoundation.com; remove Lovable dev URLs
cae-jkh-jkf-prd → JKH-PRD-FND-SNET-ACA
Create JKH-PRD-FND-TUNNEL-CSR + JKH-PRD-FND-TUNNEL-N8N → save both tokens
ZTNA for n8n.johnkeellsfoundation.com + webhook bypass rule
Apply OWASP + bot rules to both hostnames
ca-jkh-n8n-prd — n8n + cloudflared sidecar + secrets
Cloudflare dashboard → JKH-PRD-FND-TUNNEL-N8N → Healthy
ca-jkh-csr-prd — React + cloudflared sidecar + secrets
Cloudflare dashboard → JKH-PRD-FND-TUNNEL-CSR → Healthy
AZURE_CREDENTIALS + all VITE_ vars
Push to main → verify full pipeline runs
JKH-PRD-FND-APPI-001 → linked to JKH-PRD-FND-LOG-001
Entra ID (keells tenant) → csr.johnkeellsfoundation.com
Complete open items A.3–A.4 (JKH IT)
Verify all csr_app tables have RLS enabled
Supabase dashboard → enable PITR add-on before go-live
Full user journey across all integrations
The platform is a Progressive Web App (PWA) built on React 19.
Internet Explorer is not supported. Chromium-based browsers are recommended for the best experience.
✅ Safari — Share → Add to Home Screen
✅ Chrome — Install prompt or browser menu
✅ Address bar install prompt
⚠️ Limited — read-only cached views only; active features require connectivity
✅ Supported on Android and desktop; limited on iOS (iOS 16.4+ required)
✅ Required for attendance check-in via QR code
Microsoft Entra ID and SharePoint are included in existing JKH Microsoft 365 subscriptions. AI translation (Google Translate API) is usage-based and billed separately via Google Cloud. Cost increase versus v2.0 estimate is driven by web app container scaling headroom (max 10 replicas vs 3) required to support up to 1,500 concurrent users.
This document supersedes Architecture Design Document v2.0 (March 2026). All changes are recorded in the Revision History above. The architecture described herein reflects the final agreed deployment model incorporating the pre-deployment architecture review, capacity sizing analysis, and Entra ID app registration amendments (March 2026).
3.0 — March 2026
Architecture Design Document v2.0 (March 2026)
Confidential
CloudNavision Private Limited
CSR Volunteer Management Platform