Chat Architecture

Deep dive into the Phenom Chat system architecture, including component design, database schema, sequence diagrams, and security model.

Reconciling to the canonical page. The single source of truth for chat services is Chat Services (validated 2026-05-27). Parts of this page may still describe the retired dual-implementation design (Implementation B / Hasura Lite chat), the three-room model (Internal / Partners / Community). Chat is now Matrix/Synapse only behind an mTLS-locked origin. Where this page disagrees with the canonical page, the canonical page wins.

System Context

The chat system operates within the broader Phenom ecosystem, connecting mobile users, AI agents, and support staff through real-time messaging.

graph TB subgraph "Users" MU["Mobile App Users
(React Native)"] AI["AI Agents
(MCP clients)"] SS["Support Staff
(Synapse Admin UI)"] end subgraph "Phenom Chat System" CHAT["Chat Platform
Dual-implementation A/B
Matrix Synapse + Hasura Lite"] end subgraph "External Systems" COG["AWS Cognito
Identity Provider"] PHENOM["Phenom Backend
Hasura GraphQL Engine"] CW["CloudWatch
Logging & Monitoring"] end MU -->|"Send/receive messages"| CHAT AI -->|"9 MCP tools"| CHAT SS -->|"Admin UI"| CHAT CHAT -->|"OIDC SSO"| COG CHAT -->|"Link preview data"| PHENOM CHAT -->|"Logs & metrics"| CW style CHAT fill:#121010,color:#a5e3e8,rx:30 style COG fill:#151515,color:#e0e0e0,rx:30

Component Architecture

Component Details

Component Image / Runtime Port CPU Memory Purpose
Synapse Homeserver applepublicdotcom/phenom-synapse:testing 8008 512 1024 MiB Matrix homeserver (Implementation A)
Synapse Admin UI awesometechnologies/synapse-admin:latest 80 256 512 MiB Web-based admin dashboard for Synapse
Chat MCP Server applepublicdotcom/phenom-chat-mcp:testing 3001 256 512 MiB MCP tool server with 9 chat operations
Link Preview Lambda Node.js 18.x 256 MiB Resolves Phenom URLs to preview cards
User Provisioner Lambda Node.js 18.x 128 MiB Cognito post-auth trigger, provisions chat membership
RDS PostgreSQL 17.4 db.m5.large 5432 Synapse database + Hasura Lite chat tables

ALB Routing Rules

Traffic is split across two ALBs. chat.thephenom.app (production) lives on the prod ALB; the other hostnames stay on the dev ALB.

ALB Hostname Tier Priority range Backend target groups
phenom-prod-alb-1196696419 chat.thephenom.app production 195 (Hasura /v1/*) + module-internal (/_matrix/*, /_synapse/*, /chat-admin/*, /mcp/*) prod ECS Synapse + admin + MCP + GraphQL TGs
phenom-dev-alb-c4a9c9XX chat-staging.thephenom.app staging 240-base (module-internal, isolated chat_*_staging instances) dev ECS staging-isolated TGs

See the Deployment Guide for the full priority breakdown per hostname.

Security Groups

flowchart LR Internet([Internet 0.0.0.0/0]) ALB[ALB SG
Inbound 443/HTTPS] Synapse[Synapse SG
Inbound 8008] ECS[ECS Tasks SG
MCP + Admin UI
Inbound 3001 + 80] RDS[RDS SG
Inbound 5432
Outbound: none] Cognito([Cognito OIDC + federation]) Internet -- 443 --> ALB ALB -- 8008 --> Synapse ALB -- 3001 --> ECS ALB -- 80 --> ECS Synapse -- 5432 --> RDS ECS -- 5432 --> RDS Synapse -- 443 --> Cognito classDef sg fill:#e8f0fe,stroke:#1f6feb,color:#0b3d91,stroke-width:1.5px classDef ext fill:#fafafa,stroke:#888,color:#333 class ALB,Synapse,ECS,RDS sg class Internet,Cognito ext

Database Schema

Hasura Lite Tables (Implementation B)

All tables exist in the public schema of the shared RDS PostgreSQL instance.

chat_rooms

Column Type Constraints Description
id UUID PRIMARY KEY, DEFAULT gen_random_uuid() Room identifier
name TEXT NOT NULL Display name (“Phenom Internal”, “Phenom Partners”, “Phenom Community”)
description TEXT Room description
created_at TIMESTAMPTZ DEFAULT now() Creation timestamp
is_active BOOLEAN DEFAULT true Whether the room accepts new messages

Seeded with three rooms: Phenom Internal, Phenom Partners, Phenom Community.

chat_messages

Column Type Constraints Description
id UUID PRIMARY KEY, DEFAULT gen_random_uuid() Message identifier
room_id UUID NOT NULL, FK → chat_rooms(id) Room this message belongs to
user_id TEXT NOT NULL, FK → users(id) Sender’s user ID
content TEXT NOT NULL, CHECK (char_length <= 4000) Message body (max 4000 chars)
message_type TEXT DEFAULT 'text', CHECK IN ('text', 'phenom_link') Message type
phenom_id TEXT Phenom content ID (for phenom_link messages)
is_deleted BOOLEAN DEFAULT false Soft-delete flag
deleted_by TEXT FK → users(id) Who deleted the message
created_at TIMESTAMPTZ DEFAULT now() Creation timestamp
updated_at TIMESTAMPTZ DEFAULT now() Auto-updated via trigger

Indexes:

  • idx_chat_messages_room_created on (room_id, created_at DESC) – primary query path
  • idx_chat_messages_user on (user_id) – user message lookups

chat_members

Column Type Constraints Description
id UUID PRIMARY KEY, DEFAULT gen_random_uuid() Membership record ID
room_id UUID NOT NULL, FK → chat_rooms(id) Room
user_id TEXT NOT NULL, FK → users(id) User
role TEXT DEFAULT 'user', CHECK IN ('user', 'support', 'admin') Membership role
is_muted BOOLEAN DEFAULT false Whether user is muted
muted_until TIMESTAMPTZ Mute expiration (null = indefinite if muted)
joined_at TIMESTAMPTZ DEFAULT now() When user joined

Constraints: UNIQUE(room_id, user_id) Index: idx_chat_members_room on (room_id)

chat_bans

Column Type Constraints Description
id UUID PRIMARY KEY, DEFAULT gen_random_uuid() Ban record ID
room_id UUID NOT NULL, FK → chat_rooms(id) Room
user_id TEXT NOT NULL, FK → users(id) Banned user
banned_by TEXT NOT NULL, FK → users(id) Admin who issued the ban
reason TEXT Ban reason
banned_at TIMESTAMPTZ DEFAULT now() When the ban was issued
expires_at TIMESTAMPTZ Ban expiration (null = permanent)

Index: idx_chat_bans_room_user on (room_id, user_id)

Column Type Constraints Description
id UUID PRIMARY KEY, DEFAULT gen_random_uuid() Preview record ID
url TEXT NOT NULL Full URL
phenom_id TEXT UNIQUE Phenom content ID
title TEXT Preview title
description TEXT Preview description
thumbnail_url TEXT Preview image URL
media_type TEXT Content type (video, image, etc.)
resolved_at TIMESTAMPTZ DEFAULT now() When the preview was resolved
expires_at TIMESTAMPTZ Cache expiration

Index: idx_link_previews_phenom on (phenom_id)

Entity Relationship Diagram

erDiagram users ||--o{ chat_messages : "sends" users ||--o{ chat_members : "belongs to" users ||--o{ chat_bans : "is banned" users ||--o{ chat_bans : "bans" chat_rooms ||--o{ chat_messages : "contains" chat_rooms ||--o{ chat_members : "has" chat_rooms ||--o{ chat_bans : "enforces" chat_messages ||--o| link_previews : "may have" users { text id PK text username text display_name text avatar_url } chat_rooms { uuid id PK text name text description timestamptz created_at boolean is_active } chat_messages { uuid id PK uuid room_id FK text user_id FK text content text message_type text phenom_id boolean is_deleted text deleted_by FK timestamptz created_at timestamptz updated_at } chat_members { uuid id PK uuid room_id FK text user_id FK text role boolean is_muted timestamptz muted_until timestamptz joined_at } chat_bans { uuid id PK uuid room_id FK text user_id FK text banned_by FK text reason timestamptz banned_at timestamptz expires_at } link_previews { uuid id PK text url text phenom_id UK text title text description text thumbnail_url text media_type timestamptz resolved_at timestamptz expires_at }

Sequence Diagrams

Authentication Flow – Implementation A (Matrix/Synapse SSO)

sequenceDiagram participant App as Mobile App participant Synapse as Synapse Homeserver participant Cognito as AWS Cognito participant RDS as PostgreSQL App->>Synapse: GET /_matrix/client/v3/login/sso/redirect/cognito Synapse-->>App: 302 Redirect to Cognito authorize URL App->>Cognito: Open in-app browser → /oauth2/authorize Note over App,Cognito: User enters email + password Cognito-->>Synapse: Authorization code → /_synapse/client/oidc/callback Synapse->>Cognito: Exchange code for tokens → /oauth2/token Cognito-->>Synapse: ID token + access token Synapse->>Synapse: Map Cognito user to Matrix user (@username:server) Synapse->>RDS: Create/lookup Matrix user Synapse-->>App: 302 Redirect to phenomapp://chat/sso-callback?loginToken=xxx App->>Synapse: POST /_matrix/client/v3/login {type: m.login.token, token: xxx} Synapse-->>App: {access_token, user_id, device_id} App->>Synapse: GET /_matrix/client/v3/sync Synapse-->>App: Initial sync (rooms, messages, state)

Authentication Flow – Implementation B (Hasura Lite)

sequenceDiagram participant App as Mobile App participant Cognito as AWS Cognito participant Lambda as Token Enhancement Lambda participant Hasura as Hasura GraphQL participant Provisioner as User Provisioner Lambda App->>Cognito: InitiateAuth (email + password) Cognito->>Lambda: Pre-token generation trigger Lambda-->>Cognito: Add Hasura JWT claims Cognito-->>App: ID token (with x-hasura-* claims) Note over Cognito,Provisioner: Post-authentication trigger Cognito->>Provisioner: Post-auth event Provisioner->>Hasura: Upsert user + insert chat_members App->>Hasura: GraphQL query with Bearer token Hasura->>Hasura: Validate JWT, extract x-hasura-user-id Hasura-->>App: Query result (messages, rooms, etc.) App->>Hasura: WebSocket subscription (graphql-ws) Hasura-->>App: Real-time message stream

Send Message Flow

sequenceDiagram participant App as Mobile App participant Backend as Chat Backend
(Synapse or Hasura) participant LP as Link Preview Lambda participant RDS as PostgreSQL App->>Backend: Send message (content, room_id) alt Implementation A (Matrix) Backend->>RDS: Store event in Synapse DB Backend-->>App: Event ID else Implementation B (Hasura) Backend->>RDS: INSERT into chat_messages Backend-->>App: Message object end alt Message contains Phenom URL RDS->>LP: Hasura event trigger (INSERT on chat_messages with phenom_link type) LP->>LP: Fetch Phenom content metadata via GraphQL LP->>RDS: INSERT/UPDATE link_previews LP-->>RDS: Preview resolved end Backend-->>App: Real-time notification to other clients
sequenceDiagram participant Client as Mobile App participant Hasura as Hasura GraphQL participant Lambda as Link Preview Lambda participant PhenomAPI as Phenom Backend API Client->>Hasura: SendMessage(type: phenom_link, phenom_id: "abc123") Hasura->>Hasura: INSERT chat_messages Hasura-->>Client: Message created Note over Hasura,Lambda: Hasura event trigger fires on INSERT Hasura->>Lambda: Event payload {phenom_id: "abc123"} Lambda->>PhenomAPI: Query phenom content by ID PhenomAPI-->>Lambda: {title, description, thumbnail_url, media_type} Lambda->>Hasura: INSERT link_previews (url, title, thumbnail, ...) Hasura-->>Lambda: Preview stored Note over Client: Next query or subscription picks up the preview Client->>Hasura: Query chat_messages (includes link_preview relationship) Hasura-->>Client: Message + resolved preview card

Moderation Flow

sequenceDiagram participant Admin as Admin / AI Agent participant MCP as MCP Server participant Backend as Active Backend participant RDS as PostgreSQL Admin->>MCP: chat_delete_message(message_id) MCP->>Backend: deleteMessage() alt Hasura Backend Backend->>RDS: UPDATE chat_messages SET is_deleted = true else Synapse Backend Backend->>RDS: Redact event end Backend-->>MCP: {success: true} MCP-->>Admin: Confirmation Admin->>MCP: chat_ban_user(room_id, user_id, reason) MCP->>Backend: banUser() alt Hasura Backend Backend->>RDS: INSERT chat_bans else Synapse Backend Backend->>RDS: Set membership to "ban" in room state end Backend-->>MCP: Ban record MCP-->>Admin: Confirmation with ban details

Security Architecture

Transport Security

  • All external traffic uses HTTPS (TLS 1.2+) terminated at the ALB.
  • Internal traffic between ECS tasks and RDS uses private VPC networking (no public exposure).
  • WebSocket connections for GraphQL subscriptions use WSS (TLS-encrypted WebSockets).

Authentication & Authorization

Layer Mechanism Details
User identity AWS Cognito Email + password, optional MFA
Synapse auth OIDC SSO Cognito as identity provider via authorization code flow
Hasura auth JWT validation Cognito ID token with x-hasura-* claims
MCP auth Cognito agent credentials USER_PASSWORD_AUTH flow with machine account
Admin API Bearer token Synapse admin access token

Role-Based Access Control (Hasura Lite)

Hasura permissions are configured per-role for each table:

Role chat_rooms chat_messages chat_members chat_bans link_previews
user SELECT SELECT (non-deleted), INSERT (own) SELECT (own room membership) SELECT
support SELECT SELECT, UPDATE (soft-delete) SELECT SELECT, INSERT SELECT
admin SELECT, UPDATE SELECT, UPDATE, DELETE SELECT, UPDATE, INSERT SELECT, INSERT, DELETE SELECT, INSERT

Rate Limiting

Backend Mechanism Limits
Synapse Built-in rc_message config 10 messages/second, burst of 30
Hasura Lite Application-level + Cognito token expiry JWT-gated; 1 message per request
MCP Server Cognito token validation Agent credentials with token caching (5-min refresh margin)

Infrastructure

Terraform Module Structure

modules/
├── chat-shared/        # Cognito clients (OIDC, agent), Secrets Manager
├── chat-link-preview/  # Link preview Lambda function
├── chat-hasura-lite/   # SQL migration, Hasura metadata setup
├── chat-synapse/       # Synapse ECS service, Admin UI, security groups, DB setup
└── chat-mcp-server/    # MCP server ECS service, ALB rules

ECS Task Sizing

Service CPU (units) Memory (MiB) Desired Count Launch Type
Synapse 512 1024 1 Fargate
Synapse Admin UI 256 512 1 Fargate
Chat MCP Server 256 512 1 Fargate

Health Checks

Service Path Interval Timeout Retries Start Period
Synapse /health 30s 5s 3 60s
MCP Server /health 30s 5s 3 60s
Admin UI / 30s 5s 3 30s