Infrastructure
Overview
Jester runs as a serverless architecture on AWS. API Gateway and Lambda sit at the centre; the Lambdas and DocumentDB live in private subnets inside a VPC, with S3, SQS and Bedrock forming an asynchronous processing pipeline.
Generation is split into AI generation and HTML build / persistence, separated by SQS, with independent pipelines for articles and blogs. WAF in front, VPC endpoints for private connectivity, and Session Manager for operational access keep the setup secure.
Architecture
flowchart LR
Client([Admin UI / client]) -->|HTTPS| WAF[AWS WAF]
WAF --> APIGW[API Gateway]
subgraph AWSCloud[AWS Cloud]
subgraph VPC[VPC]
subgraph AppSubnet[Private Subnet: App]
AppLambda[App Lambda<br/>odds_poc_app]
end
subgraph GenSubnet[Private Subnet: generation]
ArticleGen[article_generation]
BlogGen[blog_generation]
BlogRewriter[blog_rewriter]
ThumbLambda[thumbnail_generation]
end
subgraph BuildSubnet[Private Subnet: builders]
ArticleBuilder[article_builder]
BlogBuilder[blog_builder]
end
subgraph DBSubnet[Private Subnet: DB]
DocumentDB[(Amazon DocumentDB)]
EC2[Amazon EC2<br/>bastion]
EC2 --> DocumentDB
end
VPCE_Int[VPC Endpoint<br/>Interface]
VPCE_S3[VPC Endpoint<br/>Gateway: S3]
end
SQS_ArticleGen[SQS<br/>article-generation]
SQS_ArticleBuild[SQS<br/>article-builder]
SQS_BlogGen[SQS<br/>blog-generation]
SQS_BlogRewrite[SQS<br/>blog-rewriter]
SQS_BlogBuild[SQS<br/>blog-builder]
SQS_Thumb[SQS<br/>thumbnail-generation]
S3[(Amazon S3)]
CloudFront[Amazon CloudFront]
Bedrock[(Amazon Bedrock)]
VPCE_Op[VPC Endpoint<br/>Session Manager]
end
APIGW --> AppLambda
AppLambda --> VPCE_Int
VPCE_Int --> SQS_ArticleGen
VPCE_Int --> SQS_BlogGen
VPCE_Int --> SQS_BlogRewrite
VPCE_Int --> SQS_Thumb
SQS_ArticleGen --> ArticleGen
SQS_BlogGen --> BlogGen
SQS_BlogRewrite --> BlogRewriter
SQS_Thumb --> ThumbLambda
ArticleGen --> SQS_ArticleBuild
BlogGen --> SQS_BlogBuild
BlogRewriter --> SQS_BlogBuild
SQS_ArticleBuild --> ArticleBuilder
SQS_BlogBuild --> BlogBuilder
AppLambda --> VPCE_S3
VPCE_S3 --> S3
S3 --> ArticleGen
S3 --> BlogGen
S3 --> BlogRewriter
S3 --> ThumbLambda
S3 --> CloudFront
CloudFront --> Client
ArticleGen --> Bedrock
BlogGen --> Bedrock
BlogRewriter --> Bedrock
ThumbLambda --> Bedrock
AppLambda --> Bedrock
AppLambda --> DocumentDB
ArticleGen --> DocumentDB
BlogGen --> DocumentDB
BlogRewriter --> DocumentDB
ArticleBuilder --> DocumentDB
BlogBuilder --> DocumentDB
ThumbLambda --> DocumentDB
Compass([MongoDB Compass]) --> SessionMgr[Session Manager]
SessionMgr --> VPCE_Op
VPCE_Op --> EC2
AWS services
| Service | Use |
|---|---|
| AWS WAF | Web application firewall in front of API Gateway |
| Amazon API Gateway | REST API endpoint management |
| AWS Lambda | Serverless compute (seven functions, all deployed as container images) |
| Amazon DocumentDB | Main database (MongoDB-compatible), in a private subnet |
| Amazon EC2 | Bastion host for DocumentDB access, reached through Session Manager |
| Amazon S3 | Race videos, course images, uploaded media and thumbnails |
| Amazon CloudFront | Public delivery of thumbnails from the private S3 bucket |
| Amazon SQS | Asynchronous message queues between Lambdas (with DLQs) |
| Amazon Bedrock | Generative AI inference (Nova Pro / Claude Sonnet / Titan Text Embeddings v2) |
| Amazon ECR | Container image registry for the Lambdas |
| VPC Endpoint | Private connectivity from the VPC to S3, SQS and others (interface and gateway types) |
| AWS Systems Manager Session Manager | Operational access path to EC2 |
Lambdas
| Lambda | Role | Language | Trigger |
|---|---|---|---|
App (odds_poc_app) |
REST API: CRUD, enqueuing async jobs, generating related information | Python | API Gateway |
article_generation |
Writes the 10-second race result summary and sends it to article-builder |
Python | SQS (article-generation) |
article_builder |
Renders the result to HTML with React and stores article.body_html |
TypeScript | SQS (article-builder) |
blog_generation |
Writes race recap / prediction blogs and sends them to blog-builder |
Python | SQS (blog-generation) |
blog_rewriter |
Rewrites past blogs and sends them to blog-builder |
Python | SQS (blog-rewrite / blog-rewriter) |
blog_builder |
Renders the blog body with an article template and stores blog.body_html |
TypeScript | SQS (blog-builder) |
thumbnail_generation |
Extracts the frame at an AI-selected moment of the race video and stores it in S3 | Python | SQS (thumbnail-generation) |
Every Lambda sits in a private subnet; traffic to S3, SQS and Bedrock goes through VPC endpoints.
Inference profiles and the Bedrock control plane
Lambdas inside the VPC cannot reach the Bedrock control plane
(bedrock.*.amazonaws.com), so when an inference profile ARN is used, base_model_id is supplied
explicitly to skip the GetInferenceProfile call. Without it, the call times out.
SQS queues
| Queue | Producer | Consumer | Notes |
|---|---|---|---|
article-generation |
App Lambda | article_generation | Sent on article generation / regeneration requests |
article-builder |
article_generation | article_builder | Sent when article generation completes |
blog-generation |
App Lambda | blog_generation | Sent on race recap / prediction blog requests |
blog-rewrite |
App Lambda | blog_rewriter | Sent by POST /v1/blogs/generation (the legacy path that creates a blog from a past blog). The payload carries only blog_id / old_blog_id and no race |
blog-rewriter |
App Lambda | blog_rewriter | Sent by POST /v1/old_blogs/{old_blog_id}/rewrite. The payload also carries race_id / racing_type |
blog-builder |
blog_generation / blog_rewriter | blog_builder | Sent when the blog body is ready |
thumbnail-generation |
App Lambda | thumbnail_generation | Sent on thumbnail generation requests |
Each queue has a dead letter queue for messages that exceed the maximum receive count.
There are two rewrite queue URLs
The App Lambda has two environment variables for dispatching rewrites:
BLOG_REWRITER_QUEUE_URL (POST /v1/old_blogs/{id}/rewrite) and
BLOG_REWRITE_QUEUE_URL (POST /v1/blogs/generation). Both are consumed by the
blog_rewriter Lambda, which handles the same message shape either way.
Final-attempt handling
Each Lambda decides whether a delivery is the final attempt from the SQS record's
ApproximateReceiveCount and the queue's redrive_policy.maxReceiveCount (environment variable
SQS_MAX_RECEIVE_COUNT, default 2).
| Situation | Behaviour |
|---|---|
| Failure on a non-final attempt | Re-raise and let SQS redelivery recover |
| Final-attempt failure (articles) | Mark the article as status=failed with the reason; nothing is left in the DLQ |
| Final-attempt failure (blogs) | Hard-delete the status=generating blog; nothing is left in the DLQ |
Why nothing is left in the DLQ
Reprocessing the same input fails for the same reason, and the target record either no longer exists or is already recorded as failed โ so a DLQ entry has no value. The policy is to keep the database and the queues in the same state.
S3 layout
| Use | Object key |
|---|---|
| Race videos | auto_racing/video/{race_id}.mp4 / bicycle_racing/video/{race_id}.mp4 / horse_racing/video/{race_id}.mp4 |
| Course images (horse racing only) | horse_racing/course_image/{race_id}.gif |
| Thumbnails (articles) | {racing_type}/thumbnail/{race_id}.jpg |
| Thumbnails (blogs) | {racing_type}/thumbnail/blog_{blog_id}.jpg |
| Uploaded media | {media.name} |
S3 is reached through a gateway VPC endpoint. The bucket is private (Block Public Access), so images that browsers need are served in one of two ways.
| Use | Delivery |
|---|---|
| Media API responses | A freshly generated S3 presigned URL valid for one hour |
| Thumbnails embedded in blog bodies | A public CloudFront URL |
VPC layout
- Private subnets by purpose
- App Lambda subnet
- Generation Lambda subnet (article_generation / blog_generation / blog_rewriter / thumbnail_generation)
- Builder Lambda subnet (article_builder / blog_builder)
- DocumentDB / EC2 bastion subnet
- VPC endpoints
- Interface: Lambda to SQS and other AWS APIs
- Gateway: Lambdas to S3
- Operational: Session Manager to EC2
Deployment
Every Lambda is deployed as a container image. GitHub Actions builds the Docker image, pushes it to ECR and updates the Lambda function image.
| Lambda | ECR repository / function name (dev) | Workflow |
|---|---|---|
| App | dev-jester-odds-poc-app / dev-jester-backend-odds-poc-app |
dev-odds-poc-app-deploy.yml |
| article_generation | dev-jester-article-generation |
dev-article-generation-deploy.yml |
| article_builder | dev-jester-article-builder |
dev-article-builder-deploy.yml |
| blog_generation | dev-jester-blog-generation |
dev-blog-generation-deploy.yml |
| blog_rewriter | dev-jester-blog-rewriter |
dev-blog-rewriter-deploy.yml |
| blog_builder | dev-jester-blog-builder |
dev-blog-builder-deploy.yml |
| thumbnail_generation | dev-jester-thumbnail-generation |
dev-thumbnail-generation-deploy.yml |
The TypeScript Lambdas (article_builder / blog_builder) run npm run typecheck and npm run lint
before the image is built.
The admin UI (jester-user-web) is built to static files and synced to S3 + CloudFront.
Operational access
- Lambda-to-Lambda and service-to-service traffic is authenticated with IAM roles
- Operational DocumentDB access goes MongoDB Compass โ Session Manager โ VPC Endpoint โ EC2 โ DocumentDB
- DocumentDB connections use TLS (
global-bundle.pem) withretryWrites=False(DocumentDB does not support retryable writes)