Chat Architecture

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

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

ComponentImage / RuntimePortCPUMemoryPurpose
Synapse Homeserverapplepublicdotcom/phenom-synapse:testing80085121024 MiBMatrix homeserver (Implementation A)
Synapse Admin UIawesometechnologies/synapse-admin:latest80256512 MiBWeb-based admin dashboard for Synapse
Chat MCP Serverapplepublicdotcom/phenom-chat-mcp:testing3001256512 MiBMCP tool server with 9 chat operations
Link Preview LambdaNode.js 18.x256 MiBResolves Phenom URLs to preview cards
User Provisioner LambdaNode.js 18.x128 MiBCognito post-auth trigger, provisions chat membership
RDS PostgreSQL 17.4db.m5.large5432Synapse database + Hasura Lite chat tables

ALB Routing Rules

All traffic enters through the Application Load Balancer on chat-testing.thephenom.app. Path-based rules route requests to the correct backend:

PriorityPath PatternTargetPort
200/_matrix/*, /_synapse/*Synapse target group8008
201/chat-admin/*Synapse Admin UI target group80
300/mcp/*MCP Server target group3001

Security Groups

ALB Security Group
  ├── Inbound: 443 (HTTPS) from 0.0.0.0/0
  └── Outbound: All traffic

Synapse Security Group
  ├── Inbound: 8008 from ALB SG
  ├── Outbound: 5432 to RDS SG (PostgreSQL)
  └── Outbound: 443 to 0.0.0.0/0 (Cognito OIDC, federation)

ECS Tasks Security Group (MCP, Admin UI)
  ├── Inbound: 3001, 80 from ALB SG
  └── Outbound: All traffic

RDS Security Group
  ├── Inbound: 5432 from Synapse SG
  ├── Inbound: 5432 from ECS Tasks SG
  └── Outbound: None

Database Schema

Hasura Lite Tables (Implementation B)

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

chat_rooms

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEY, DEFAULT gen_random_uuid()Room identifier
nameTEXTNOT NULLDisplay name (“Phenom Internal”, “Phenom Partners”, “Phenom Community”)
descriptionTEXTRoom description
created_atTIMESTAMPTZDEFAULT now()Creation timestamp
is_activeBOOLEANDEFAULT trueWhether the room accepts new messages

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

chat_messages

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEY, DEFAULT gen_random_uuid()Message identifier
room_idUUIDNOT NULL, FK → chat_rooms(id)Room this message belongs to
user_idTEXTNOT NULL, FK → users(id)Sender’s user ID
contentTEXTNOT NULL, CHECK (char_length <= 4000)Message body (max 4000 chars)
message_typeTEXTDEFAULT 'text', CHECK IN ('text', 'phenom_link')Message type
phenom_idTEXTPhenom content ID (for phenom_link messages)
is_deletedBOOLEANDEFAULT falseSoft-delete flag
deleted_byTEXTFK → users(id)Who deleted the message
created_atTIMESTAMPTZDEFAULT now()Creation timestamp
updated_atTIMESTAMPTZDEFAULT 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

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEY, DEFAULT gen_random_uuid()Membership record ID
room_idUUIDNOT NULL, FK → chat_rooms(id)Room
user_idTEXTNOT NULL, FK → users(id)User
roleTEXTDEFAULT 'user', CHECK IN ('user', 'support', 'admin')Membership role
is_mutedBOOLEANDEFAULT falseWhether user is muted
muted_untilTIMESTAMPTZMute expiration (null = indefinite if muted)
joined_atTIMESTAMPTZDEFAULT now()When user joined

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

chat_bans

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEY, DEFAULT gen_random_uuid()Ban record ID
room_idUUIDNOT NULL, FK → chat_rooms(id)Room
user_idTEXTNOT NULL, FK → users(id)Banned user
banned_byTEXTNOT NULL, FK → users(id)Admin who issued the ban
reasonTEXTBan reason
banned_atTIMESTAMPTZDEFAULT now()When the ban was issued
expires_atTIMESTAMPTZBan expiration (null = permanent)

Index: idx_chat_bans_room_user on (room_id, user_id)

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEY, DEFAULT gen_random_uuid()Preview record ID
urlTEXTNOT NULLFull URL
phenom_idTEXTUNIQUEPhenom content ID
titleTEXTPreview title
descriptionTEXTPreview description
thumbnail_urlTEXTPreview image URL
media_typeTEXTContent type (video, image, etc.)
resolved_atTIMESTAMPTZDEFAULT now()When the preview was resolved
expires_atTIMESTAMPTZCache 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

LayerMechanismDetails
User identityAWS CognitoEmail + password, optional MFA
Synapse authOIDC SSOCognito as identity provider via authorization code flow
Hasura authJWT validationCognito ID token with x-hasura-* claims
MCP authCognito agent credentialsUSER_PASSWORD_AUTH flow with machine account
Admin APIBearer tokenSynapse admin access token

Role-Based Access Control (Hasura Lite)

Hasura permissions are configured per-role for each table:

Rolechat_roomschat_messageschat_memberschat_banslink_previews
userSELECTSELECT (non-deleted), INSERT (own)SELECT (own room membership)SELECT
supportSELECTSELECT, UPDATE (soft-delete)SELECTSELECT, INSERTSELECT
adminSELECT, UPDATESELECT, UPDATE, DELETESELECT, UPDATE, INSERTSELECT, INSERT, DELETESELECT, INSERT

Rate Limiting

BackendMechanismLimits
SynapseBuilt-in rc_message config10 messages/second, burst of 30
Hasura LiteApplication-level + Cognito token expiryJWT-gated; 1 message per request
MCP ServerCognito token validationAgent 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

ServiceCPU (units)Memory (MiB)Desired CountLaunch Type
Synapse51210241Fargate
Synapse Admin UI2565121Fargate
Chat MCP Server2565121Fargate

Health Checks

ServicePathIntervalTimeoutRetriesStart Period
Synapse/health30s5s360s
MCP Server/health30s5s360s
Admin UI/30s5s330s