Generated: 2026-01-15
Current Stack: Python/FastAPI on Railway.app
Purpose: Minimize AWS costs using serverless and on-demand services
This report analyzes the most cost-effective AWS architecture for Yoto Smart Stream, a service that:
- Streams audio to Yoto devices (HTTP audio delivery)
- Manages MQTT event processing from Yoto devices
- Provides web UI for audio/card management
- Handles OAuth authentication with Yoto API
- Stores user data and audio files
Key Recommendation: Hybrid serverless architecture with Lambda for API endpoints, ECS Fargate Spot for long-running MQTT, Amazon Polly for TTS, and S3 for storage can deliver professional quality at approximately $7-14/month at low-to-medium usage.
| Component | Current Implementation | Usage Pattern | Cost Driver |
|---|---|---|---|
| API Server | FastAPI on Railway | Always-on, HTTP requests | Compute time |
| MQTT Handler | Background task in FastAPI | Long-running connection | Always-on compute |
| Database | SQLite on persistent volume | Read/write for users, tokens | Storage |
| Audio Storage | Local filesystem (Railway volume) | Serve MP3 files, TTS generation | Storage + bandwidth |
| Static UI | Served by FastAPI | HTML/CSS/JS files | Compute + bandwidth |
| OAuth Tokens | File-based on persistent volume | Periodic refresh | Storage |
| TTS Generation | Amazon Polly (Neural) | On-demand audio creation | Per-character API calls |
| Background Tasks | Token refresh every 12 hours | Periodic execution | Compute |
Estimated Traffic (Low-Medium Usage Household):
- API Requests: 100-500/day (player control, card management)
- Audio Streaming: 5-20 streams/day, 5-30 MB each (children's stories)
- MQTT Events: 50-200/day (button presses, status updates)
- Static Assets: 20-50 page loads/day
- TTS Generation: 5-10/day (new cards)
- Database Queries: 200-1000/day
Key Characteristics:
- Bursty traffic (concentrated during morning/evening/bedtime)
- Long periods of inactivity (nighttime, school hours)
- Audio files served multiple times (favorite stories)
- MQTT requires persistent connection
- OAuth tokens must persist across restarts
Current Routes:
/api/auth/*- Yoto OAuth flow/api/user-auth/*- User login/logout/api/players/*- Player control (play, pause, volume)/api/cards/*- Card management, TTS generation/api/library/*- Browse Yoto library/api/streams/*- Dynamic playlist management/api/admin/*- User management/api/health- Health checks
AWS Options:
Cost: ~$1-3/month at low-medium usage
- Service: AWS Lambda (Python 3.9+) + API Gateway HTTP
- Architecture: One Lambda per route group or monolithic FastAPI on Lambda
- Pricing:
- Lambda: Free tier 1M requests/month, then $0.20 per 1M requests
- API Gateway HTTP: $1.00 per million requests
- Typical cost: 10K requests/month = $0.02 (within free tier)
Pros:
- Zero cost when idle
- Scales automatically
- No infrastructure management
- FastAPI works with Mangum adapter
Cons:
- Cold starts (2-5 seconds first request)
- 15-minute execution limit (fine for API calls)
- Requires Lambda-specific adaptations (e.g., file storage)
Implementation:
# Lambda handler with Mangum
from mangum import Mangum
from yoto_smart_stream.api import app
handler = Mangum(app, lifespan="off")Cost: ~$15-25/month (1 task, 0.5 vCPU, 1GB RAM)
- Service: ECS Fargate + ALB
- Architecture: Container running FastAPI (like Railway)
- Pricing:
- Fargate: $0.04/hour for 0.5 vCPU + $0.004/GB-hour for 1GB = ~$15/month
- ALB: $0.0225/hour = ~$16/month
- Total: ~$31/month always-on
Pros:
- No cold starts
- Full FastAPI compatibility
- Easy migration from Railway
Cons:
- Always-on cost even when idle
- More expensive than Lambda at low usage
Cost: ~$8-12/month
- Most API calls: Lambda (cost-effective)
- MQTT + Background tasks: Small Fargate task (0.25 vCPU, 0.5GB)
- Best of both worlds
Requirements:
- Maintain persistent connection to
mqtt.yoto.io - Process real-time events (button presses, playback status)
- Update database/trigger actions
- Must run 24/7
AWS Options:
Cost: ~$3-6/month for 0.25 vCPU, 0.5GB RAM
- Service: ECS Fargate Spot
- Architecture: Separate container just for MQTT handler
- Pricing:
- Fargate Spot: 70% discount vs on-demand
- 0.25 vCPU + 0.5GB = ~$5/month on-demand, ~$2-3/month with Spot
- Can tolerate interruptions (auto-reconnect)
Pros:
- Persistent connection maintained
- 70% cost savings with Spot pricing
- Auto-reconnect on interruption
Cons:
- Spot interruptions (rare, but possible)
- Slightly more complex than always-on
Implementation:
# Separate MQTT container
# Dockerfile.mqtt
FROM python:3.9-slim
COPY mqtt_handler.py .
CMD ["python", "mqtt_handler.py"]Issue: Lambda cannot maintain persistent MQTT connection (15-min limit)
Alternative: Use AWS IoT Core as MQTT proxy, but adds complexity and cost
Cost: ~$1-2/month
- Service: EC2 t4g.nano (ARM) Spot
- Pricing: $0.0017/hour = ~$1.25/month
- Pros: Very cheap, full control
- Cons: More management, less "serverless"
Current Schema:
users- User authentication, Yoto tokensevents- MQTT event log (optional, for debugging)
Data Volume: <10MB expected
AWS Options:
Cost: ~$0.50-2/month
- Service: DynamoDB with on-demand billing
- Pricing:
- $1.25 per million write requests
- $0.25 per million read requests
- $0.25/GB storage per month
- Typical usage: 1K writes, 5K reads/day = $0.50/month
Pros:
- Serverless, no provisioning
- Pay per request
- Automatic backups
- Perfect for low-traffic apps
Cons:
- NoSQL (schema change required)
- Different query patterns
Schema Migration:
# User table
{
"PK": "USER#admin",
"SK": "PROFILE",
"username": "admin",
"hashed_password": "...",
"yoto_refresh_token": "...",
"yoto_token_expires_at": 1234567890
}
# Event log
{
"PK": "EVENT#2026-01-15",
"SK": "EVENT#123456789",
"player_id": "...",
"event_type": "button_press",
"event_data": {...}
}Cost: ~$15-30/month minimum
- Service: Aurora Serverless v2
- Pricing: $0.10/ACU-hour, minimum 0.5 ACU = ~$36/month
- NOT RECOMMENDED: Too expensive for this use case
Cost: ~$13-15/month
- Service: RDS db.t4g.micro (20GB storage)
- Pricing: $0.018/hour = ~$13/month + $2.30/month storage
- Pros: SQL compatibility (no code changes)
- Cons: Always-on cost, overkill for small data
Recommendation: DynamoDB On-Demand for minimal cost, or RDS db.t4g.micro if SQL is essential
Current: Local filesystem on Railway persistent volume Requirements:
- Store MP3 files (5-30 MB each)
- Serve via HTTP for Yoto device streaming
- TTS-generated files
- Cover images (PNG, small)
Expected Storage: 500MB - 5GB
AWS Options:
Cost: ~$0.50-2/month for 2GB + bandwidth
- Service: S3 Standard
- Pricing:
- Storage: $0.023/GB/month = $0.05 for 2GB
- GET requests: $0.0004 per 1,000 = $0.02 for 50K requests
- Data transfer out: First 100GB/month free, then $0.09/GB
- Typical cost: 2GB storage + 1GB transfer/month = $1-2
Pros:
- Highly available (99.99%)
- Direct streaming from S3 (pre-signed URLs)
- Versioning and backup
- CloudFront CDN integration
Implementation:
import boto3
s3_client = boto3.client('s3')
# Generate pre-signed URL for Yoto device
url = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': 'yoto-audio', 'Key': 'story.mp3'},
ExpiresIn=3600 # 1 hour
)
# Return URL to Yoto API
card_data = {
"chapters": [{
"tracks": [{"url": url}]
}]
}Cost: ~$0.30-1.50/month for 2GB
- Service: S3 with automatic tiering
- Pricing: $0.023/GB for frequent access, $0.0125/GB for infrequent
- Pros: Auto-optimizes for favorite vs. rarely-played stories
- Cons: Slight overhead for tiering monitoring
Cost: ~$6-10/month for 2GB
- NOT RECOMMENDED: Much more expensive than S3 for this use case
- Use case: When you need POSIX filesystem (not needed here)
Recommendation: S3 Standard with CloudFront CDN for frequently-accessed files
Current: Served by FastAPI from /static directory
Files: HTML, CSS, JavaScript (~2-5 MB total)
AWS Options:
Cost: ~$0.50-1/month
- Service: S3 Static Website + CloudFront CDN
- Pricing:
- S3 hosting: $0.023/GB = $0.01 for 5MB
- CloudFront: 1TB free tier for 12 months, then $0.085/GB
- Typical cost: 50 page loads/day, 5MB each = 7.5GB/month = $0.60
Pros:
- Global CDN (low latency)
- Scales automatically
- Very cheap
Cons:
- Separate domain/setup from API
Cost: Included in API costs
- Pros: Single endpoint for UI + API
- Cons: Slower than CloudFront, consumes Lambda invocations
Recommendation: S3 + CloudFront for best performance and cost
Current: File-based (.yoto_refresh_token on Railway volume)
Requirements:
- Store OAuth refresh token securely
- Survive container restarts
- Enable token refresh every 12 hours
AWS Options:
Cost: Included in DynamoDB database costs
# Store in User record
{
"PK": "USER#admin",
"SK": "PROFILE",
"yoto_refresh_token": "...",
"yoto_access_token": "...",
"yoto_token_expires_at": 1234567890
}Cost: $0.40/month per secret + $0.05 per 10K API calls
- Service: Secrets Manager
- Pros: Designed for secrets, automatic rotation, encryption
- Cons: Overkill for single token, adds $0.40/month base cost
Cost: Free for Standard parameters
- Service: Systems Manager Parameter Store
- Pricing: Free for up to 10,000 parameters
- Pros: Free, secure, encrypted
- Cons: Requires IAM permissions
Recommendation: DynamoDB (if using) or SSM Parameter Store (free)
Current: AsyncIO task in FastAPI (periodic_token_refresh)
Frequency: Every 12 hours
Task: Refresh Yoto OAuth tokens
AWS Options:
Cost: ~$0.01/month
- Service: Lambda triggered by EventBridge (CloudWatch Events)
- Pricing:
- EventBridge: Free for first 1M events
- Lambda: 2 invocations/day × 30 days = 60 invocations (free tier)
- Cost: $0 within free tier
Implementation:
# token_refresh_lambda.py
def handler(event, context):
yoto_client = YotoClient()
yoto_client.load_token_from_dynamodb()
yoto_client.refresh_token()
yoto_client.save_token_to_dynamodb()
return {"statusCode": 200}
# EventBridge rule: rate(12 hours)Cost: Included in Fargate MQTT handler
Implementation:
# Run alongside MQTT handler
import asyncio
async def token_refresh_loop():
while True:
await asyncio.sleep(12 * 3600)
await refresh_tokens()Recommendation: Option B (include in Fargate MQTT) - simpler, no extra cost
Current: Google TTS (gTTS) library, generates MP3 files on-demand Usage: ~5-10 card creations per day
AWS Options:
Cost: ~$1-2/month
- Service: Amazon Polly Neural voices
- Pricing: $16 per 1M characters (Neural), $4 per 1M characters (Standard)
- Typical usage: 10 TTS/day × 500 characters = 150K characters/month = $2.40 (Neural) or $0.60 (Standard)
- Free tier: 5M characters/month for Standard voices (first 12 months), 1M characters/month for Neural voices (first 12 months)
- API integration: Direct boto3 calls from Lambda
Pros:
- Professional quality voices - Neural TTS sounds natural and human-like
- 60+ voices in 30+ languages and dialects
- SSML support (emotions, pauses, emphasis, pronunciation)
- Real-time streaming or file generation
- No dependencies to bundle (boto3 already in Lambda)
- Consistent with AWS-native approach
Cons:
- ~$2/month cost vs. free gTTS (still very affordable)
- Requires AWS SDK integration
Implementation:
import boto3
polly = boto3.client('polly')
response = polly.synthesize_speech(
Text='Hello from Yoto Smart Stream',
OutputFormat='mp3',
VoiceId='Joanna', # Neural voice
Engine='neural'
)
audio_stream = response['AudioStream'].read()
# Upload to S3 or stream directlyCost: $0/month (within free tier)
- Keep current implementation
- Lambda execution: ~10 seconds per TTS generation
- Cost: 10 invocations/day × 10 seconds = 100 seconds/day = 3,000 seconds/month
- Lambda GB-seconds: 3,000 seconds × 1GB = 3,000 GB-seconds (free tier: 400,000)
- Cost: $0 within free tier
Pros:
- No code changes required
- Completely free
- Simple implementation
Cons:
- Robotic voice quality - Not suitable for children's content
- Limited voices and languages
- Requires bundling gTTS dependency (~2MB)
- Not AWS-native
Recommendation: Use Amazon Polly for professional voice quality suitable for children's storytelling. The ~$2/month cost is justified by significantly better audio quality and AWS integration.
Current: Not implemented Potential Use Case: Voice commands for interactive cards, audio content indexing
AWS Option:
Cost: ~$0.50-1/month (if implemented)
- Service: Amazon Transcribe (Standard or Medical)
- Pricing: $0.024 per minute for Standard, $0.031 per minute for Medical
- Typical usage: 5 minutes/day × 30 days = 150 minutes/month = $3.60/month
- Free tier: 60 minutes/month for first 12 months
- Batch or real-time: Supports both asynchronous and streaming transcription
Pros:
- Accurate speech recognition with punctuation
- Speaker identification (diarization)
- Custom vocabulary support
- Multiple languages and dialects
- Timestamps for word-level alignment
Cons:
- Not currently a feature in the app
- Would add complexity
- Per-minute pricing can add up with heavy usage
Use Cases for Yoto:
- Voice commands: "Play next", "Pause", "Tell me a story about..."
- Content creation: Upload audio story, auto-generate transcript for metadata
- Interactive cards: Voice responses from children triggering actions
Implementation:
import boto3
transcribe = boto3.client('transcribe')
transcribe.start_transcription_job(
TranscriptionJobName='yoto-card-audio-123',
Media={'MediaFileUri': 's3://yoto-audio/input.mp3'},
MediaFormat='mp3',
LanguageCode='en-US',
OutputBucketName='yoto-transcriptions'
)
# Poll for completion or use EventBridge for notificationRecommendation: Not needed for MVP. Consider for future features if adding voice interaction to cards.
AWS Options:
Cost: ~$0.50-2/month
- Service: CloudWatch Logs (Lambda/Fargate auto-configured)
- Pricing:
- Ingestion: $0.50/GB
- Storage: $0.03/GB
- Typical usage: 100MB logs/month = $0.05 ingestion + $0.003 storage
Cost: +$0.50/month
- Service: AWS X-Ray for distributed tracing
- Pricing: Free tier: 100K traces/month
Recommendation: CloudWatch Logs (included), add X-Ray if debugging distributed issues
Estimated Monthly Cost: $7-10
┌─────────────────────────────────────────────────────────────┐
│ Users/Yoto Devices │
└────────────┬────────────────────────────────────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────┐
│ AWS API Gateway (HTTP API) │
│ • REST endpoints: /api/* │
│ • Cost: $1/million requests (~$0.50/month) │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AWS Lambda (FastAPI via Mangum) │
│ • Python 3.9, 1GB RAM │
│ • Cold start: 2-5 seconds │
│ • Routes: players, cards, library, admin, auth │
│ • Cost: Free tier (1M requests/month) │
└────────────┬────────────────────────────────────────────────┘
│
├─────────┐ ┌─────────────────────┐
│ │ │ S3 Bucket │
│ └────────────────▶│ • Audio files │
│ │ • Static UI │
│ │ • Polly TTS audio │
▼ │ Cost: ~$1/month │
┌─────────────────────────────┐ └─────────────────────┘
│ DynamoDB (On-Demand) │
│ • Users + Yoto tokens │ ┌─────────────────────┐
│ • Event log (optional) │ │ CloudFront CDN │
│ Cost: ~$0.50/month │ │ • Cache static UI │
└─────────────────────────────┘ │ • Cache audio │
│ Cost: ~$0.50/month │
┌─────────────────────────────┐ └─────────────────────┘
│ ECS Fargate Spot │
│ • MQTT Handler (0.25 vCPU) │ ┌─────────────────────┐
│ • Persistent connection │ │ EventBridge │
│ • Background token refresh │────────▶│ • Token refresh │
│ • Auto-reconnect on spot │ │ • every 12 hours │
│ Cost: ~$2-3/month │ │ Cost: Free │
└─────────────────────────────┘ └─────────────────────┘
┌─────────────────────┐
│ Amazon Polly │
│ • Neural TTS │
│ • On-demand API │
│ Cost: ~$2/month │
└─────────────────────┘
TOTAL COST: ~$7-10/month
Pros:
- Low cost with professional voice quality
- Scales to zero for API (Lambda)
- MQTT always-on but cheap (Spot)
- AWS-native TTS integration
Cons:
- Cold starts on API Gateway
- Spot interruptions (rare, auto-recovers)
- More complex deployment
Estimated Monthly Cost: $35-49
┌─────────────────────────────────────────────────────────────┐
│ Users/Yoto Devices │
└────────────┬────────────────────────────────────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────┐
│ Application Load Balancer (ALB) │
│ • Public endpoint │
│ • SSL termination │
│ • Cost: ~$16/month │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ECS Fargate (1 task) │
│ • FastAPI + MQTT combined │
│ • 0.5 vCPU, 1GB RAM │
│ • No cold starts │
│ • Cost: ~$15/month │
└────────────┬────────────────────────────────────────────────┘
│
├─────────┐ ┌─────────────────────┐
│ │ │ S3 + CloudFront │
│ └────────────────▶│ • Audio storage │
│ │ • Static UI │
▼ │ Cost: ~$1.50/month │
┌─────────────────────────────┐ └─────────────────────┘
│ RDS db.t4g.micro │
│ • PostgreSQL (or Aurora) │ ┌─────────────────────┐
│ • 20GB storage │ │ Amazon Polly │
│ • SQL compatibility │ │ • Neural TTS │
│ Cost: ~$15/month │ │ Cost: ~$2/month │
└─────────────────────────────┘ └─────────────────────┘
TOTAL COST: ~$49/month (with RDS)
TOTAL COST: ~$35/month (with DynamoDB instead of RDS)
Pros:
- No cold starts (instant API responses)
- SQL database (easy migration)
- Simpler deployment (like Railway)
- Professional TTS quality
Cons:
- Always-on cost even when idle
- More expensive than Lambda at low usage
Estimated Monthly Cost: $10-14
┌─────────────────────────────────────────────────────────────┐
│ Users / Yoto Devices │
└────────────┬────────────────────────────────────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway (HTTP API) │
│ • /api/players, /api/cards, /api/library │
│ • Lambda backends (cold starts acceptable) │
│ • Cost: ~$0.50/month │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Lambda Functions │
│ • Read-only operations (players, library, health) │
│ • Create cards, Amazon Polly TTS generation │
│ • Cost: ~$1/month │
└────────────┬────────────────────────────────────────────────┘
│
├─────────┐ ┌─────────────────────┐
│ │ │ S3 + CloudFront │
│ └────────────────▶│ • Audio + UI │
│ │ Cost: ~$1.50/month │
▼ └─────────────────────┘
┌─────────────────────────────┐
│ DynamoDB (On-Demand) │ ┌─────────────────────┐
│ • Users, tokens, events │ │ ECS Fargate Spot │
│ Cost: ~$1/month │◀────────│ • MQTT Handler │
└─────────────────────────────┘ │ • 0.25 vCPU, 0.5GB │
│ • Token refresh │
│ Cost: ~$3/month │
└─────────────────────┘
┌─────────────────────────────┐ ┌─────────────────────┐
│ CloudWatch │ │ Amazon Polly │
│ • Logs + Metrics │ │ • Neural TTS │
│ Cost: ~$0.50/month │ │ Cost: ~$2/month │
└─────────────────────────────┘ └─────────────────────┘
TOTAL COST: ~$10-14/month
Pros:
- Balanced cost vs. performance
- API scales to zero (Lambda)
- MQTT always-on (Fargate Spot)
- Cold starts only on infrequent APIs
- Professional voice quality
Cons:
- Slightly more complex than single Fargate
- Cold starts on first API call after idle
| Component | Arch 1 (Minimal) | Arch 2 (No Cold Starts) | Arch 3 (Hybrid) | Current (Railway) |
|---|---|---|---|---|
| API Endpoints | Lambda (~$0.50) | Fargate ($15) | Lambda ($1) | Included |
| MQTT Handler | Fargate Spot ($3) | Included above | Fargate Spot ($3) | Included |
| Database | DynamoDB ($1) | RDS ($15) | DynamoDB ($1) | SQLite (volume) |
| Audio Storage | S3 ($1) | S3 ($1.50) | S3 ($1.50) | Volume ($5-10) |
| Static UI | CloudFront ($0.50) | ALB (included) | CloudFront ($0.50) | Included |
| TTS | Polly Neural ($2) | Polly Neural ($2) | Polly Neural ($2) | gTTS (free) |
| Load Balancer | None | ALB ($16) | None | Included |
| Background Jobs | Included | Included | Included | Included |
| Monitoring | CloudWatch ($0.50) | CloudWatch ($0.50) | CloudWatch ($0.50) | Included |
| TOTAL/MONTH | $7-10 | $49 (RDS) / $35 (DynamoDB) | $10-14 | $5 (Hobby) / $20 (Pro) |
Railway Comparison:
- Railway Hobby: $5/month (500 hours execution, $10 credit)
- Railway Pro: $20/month (unlimited execution)
Key Insight: Architecture 1 (Minimal) now $7-10/month with Polly but with unlimited scalability. Architecture 3 (Hybrid) provides best balance for $10-14/month.
-
Move audio files to S3
- Use AWS CLI:
aws s3 sync ./audio_files s3://yoto-audio/ - Update code to generate S3 pre-signed URLs
- Test audio streaming from S3
- Use AWS CLI:
-
Move database to DynamoDB
- Export SQLite to JSON:
sqlite3 db.db .dump - Transform schema to DynamoDB single-table design
- Bulk load with
boto3.batch_write_item()
- Export SQLite to JSON:
- Create Docker image for MQTT-only handler
- Deploy to ECS Fargate Spot
- Configure environment variables (client ID, tokens)
- Verify MQTT connection and event processing
- Install Mangum adapter:
pip install mangum - Create Lambda handler:
handler = Mangum(app, lifespan="off") - Package dependencies:
pip install -t ./package -r requirements.txt - Deploy via AWS SAM or Terraform
- Configure API Gateway to trigger Lambda
- Upload static files to S3:
aws s3 sync ./static s3://yoto-ui/ - Enable S3 static website hosting
- Create CloudFront distribution pointing to S3
- Update DNS records
- Create EventBridge rule:
rate(12 hours) - Create Lambda for token refresh
- Test token refresh flow
# yoto_smart_stream/storage.py
from abc import ABC, abstractmethod
import boto3
from pathlib import Path
class StorageBackend(ABC):
@abstractmethod
def save_audio(self, file_id: str, data: bytes) -> str:
pass
@abstractmethod
def get_audio_url(self, file_id: str, expires_in: int = 3600) -> str:
pass
class S3StorageBackend(StorageBackend):
def __init__(self, bucket_name: str):
self.s3 = boto3.client('s3')
self.bucket = bucket_name
def save_audio(self, file_id: str, data: bytes) -> str:
key = f"audio/{file_id}.mp3"
self.s3.put_object(Bucket=self.bucket, Key=key, Body=data)
return key
def get_audio_url(self, file_id: str, expires_in: int = 3600) -> str:
return self.s3.generate_presigned_url(
'get_object',
Params={'Bucket': self.bucket, 'Key': f"audio/{file_id}.mp3"},
ExpiresIn=expires_in
)
class LocalStorageBackend(StorageBackend):
def __init__(self, base_path: Path):
self.base_path = base_path
def save_audio(self, file_id: str, data: bytes) -> str:
path = self.base_path / f"{file_id}.mp3"
path.write_bytes(data)
return str(path)
def get_audio_url(self, file_id: str, expires_in: int = 3600) -> str:
# Return local URL (requires FastAPI to serve)
return f"http://localhost:8080/audio/{file_id}.mp3"
# Configuration
def get_storage_backend() -> StorageBackend:
if os.getenv('AWS_S3_BUCKET'):
return S3StorageBackend(os.getenv('AWS_S3_BUCKET'))
else:
return LocalStorageBackend(Path('audio_files'))# yoto_smart_stream/db_adapter.py
from abc import ABC, abstractmethod
class DatabaseAdapter(ABC):
@abstractmethod
def get_user(self, username: str) -> dict:
pass
@abstractmethod
def save_user(self, user: dict):
pass
class DynamoDBAdapter(DatabaseAdapter):
def __init__(self, table_name: str):
import boto3
self.dynamodb = boto3.resource('dynamodb')
self.table = self.dynamodb.Table(table_name)
def get_user(self, username: str) -> dict:
response = self.table.get_item(
Key={'PK': f'USER#{username}', 'SK': 'PROFILE'}
)
return response.get('Item')
def save_user(self, user: dict):
self.table.put_item(Item={
'PK': f"USER#{user['username']}",
'SK': 'PROFILE',
**user
})
class SQLiteAdapter(DatabaseAdapter):
def __init__(self, db_url: str):
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
self.engine = create_engine(db_url)
self.Session = sessionmaker(bind=self.engine)
def get_user(self, username: str) -> dict:
session = self.Session()
user = session.query(User).filter(User.username == username).first()
return user.__dict__ if user else None
def save_user(self, user: dict):
session = self.Session()
user_obj = User(**user)
session.add(user_obj)
session.commit()# lambda_handler.py
from mangum import Mangum
from yoto_smart_stream.api.app import create_app
app = create_app()
handler = Mangum(app, lifespan="off")
# Note: Disable background tasks in Lambda (use EventBridge instead)# mqtt_standalone.py
import asyncio
import logging
from yoto_smart_stream.core import YotoClient
from yoto_smart_stream.db_adapter import get_db_adapter
async def main():
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize clients
db = get_db_adapter()
yoto_client = YotoClient()
# Load tokens from database
admin_user = db.get_user('admin')
yoto_client.set_refresh_token(admin_user['yoto_refresh_token'])
await yoto_client.ensure_authenticated()
# Start MQTT connection
logger.info("Starting MQTT handler...")
await yoto_client.start_mqtt()
# Background token refresh
async def token_refresh_loop():
while True:
await asyncio.sleep(12 * 3600) # 12 hours
logger.info("Refreshing OAuth tokens...")
await yoto_client.ensure_authenticated()
# Save updated token to database
admin_user['yoto_refresh_token'] = yoto_client.get_refresh_token()
db.save_user(admin_user)
# Run both tasks
await asyncio.gather(
yoto_client.run_mqtt_loop(),
token_refresh_loop()
)
if __name__ == '__main__':
asyncio.run(main())# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
YotoClientId:
Type: String
Description: Yoto API Client ID
Resources:
# S3 Bucket for Audio Storage
AudioBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: yoto-smart-stream-audio
CorsConfiguration:
CorsRules:
- AllowedOrigins: ['*']
AllowedMethods: [GET]
AllowedHeaders: ['*']
# DynamoDB Table
DataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: yoto-smart-stream
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
# API Lambda Function
ApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: yoto-api
Runtime: python3.9
Handler: lambda_handler.handler
MemorySize: 1024
Timeout: 30
Environment:
Variables:
YOTO_CLIENT_ID: !Ref YotoClientId
AWS_S3_BUCKET: !Ref AudioBucket
DYNAMODB_TABLE: !Ref DataTable
Policies:
- S3CrudPolicy:
BucketName: !Ref AudioBucket
- DynamoDBCrudPolicy:
TableName: !Ref DataTable
Events:
ApiGateway:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
# ECS Fargate for MQTT Handler
MQTTCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: yoto-mqtt-cluster
CapacityProviders:
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE_SPOT
Weight: 1
MQTTTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: yoto-mqtt-handler
RequiresCompatibilities: [FARGATE]
NetworkMode: awsvpc
Cpu: 256 # 0.25 vCPU
Memory: 512 # 0.5 GB
ContainerDefinitions:
- Name: mqtt-handler
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/yoto-mqtt:latest
Essential: true
Environment:
- Name: YOTO_CLIENT_ID
Value: !Ref YotoClientId
- Name: DYNAMODB_TABLE
Value: !Ref DataTable
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: /ecs/yoto-mqtt
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
MQTTService:
Type: AWS::ECS::Service
Properties:
ServiceName: yoto-mqtt-service
Cluster: !Ref MQTTCluster
TaskDefinition: !Ref MQTTTaskDefinition
DesiredCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
Subnets:
- !Ref PublicSubnet
SecurityGroups:
- !Ref MQTTSecurityGroup
AssignPublicIp: ENABLED
# EventBridge Rule for Token Refresh
TokenRefreshRule:
Type: AWS::Events::Rule
Properties:
Description: Refresh Yoto OAuth tokens every 12 hours
ScheduleExpression: rate(12 hours)
State: ENABLED
Targets:
- Arn: !GetAtt TokenRefreshFunction.Arn
Id: TokenRefreshTarget
TokenRefreshFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: yoto-token-refresh
Runtime: python3.9
Handler: token_refresh.handler
MemorySize: 256
Timeout: 60
Environment:
Variables:
DYNAMODB_TABLE: !Ref DataTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DataTable
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/
AudioBucketName:
Description: S3 bucket for audio files
Value: !Ref AudioBucket# Build and deploy with AWS SAM
sam build
sam deploy --guided --parameter-overrides YotoClientId=YOUR_CLIENT_ID
# Or use Terraform (alternative)
terraform init
terraform plan
terraform apply- Use lower bitrate MP3 (128 kbps vs. 256 kbps) for children's stories
- Saves 50% storage and bandwidth
- Savings: $0.50-1/month
- Cache audio files for 24 hours (frequently-played stories)
- Reduces S3 GET requests by 80%
- Savings: $0.20-0.50/month
- Move old/unused audio to S3 Glacier after 90 days
- Reduces storage cost by 80% for archived files
- Savings: $0.10-0.50/month (depending on usage)
- If Lambda usage exceeds free tier, purchase Savings Plan
- 1-year commitment: 17% discount, 3-year: 25% discount
- Savings: 17-25% on Lambda costs (if > $10/month)
- Lambda: 1M requests + 400K GB-seconds/month free (permanent)
- API Gateway: 1M requests/month free (12 months)
- DynamoDB: 25GB storage + 200M requests/month free (permanent)
- CloudFront: 1TB transfer + 10M requests/month free (12 months)
- S3: 5GB storage + 20K GET requests/month free (12 months)
Total Free Tier Value: ~$20-30/month for 12 months, then ~$10-15/month permanently
- ECS Fargate Spot: 70% discount vs. on-demand
- Savings: $5-7/month on MQTT handler
- For global users, consider:
- CloudFront for audio (automatic global CDN)
- S3 Cross-Region Replication (only if needed)
- Trade-off: Adds complexity, benefits only multi-region users
Impact:
- First request after idle: 2-5 seconds
- Subsequent requests: <100ms
Mitigation:
- Provisioned Concurrency (adds cost: $10-20/month for 1 instance)
- Warming Lambda (EventBridge ping every 5 minutes - hacky)
- Accept cold starts (acceptable for non-critical APIs)
Recommendation: Accept cold starts for low-traffic home use
Requirement: Real-time event processing (<1 second)
Solution: ECS Fargate maintains persistent MQTT connection
- No cold starts
- Events processed immediately
- Auto-reconnects on network issues
Requirement: Low latency, no buffering issues
Solution:
- S3 with CloudFront CDN
- Pre-signed URLs with 1-hour expiry
- Range request support (seeking)
Performance:
- First byte latency: 50-200ms (CloudFront edge)
- Throughput: 10-100 Mbps (sufficient for MP3)
Current: File-based on persistent volume
AWS Options:
- DynamoDB: Encrypted at rest (KMS)
- Secrets Manager: Designed for secrets ($0.40/month)
- SSM Parameter Store: Free, encrypted (KMS)
Recommendation: DynamoDB with encryption at rest (KMS)
Current: Session cookies with JWT
AWS Integration:
- Lambda Authorizer for API Gateway
- Validate JWT in Lambda before API execution
- Store sessions in DynamoDB
Audio Files:
- Private bucket (no public access)
- Pre-signed URLs with short expiry (1 hour)
- CloudFront signed URLs for UI assets
Implementation:
# Generate pre-signed URL
url = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': 'yoto-audio', 'Key': 'story.mp3'},
ExpiresIn=3600 # 1 hour
)Principle of Least Privilege:
- Lambda execution role: S3 read/write, DynamoDB CRUD
- ECS task role: DynamoDB CRUD, Secrets Manager read
- No hard-coded credentials in code
Key Metrics:
- Lambda invocations, errors, duration
- API Gateway 4xx/5xx errors
- ECS CPU/memory utilization
- DynamoDB read/write capacity
- S3 GET requests, data transfer
Alarms:
- Lambda error rate > 5%
- API Gateway 5xx errors > 1%
- ECS task stopped (MQTT disconnected)
- DynamoDB throttling
Log Groups:
/aws/lambda/yoto-api- API logs/ecs/yoto-mqtt- MQTT handler logs/aws/lambda/yoto-token-refresh- Token refresh logs
Log Retention: 7-30 days (cost: $0.03/GB)
Use Case: Distributed tracing across Lambda, API Gateway, S3, DynamoDB
Cost: Free tier: 100K traces/month
DynamoDB:
- Point-in-time recovery (PITR): $0.20/GB/month
- On-demand backups: $0.10/GB
- Recommendation: Enable PITR for user data
S3:
- S3 Versioning: Keep old versions of audio files
- Cross-Region Replication (optional): Copy to second region
- Cost: Minimal for small dataset (<5GB)
Critical: Yoto refresh token must survive failures
Strategy:
- Store in DynamoDB (auto-replicated across AZs)
- Backup to SSM Parameter Store (redundancy)
- Export to encrypted S3 bucket (disaster recovery)
| Aspect | Railway (Current) | AWS Arch 1 (Minimal) | AWS Arch 3 (Hybrid) |
|---|---|---|---|
| Monthly Cost | $5 (Hobby) - $20 (Pro) | $5-8 | $8-12 |
| Deployment | Git push (simple) | SAM/Terraform (complex) | SAM/Terraform (complex) |
| Scaling | Limited (Hobby: 500 hours) | Unlimited | Unlimited |
| Cold Starts | No | Yes (API Gateway) | Yes (API Gateway) |
| MQTT | Always-on | Fargate Spot (interruptions) | Fargate Spot (interruptions) |
| Database | SQLite (volume) | DynamoDB (NoSQL) | DynamoDB (NoSQL) |
| Storage | Persistent volume | S3 (object storage) | S3 (object storage) |
| Monitoring | Railway dashboard | CloudWatch | CloudWatch |
| Free Tier | 500 hours/month credit | 1M Lambda + 25GB DynamoDB | 1M Lambda + 25GB DynamoDB |
Recommendation:
-
Stay on Railway if:
- Simplicity is priority
- Hobby plan (500 hours) is sufficient
- Don't want to manage AWS complexity
-
Move to AWS if:
- Need unlimited scaling
- Want to optimize costs at higher usage
- Comfortable with AWS services
- Need multi-region or advanced features
Architecture 1 (Minimal - Lambda + Fargate Spot): $5-8/month
- Lambda for API endpoints (scales to zero)
- ECS Fargate Spot for MQTT (70% discount)
- DynamoDB on-demand (pay per request)
- S3 + CloudFront for storage and CDN
When to Use:
- Low-to-medium traffic (100-1000 requests/day)
- Cost is top priority
- Acceptable to have cold starts on API
- Home/personal use
Architecture 3 (Hybrid - Lambda + Fargate): $8-12/month
- Lambda for most APIs (cold starts on infrequent calls)
- ECS Fargate Spot for MQTT (always-on)
- DynamoDB on-demand
- S3 + CloudFront
When to Use:
- Balance between cost and performance
- Most APIs can tolerate cold starts
- MQTT must be always-on
- Small-to-medium user base
Effort: 40-60 hours (spread over 2-4 weeks)
Phases:
- Storage migration (S3) - 8 hours
- Database migration (DynamoDB) - 16 hours
- API Lambda deployment - 12 hours
- MQTT Fargate deployment - 8 hours
- UI on S3/CloudFront - 4 hours
- Testing and debugging - 12 hours
Skills Required:
- AWS basics (IAM, S3, Lambda)
- Docker (for Fargate)
- Infrastructure as Code (SAM or Terraform)
For Home/Personal Use:
- Stay on Railway Hobby ($5/month) if current usage fits within 500 hours
- Migrate to AWS Arch 1 ($5-8/month) if need unlimited usage
For Small Business/Multiple Users:
- AWS Arch 3 (Hybrid - $8-12/month) for best cost/performance balance
For Growth/Scale:
- AWS Arch 2 (No Cold Starts - $33/month with DynamoDB) when user base grows
If proceeding with AWS migration:
- Week 1: Setup AWS account, IAM roles, S3 bucket
- Week 2: Migrate audio files to S3, test streaming
- Week 3: Deploy MQTT handler to Fargate, test events
- Week 4: Deploy API to Lambda, test all endpoints
- Week 5: Setup static UI on S3/CloudFront
- Week 6: Cutover DNS, decommission Railway
Rollback Plan: Keep Railway running for 1-2 weeks during migration for easy rollback.
- Purpose: Object storage for audio files, static UI
- Pricing: $0.023/GB/month + $0.0004 per 1,000 GET requests
- Use Case: Store MP3 files, serve via pre-signed URLs
- Purpose: Serverless compute for API endpoints
- Pricing: $0.20 per 1M requests + $0.00001667 per GB-second
- Free Tier: 1M requests + 400K GB-seconds/month (permanent)
- Use Case: FastAPI endpoints (players, cards, library)
- Purpose: HTTP endpoint for Lambda functions
- Pricing: $1.00 per million requests
- Use Case: Route /api/* to Lambda functions
- Purpose: NoSQL database for users, tokens, events
- Pricing (On-Demand): $1.25 per million writes, $0.25 per million reads
- Free Tier: 25GB storage + 200M requests/month (permanent)
- Use Case: Store user data, Yoto OAuth tokens
- Purpose: Containerized workloads without managing servers
- Pricing: $0.04048/hour per vCPU + $0.004445/hour per GB RAM
- Fargate Spot: 70% discount (interruptions possible)
- Use Case: Run MQTT handler with persistent connection
- Purpose: CDN for caching static UI and audio files
- Pricing: $0.085/GB for first 10TB (US/Europe)
- Free Tier: 1TB transfer + 10M requests/month (12 months)
- Use Case: Serve static UI, cache popular audio files
- Purpose: Scheduled tasks (cron jobs)
- Pricing: Free for first 1M events/month
- Use Case: Token refresh every 12 hours
- Purpose: Logs, metrics, alarms
- Pricing: $0.50/GB ingested, $0.03/GB stored
- Use Case: Application logs, Lambda/ECS metrics
Document Version: 1.0
Last Updated: 2026-01-15
Author: GitHub Copilot Workspace
Audience: Developers, Solution Architects, AI Agents