Chat Architecture
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.
(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
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_createdon(room_id, created_at DESC)– primary query pathidx_chat_messages_useron(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)
link_previews
| 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
Sequence Diagrams
Authentication Flow – Implementation A (Matrix/Synapse SSO)
Authentication Flow – Implementation B (Hasura Lite)
Send Message Flow
(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
Link Preview Resolution
Moderation Flow
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 |
Related Documentation
- API Reference – complete API specifications
- Admin & Operations – monitoring and troubleshooting
- Deployment Guide – Terraform deployment procedures
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.