End-to-end, low-latency voice conversation system on AWS. Users speak; we transcribe, generate an AI response, and render speech back in real time.
-
Cognito (Authentication)
-
ECS Fargate (compute)
- TypeScript with Bun runtime
- AWS SDK for Amazon Transcribe Streaming
- Openrouter and AI SDK for response generation
- ElevenLabs (text-to-speech)
-
Amazon Application Load Balancer
-
Amazon ECR (container registry)
convo-stream/
├── audio-orchestrator/ # App service: Express + Socket.IO + Transcribe + AI + TTS
│ ├── index.ts # Main server
│ ├── ai/ # OpenRouter + ElevenLabs integrations
│ ├── aws-transcribe/ # AWS Transcribe client (streaming)
│ ├── bin/health-check # Container healthcheck script (bash + curl)
│ ├── client-example.html # Simple web client
│ ├── package.json
│ └── README.md # Module-specific docs (local dev, API/events)
├── aws/
│ └── cfn/
│ ├── networking/template.yaml # VPC + Subnets + etc
│ ├── cluster/template.yaml # ECS cluster + ALB + Target Groups
│ ├── fargate-service/template.yaml # Task Definition + Service
│ └── cloudfront/template.yaml # CloudFront distribution (HTTPS in front of ALB)
├── bin/
│ ├── cfn/networking # Deploy networking stack
│ ├── cfn/cluster # Deploy cluster stack
│ ├── cfn/fargate-service # Deploy service stack
│ └── cfn/cloudfront # Deploy CloudFront stack (HTTPS + WebSockets)
│ └── ecr/
│ ├── login # ECR login
│ ├── build-push-ecr-image # Buildx multi-arch build + push to ECR
│ └── build-push-local-image# Local build (and optional Docker Hub push)
├── docker-compose.yml
├── dockerfile # Bun base image; installs curl; builds app
└── README.md # (this file)
- Health endpoint:
GET /health-check(served byaudio-orchestrator) - Health script:
audio-orchestrator/bin/health-check
Current script:
#!/usr/bin/env bash
curl -fsS http://localhost:3000/health-check || exit 1- ECS TaskDefinition healthcheck (defined in
aws/cfn/fargate-service/template.yaml):Command: - 'CMD-SHELL' - 'bash /app/bin/health-check'
- docker-compose (local):
healthcheck: test: ["CMD", "bin/health-check"]
- AWS account + credentials configured (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)
- ECR repository for the image
- SSM parameters for secrets used by the service
bin/ecr/loginexport AWS_ACCOUNT_ID=<your-id>
export AWS_DEFAULT_REGION=eu-central-1
bin/ecr/build-push-ecr-imageThis builds a multi-arch image (amd64, arm64) and pushes to EcrImage (see below).
Edit defaults in aws/cfn/fargate-service/template.yaml or override at deploy-time:
Parameters:
EcrImage:
Type: String
Default: '<your-account-id>.dkr.ecr.<your-region>.amazonaws.com/convo-stream'
SecretsAWSAccessKeyId:
Type: String
Default: 'arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/AWS_ACCESS_KEY_ID'
SecretsSecretAccessKey:
Type: String
Default: 'arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/AWS_SECRET_ACCESS_KEY'
SecretsOpenrouterApiKey:
Type: String
Default: 'arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/OPENROUTER_API_KEY'
SecretsElevenlabsApiKey:
Type: String
Default: 'arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/ELEVENLABS_API_KEY'Override example (CLI):
aws cloudformation deploy \
--stack-name ConvoStreamFargateService \
--template-file aws/cfn/fargate-service/template.yaml \
--parameter-overrides \
EcrImage=<your-account-id>.dkr.ecr.<your-region>.amazonaws.com/convo-stream:latest \
SecretsAWSAccessKeyId=arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/AWS_ACCESS_KEY_ID \
SecretsSecretAccessKey=arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/AWS_SECRET_ACCESS_KEY \
SecretsOpenrouterApiKey=arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/OPENROUTER_API_KEY \
SecretsElevenlabsApiKey=arn:aws:ssm:<region>:<account-id>:parameter/convo-stream/ELEVENLABS_API_KEY \
--capabilities CAPABILITY_NAMED_IAM# Networking
chmod u+x bin/cfn/networking
bin/cfn/networking
# Cluster (ECS + ALB + TargetGroup)
chmod u+x bin/cfn/cluster
bin/cfn/cluster
# Service (Task Definition + Service)
chmod u+x bin/cfn/fargate-service
bin/cfn/fargate-service
# CloudFront (HTTPS in front of ALB; WebSocket-enabled)
chmod u+x bin/cfn/cloudfront
bin/cfn/cloudfrontWe added CloudFront in front of the ALB to provide HTTPS while keeping the ALB origin on HTTP. CloudFront Viewer policy is set to redirect-to-https, and the origin protocol policy is http-only pointing to the ALB DNS.
To make WebSockets work through CloudFront, we forward the recommended WebSocket headers in the Origin Request Policy:
Sec-WebSocket-KeySec-WebSocket-VersionSec-WebSocket-ProtocolSec-WebSocket-AcceptSec-WebSocket-Extensions
Reference: AWS docs — Use WebSockets with CloudFront distributions
Note:
- If you customize the export name for the ALB HTTP domain in the cluster stack, pass the matching value to the CloudFront template parameter
ALBHTTPDomainNameExportNameat deploy time.
Use aws ecs execute-command to exec into the running task:
CLUSTER=ConvoStreamClusterFargateCluster
TASK_ARN=<task-arn>
CONTAINER=audio-orchestrator
# Verify working directory and files
aws ecs execute-command --cluster "$CLUSTER" --task "$TASK_ARN" --container "$CONTAINER" --interactive --command "pwd"
aws ecs execute-command --cluster "$CLUSTER" --task "$TASK_ARN" --container "$CONTAINER" --interactive --command "ls -l"
# Verify health from inside container
aws ecs execute-command --cluster "$CLUSTER" --task "$TASK_ARN" --container "$CONTAINER" --interactive --command "curl -v http://localhost:3000/health-check"
# Run the healthcheck script via bash
aws ecs execute-command --cluster "$CLUSTER" --task "$CLUSTER" --container "$CONTAINER" --interactive --command "chmod +x /app/bin/health-check && bash /app/bin/health-check"For running and testing the application server locally (Express + Socket.IO + AI/TTS), see audio-orchestrator/README.md.
