Event-driven serverless pipeline on AWS that detects sports results and pushes them to Telegram. Designed to be deployed with AWS CDK and GitHub Actions (OIDC), observable end to end, and €0 to run.
Popular live-score apps cover the big leagues, but not amateur teams or lesser-known sports. Their results are published late, on scattered websites, and I ended up checking them by hand. I wanted each score on my phone shortly after the final whistle.
Rugby league comes first, from the lowest amateur division up to the professional level. Other sports may follow later, each through its own source adapter.
It is also a side project to practise serverless and event-driven design on a real use case, where the interesting questions are what happens when a step fails, when the same event arrives twice, or when a deployment goes wrong.
- Every morning, a schedule runs the
plannerLambda. It reads the day's kick-off times on Treize Mondial. - For each match, the planner creates a one-time schedule about 2 hours after kick-off, never at night (23:00 to 08:00). No match, nothing is scheduled.
- When a schedule fires,
fetch-resultsreads the result through a source adapter and publishes aMatchFinishedevent on an EventBridge bus. If the score is not published yet, it schedules its next check itself, waiting longer each time (1 h, 1 h, 1 h, 2 h, 4 h, then 8 h, never at night), and gives the match up after 7 checks. notifyis subscribed toMatchFinished. For each event it:- checks in DynamoDB that the score was not already sent,
- sends it to Telegram,
- remembers it in DynamoDB, only once the message has gone out.
The bot token and the chat id live in SSM Parameter Store and are read once per Lambda instance.
- At-least-once delivery: send, then record. If
notifycrashes between the two steps, the score goes out twice rather than never. - Idempotence: the DynamoDB check absorbs the duplicates caused by retries.
- Dead-letter queue: an event that still fails after Lambda's retries, or that a schedule or the bus cannot deliver, lands in an SQS queue instead of disappearing.
- Alerting: CloudWatch alarms (a message in the DLQ, a match never found on the page, a planner that stopped running) email me through SNS, a channel that keeps working when Telegram does not.
- Traceability: structured JSON logs carrying the match id, so one match can be followed across all Lambdas.
- Least privilege: one IAM role per Lambda, limited to what it actually uses.
Every change goes through a pull request; main is protected and deploys itself.
- CI on each pull request: lint, types, unit tests,
cdk synthwith cdk-nag security rules,cdk diffin the job summary, commitlint, CodeQL and Gitleaks. - Merge gate: all required checks green, squash merge only, no direct push to
main. - CD on merge: each merge deploys to the GitHub
productionenvironment, which onlymaincan use. GitHub Actions assumes an AWS role through OIDC (no AWS keys stored), runscdk deploy, then a smoke test on the new Lambda versions. Theprodalias moves to the new version only if the test passes; otherwise it stays on the previous one and I get an alert. - Always on: secret scanning with push protection, Dependabot alerts and updates.
€0 in practice: the services used stay within the AWS Always Free quotas (Lambda, EventBridge Scheduler, DynamoDB in provisioned mode, SQS, SNS, CloudWatch), apart from a few custom events and the CDK assets bucket, which cost fractions of a cent.
TypeScript · AWS Lambda (Node.js, arm64) · EventBridge (Scheduler and event bus) · DynamoDB · SQS · SNS · SSM Parameter Store · CloudWatch · AWS CDK · GitHub Actions
Work in progress: the diagram shows the target architecture, built one step at a time; its dashed grey boxes are not built yet.
- Done: TypeScript project, required pull request checks (lint, types, unit tests, commitlint, Gitleaks, CodeQL), Dependabot updates, an empty CDK app synthesized on each pull request with cdk-nag rules, GitHub OIDC roles, a
cdk diffof each pull request and acdk deployon each merge tomain. - Done: the Treize Mondial adapter, the event bus and the
fetch-resultsLambda, which publishesMatchFinishedonce a match is finished. - Done: the
notifyLambda, subscribed toMatchFinished: it checks DynamoDB, sends the score to Telegram, then records it. - Done: the
plannerLambda, run every day at 10:00 (Paris time): it schedules onefetch-resultscheck per match, about 2 hours after kick-off and never at night. When the score is not published yet,fetch-resultschecks again later, with growing delays, up to 7 checks. - Done: the dead-letter queue, fed by every Lambda and every schedule or rule that invokes one, and three CloudWatch alarms that email me through SNS.
- Next: the smoke test and the
prodalias rollback.
Progress is tracked in the v1 milestone.
Each structural choice and its trade-offs are recorded in docs/decisions.md.
