Keeping supporters up to date using AWS Events and Web Push (part 2)

Parsing the HTML and using an AWS fan-out architecture to publish the results

Keeping supporters up to date using AWS Events and Web Push (part 2)

This is part 2 of a series of articles showing how I built a solution to keep supporters of a sports club up to date using an event-driven architecture in AWS and Web Push notifications.

The first part can be found here.


  • Keep supporters up to date as much as possible during the games

  • Use an event-driven architecture in AWS

  • Require no manual intervention (I’ve better things to do at the weekend)

  • Minimal cost as it’s running in my AWS account

Part 2

Now I have the raw HTML for the current score on an SQS queue it needs parsing to create a JSON representation of the scorecard which will be used to make several updates:

  • JSON object in an S3 bucket

  • WebSockets

  • Web Push notifications

  • Update the result in CMS when the game is over

  • Teardown EC2 instance when the game is over

  • In the near future create a ChatGPT match report when the game is over

This article will deal with updating the S3 bucket and the game over updates — WebSockets and Web Push will be covered in parts 3 & 4.

Creating the scorecard JSON

To create the scorecard I created a Lambda, create-scorecard, that reads the HTML from the SQS and uses Cheerio to parse it and create an object. Once the object has been created it is validated using Zod against this schema:

import { z } from 'zod';

const BowlingFiguresSchema = z.object({
  name: z.string(),
  overs: z.string(),
  maidens: z.string(),
  runs: z.string(),
  wickets: z.string(),
  wides: z.string(),
  noBalls: z.string(),
  economyRate: z.string(),

const PlayerInningsSchema = z.object({
  name: z.string(),
  runs: z.string(),
  balls: z.string(),
  minutes: z.string(),
  fours: z.string(),
  sixes: z.string(),
  strikeRate: z.string(),
  howout: z.array(z.string()),

const InningsSchema = z.object({
  batting: z.object({
    innings: z.array(PlayerInningsSchema),
    extras: z.string(),
    total: z.string(),
    team: z.string(),
  fallOfWickets: z.string(),
  bowling: z.array(BowlingFiguresSchema),

const ScorecardSchema = z.object({
  url: z.string(),
  teamName: z.string(),
  result: z.string().nullable(),
  innings: z.array(InningsSchema),

This schema can then be used by any downstream service to validate that the message received is a valid scorecard.

Making the updates

Once the JSON had been created the various updates needed to be made. This could easily be done from the same Lambda that creates it however I like my Lambda functions to adhere to the Single Responsibility Principle as much as I can and only perform one function. This means creating a single Lambda to perform each update (update S3, teardown the EC2 instance, update the CMS) and pass the JSON to each one.

AWS SNS allows the JSON to be published to a topic and multiple Lambdas (amongst other services) to subscribe to the topic. So I created a scorecard-updated topic and added a subscription for each of the Lambda functions and the create-scorecard Lambda publishes the JSON to the topic.

Each Lambda validates the incoming message using the Zod schema defined in the previous section in case rogue messages have been published to the topic.

Update S3

The update-bucket Lambda updates the S3 Object, it simply creates a key for the object based on the team and the match date and PUTs the object using that key. The PUT operation will either create a new object or overwrite an existing one.

Update CMS

The update-sanity Lambda checks if the match has a result and, if it does, makes an update to Sanity. The fixtures should already exist in Sanity so firstly it makes a query to find the fixture based on team and date and, if one is found, makes a PATCH request for the fixture setting the result.

Teardown EC2 Instance

The update-processors Lambda also checks if the match has a result and, if it does, checks if the EC2 instance needs terminating. When the EC2 instance is created two tags are added, firstly the Owner which is set to cleckheaton-cc and secondly InProgress which is a count of the number of matches currently being processed by the instance.

This Lambda finds the instance with and Owner of cleckheaton-cc and checks the InProgress count, if the count is 1 then, as the match has now been completed the instance can be terminated. If the count is > 1 then the count is decreased by 1 and the tag is updated.

There is still a Lambda that will run at 9 pm to terminate any instances that are running that will catch anything that is missed by this process but it may save the odd cent here and there!

The components added so far look like this:


Of the three Lambda functions I just added two of them only do any updates if the match is complete (has a result). This made me think that it might be worth adding a new SNS topic that publishes only matches that are complete which the update-processors and update-sanity Lambdas subscribe to instead.

So I created a new Lambda game-over that subscribes to the scorecard-updated SNS and checks whether the match has a result or not and if there is a result publishes to the game-over topic. This check can then be removed from update-processors and update-sanity which now subscribe to the game-over topic.

The architecture now looks like this:

The code for this article can be found on this branch and the full service is here.

Coming next

Part 3 will show how the updates are pushed to WebSockets.

If you would like to test out the service and get updates through the summer go here and hit Subscribe. Note that on IOS Web Push is only available on v16.4 and above.