import React, {useState, useMemo} from 'react'; import styles from './styles.module.css'; import { TIERS, PROVIDERS, bomFor, DB_BRAND, REDIS_BRAND, SEARCH_BRAND, STORAGE_BRAND, LB_BRAND_VM, LB_BRAND_K8S, type Tier, type DeploymentType, type Provider, } from './data'; /** * Interactive deployment-architecture builder. * * Pick a scale tier, deployment type, hosting provider, and optional * services (Calls / Push). Renders an architecture diagram with explicit * per-connection port labels, plus a bill of materials and a network * flows table sized to those choices. */ /* ============================ Diagram primitives ============================ */ function FlowDown({port}: {port: string}) { return (
↓ {port}
); } function MiniFlow({port}: {port: string}) { return (
↓ {port}
); } function HFlow({direction, port}: {direction: 'left' | 'right'; port: string}) { const arrow = direction === 'left' ? '←' : '→'; return (
{arrow} {port}
); } function DataItem({ port, accent, label, subtitle, }: { port: string; accent: string; label: string; subtitle: string; }) { return (
{label}
{subtitle}
); } /* ============================ Architecture diagram ============================ * * 3-column CSS grid: left side / center / right side. * - Center column holds: clients, ALB, Mattermost, data tier (so they all * align vertically — ALB is locked directly above Mattermost). * - Left column holds RTCD when Calls is on. * - Right column holds Push Proxy / HPNS. * * Connection labels are minimal (just ports). The Network Flows table * below the BOM documents what each port is for. */ function ArchDiagram({ tier, k8s, provider, showCalls, pushMode, }: { tier: Tier; k8s: boolean; provider: Provider; showCalls: boolean; pushMode: 'self-hosted' | 'hpns'; }) { const showLb = tier.loadBalancer !== 'none'; const showRedis = tier.redis; const showSearch = tier.search !== 'none'; const showReplica = tier.dbReplicas > 0; const lbName = k8s ? LB_BRAND_K8S[provider] : LB_BRAND_VM[provider]; return (
{/* Client tier — labeled container, two cards (Web/Desktop + Mobile) */}
Clients
Web & Desktop
Channels web app · Windows · macOS · Linux
Mobile
iOS · Android
{/* Flow row: main HTTPS arrow from clients into LB / Mattermost */}
{/* Edge tier (ALB) — center column, locked above Mattermost */} {showLb && ( <>
{lbName}
{tier.loadBalancer === 'cluster' ? 'HA pair · TLS termination' : 'TLS termination'}
)} {/* App row: RTCD (left) — Mattermost (center) — Push (right), all in one row. * The DIRECT pill floats absolutely above the RTCD box so it doesn't claim * its own grid row (which would create an empty band across the diagram). */} {showCalls && (
direct ↓ UDP+TCP/8443
RTCD (Calls)
{tier.appNodes >= 8 ? 'HA (2–3 nodes)' : '1 node'} · audio + screen-share
)}
Mattermost{k8s ? ' (Operator)' : ''} ×{tier.appNodes}
{tier.appCpu} vCPU · {tier.appRam} GB RAM{tier.ha ? ' · HA cluster' : ''}
{pushMode === 'self-hosted' ? (
Push Proxy
self-hosted
) : (
Mattermost HPNS
managed by Mattermost
Hosted Push Notifications Service
)}
{/* Data tier — spans full width below center */}
1 ? 's' : ''}` : ''}`} /> {showRedis && ( = 8 ? 'Cluster' : 'Single'} · sessions + cluster state`} /> )} {showSearch && ( 1 ? 's' : ''} · message search`} /> )}
); } /* ============================ Network Flows table ============================ * * Documents every connection in the architecture: source → destination, * protocol, port, and purpose. Useful for firewall configuration. * Rows are conditional on the user's choices. */ type NetworkFlow = { from: string; to: string; protocol: string; port: string; direction: 'inbound' | 'internal' | 'egress'; purpose: string; }; function networkFlowsFor( tier: Tier, provider: Provider, showCalls: boolean, pushMode: 'self-hosted' | 'hpns', ): NetworkFlow[] { const flows: NetworkFlow[] = []; flows.push({ from: 'Web / Desktop / Mobile clients', to: tier.loadBalancer !== 'none' ? 'Load balancer' : 'Mattermost', protocol: 'HTTPS + WSS', port: 'TCP/443', direction: 'inbound', purpose: 'Channels API, WebSocket events, file uploads', }); if (showCalls) { flows.push({ from: 'Web / Desktop / Mobile clients', to: 'RTCD', protocol: 'UDP, TCP (fallback)', port: '8443', direction: 'inbound', purpose: 'Calls media (audio, video, screen-share) — direct, bypasses LB', }); } if (tier.loadBalancer !== 'none') { flows.push({ from: 'Load balancer', to: 'Mattermost', protocol: 'HTTP', port: 'TCP/8065', direction: 'internal', purpose: 'TLS-terminated app traffic', }); } flows.push({ from: 'Mattermost', to: `PostgreSQL (${DB_BRAND[provider]})`, protocol: 'TCP', port: '5432', direction: 'internal', purpose: 'Database queries', }); if (tier.redis) { flows.push({ from: 'Mattermost', to: `Redis (${REDIS_BRAND[provider]})`, protocol: 'TCP', port: '6379', direction: 'internal', purpose: 'Sessions, cluster coordination, cache', }); } if (tier.search !== 'none') { flows.push({ from: 'Mattermost', to: `OpenSearch (${SEARCH_BRAND[provider]})`, protocol: 'HTTPS', port: '9200', direction: 'internal', purpose: 'Message search index reads + writes', }); } flows.push({ from: 'Mattermost', to: tier.storage === 'object' ? `Object storage (${STORAGE_BRAND[provider]})` : 'Local disk', protocol: 'HTTPS', port: 'TCP/443', direction: tier.storage === 'object' ? 'internal' : 'internal', purpose: 'File uploads, plugin assets, compliance exports', }); if (showCalls) { flows.push({ from: 'Mattermost', to: 'RTCD', protocol: 'TCP', port: '8045', direction: 'internal', purpose: 'Calls signaling and orchestration API', }); } flows.push({ from: 'Mattermost', to: pushMode === 'self-hosted' ? 'Mattermost Push Proxy (self-hosted)' : 'Mattermost HPNS (managed)', protocol: 'HTTPS', port: 'TCP/443', direction: pushMode === 'self-hosted' ? 'internal' : 'egress', purpose: 'Push notification submission', }); flows.push({ from: pushMode === 'self-hosted' ? 'Mattermost Push Proxy' : 'Mattermost HPNS', to: 'Apple APNs (api.push.apple.com)', protocol: 'HTTPS', port: 'TCP/443', direction: 'egress', purpose: 'iOS push fan-out', }); flows.push({ from: pushMode === 'self-hosted' ? 'Mattermost Push Proxy' : 'Mattermost HPNS', to: 'Google FCM (fcm.googleapis.com)', protocol: 'HTTPS', port: 'TCP/443', direction: 'egress', purpose: 'Android push fan-out', }); return flows; } function NetworkFlowsTable({flows}: {flows: NetworkFlow[]}) { return ( <>

Network flows

Every connection in the architecture above — useful when configuring firewall rules, security groups, or a CAP. Inbound = from clients;{' '} internal = within the deployment boundary;{' '} egress = out to external services.

{flows.map((f, i) => ( ))}
From To Direction Protocol Port Purpose
{f.from} {f.to} {f.direction} {f.protocol} {f.port} {f.purpose}
); } /* ============================ Main component ============================ */ export default function DeploymentArchitectureBuilder() { const [tierId, setTierId] = useState('medium'); const [deploymentType, setDeploymentType] = useState('vm'); const [provider, setProvider] = useState('aws'); const [includeCalls, setIncludeCalls] = useState(false); const [selfHostPush, setSelfHostPush] = useState(true); const tier = useMemo( () => TIERS.find((t) => t.id === tierId) ?? TIERS[0], [tierId], ); const bom = useMemo( () => bomFor(tier, deploymentType, provider, {calls: includeCalls, push: selfHostPush}), [tier, deploymentType, provider, includeCalls, selfHostPush], ); const flows = useMemo( () => networkFlowsFor(tier, provider, includeCalls, selfHostPush ? 'self-hosted' : 'hpns'), [tier, provider, includeCalls, selfHostPush], ); return (
setDeploymentType(v as DeploymentType)} options={[ {value: 'vm', label: 'Virtual machines'}, {value: 'kubernetes', label: 'Kubernetes'}, ]} />

Bill of materials

{bom.map((row, i) => ( ))}
Component Quantity Recommended size / SKU Notes
{row.component} {row.quantity} {row.spec} {row.notes}

Sizing follows the Mattermost scaling guide; provider SKUs are illustrative starting points. Verify against current pricing, regional availability, and any compliance constraints before procurement.

); } /* ============================ Small UI bits ============================ */ function ControlGroup({label, children}: {label: string; children: React.ReactNode}) { return (
{label}
{children}
); } function SegmentedControl({ name, value, onChange, options, }: { name: string; value: T; onChange: (v: T) => void; options: {value: T; label: string}[]; }) { return (
{options.map((opt) => ( ))}
); }