A real-time group chat application built entirely on AWS serverless infrastructure. Messages are delivered instantly via WebSocket connections, authenticated through Cognito JWTs, and persisted in DynamoDB. No servers to manage, no idle costs.
Client
│
├── WebSocket (wss://) ──► API Gateway WebSocket API
│ │
│ ┌────────┴────────────┐
│ $connect sendMessage
│ (+ Authorizer) │
│ │ │
│ Lambda (app.py) Lambda (send_message.py)
│ │ │
│ ConnectionsTable ┌─────────┴──────────┐
│ (DynamoDB) ConnectionsTable MessagesTable
│ (broadcast scan) (persist msg)
│
└── Auth (token query param) ──► Lambda Authorizer (auth.py)
│
Cognito User Pool
(JWKS / RS256)
Flow:
- User registers/logs in via Cognito (USER_PASSWORD_AUTH)
- Client connects to WebSocket URL with JWT as
?token=query param - Lambda Authorizer validates the token against Cognito JWKS, returns Allow/Deny policy
- On Allow,
$connecthandler storesconnectionId + userId + emailin DynamoDB with 24h TTL - Client sends
{ "action": "sendMessage", "message": "..." } sendMessagehandler persists the message, then scans all active connections and broadcasts- Stale connections (GoneException) are cleaned up inline
- On tab close / disconnect,
$disconnectremoves the connection record
| Service | Role |
|---|---|
| API Gateway WebSocket | Persistent client connections, route by action key |
| Lambda (Python 3.12) | Connect, disconnect, send, authorize |
| DynamoDB | Connections table (active sessions) + Messages table (history) |
| Cognito User Pool | User identity, JWT issuance |
| IAM | Least-privilege policies per Lambda via SAM |
.
├── template.yaml # SAM/CloudFormation — all infrastructure as code
├── samconfig.toml # SAM CLI deployment config
├── Taskfile.yaml # Task runner (build, deploy, delete)
└── src/
├── app.py # $connect handler — stores connection in DynamoDB
├── auth.py # Lambda Authorizer — validates Cognito JWT (RS256)
├── disconnect.py # $disconnect handler — removes connection record
├── send_message.py # sendMessage handler — persists + broadcasts
└── requirements.txt # python-jose[cryptography]
Lambda Authorizer on $connect only
The JWT is validated once at connection time. API Gateway caches the authorizer result and passes userId + email through context to all subsequent route handlers — avoiding per-message token verification overhead.
TTL on ConnectionsTable
Connections are written with a 24-hour TTL. If a client disconnects without triggering $disconnect (network drop, crash), DynamoDB automatically expires the record rather than leaving ghost entries.
GoneException cleanup in sendMessage
When broadcasting, any connection that returns GoneException is collected and deleted in the same invocation. This keeps the connections table clean without a separate cleanup job.
GSI on MessagesTable (userId-timestamp-index)
Messages are keyed by UUID but indexed by userId + timestamp, allowing efficient per-user message history queries without a table scan.
IAM least privilege
Each Lambda has only the DynamoDB permissions it needs. SendMessageFunction additionally holds execute-api:ManageConnections to post back to connected clients via the management API.
- AWS CLI configured (
aws configure) - AWS SAM CLI (
pip install aws-sam-cli) - Python 3.12
- Task (optional, for Taskfile commands)
# Build and deploy (guided first-time setup)
sam build
sam deploy --guided
# Or using Taskfile
task deploySAM will output:
WebSocketURL = wss://<api-id>.execute-api.<region>.amazonaws.com/Prod
UserPoolId = <pool-id>
ClientId = <client-id>
1. Register a user
aws cognito-idp sign-up \
--client-id <ClientId> \
--username you@example.com \
--password "YourPassword1!"2. Confirm the user (skip email verification in dev)
aws cognito-idp admin-confirm-sign-up \
--user-pool-id <UserPoolId> \
--username you@example.com3. Get a JWT
aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id <ClientId> \
--auth-parameters USERNAME=you@example.com,PASSWORD="YourPassword1!"Copy the IdToken from the response.
4. Connect via WebSocket
# Install wscat if needed: npm install -g wscat
wscat -c "wss://<api-id>.execute-api.<region>.amazonaws.com/Prod?token=<IdToken>"5. Send a message
{"action": "sendMessage", "message": "hello world"}All connected clients will receive:
{"sender": "you@example.com", "message": "hello world", "timestamp": 1710000000}sam delete
# or
task delete- No REST endpoint for message history — clients only see messages sent after they connect
-
sendMessageusesscanfor broadcasting — fine for small groups, needs pagination for scale - JWKS fetched on every cold start — module-level caching would reduce latency
- No frontend client — test via
wscator build your own - No rate limiting on
sendMessageroute
Python 3.12 · AWS SAM · API Gateway WebSocket · Lambda · DynamoDB · Cognito · python-jose