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 (
);
}
/* ============================ 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
{/* 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' ? (
) : (
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.
From
To
Direction
Protocol
Port
Purpose
{flows.map((f, i) => (
{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 (
{tier.seats} — {tier.description}
Bill of materials
Component
Quantity
Recommended size / SKU
Notes
{bom.map((row, i) => (
{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 (
);
}
function SegmentedControl({
name,
value,
onChange,
options,
}: {
name: string;
value: T;
onChange: (v: T) => void;
options: {value: T; label: string}[];
}) {
return (
{options.map((opt) => (
onChange(opt.value)}
className={styles.segmentInput}
/>
{opt.label}
))}
);
}