Built in public · Case study

Arsenal Hub — a serverless fan platform on AWS

Fans sign in with Google and submit photos, GIFs, or short videos. Each submission goes through a Slack-based moderation step before joining a Pinterest-style grid. The whole stack is serverless, runs for single-digit pounds a month, and ships with a reusable Terraform module the same pattern can drop into client projects.

What it is, and what it's for

Arsenal Hub is a fan-curated home for Arsenal moments at afc.dependabledigitalsolutions.com. Fans submit content. The owner approves or rejects each submission from a Slack message. Approved content lands on a public grid; rejected content is deleted.

It doubles as a portfolio piece. Building it gave DDS a real-world test of an architecture pattern small businesses keep needing — "users submit media, an operator approves, the public site updates" — and let us prove the same stack runs on a hobbyist budget while being production-grade.

The stack at a glance

Eight AWS services, no servers

CloudFront + S3

Astro-built static site served from S3 behind a single CloudFront distribution. /api/* behaviour forwards to API Gateway; everything else is cached static.

  • Static-first
  • OAC
  • CDN

API Gateway + Lambda

Six Lambdas behind an HTTP API: sign-upload, finalize-upload, list-content, slack-interaction, transcode-complete, trigger-rebuild. All TypeScript, all bursty, all pennies a month.

  • HTTP API
  • TypeScript
  • esbuild

DynamoDB single-table

One table holds content rows, ban list, and short-lived upload stash rows. Three GSIs cover the read patterns; conditional writes make Slack actions idempotent.

  • On-demand
  • Single-table
  • TTL

MediaConvert

Video submissions get transcoded to a 720p / ~3Mbps MP4 with a 1-second poster frame. EventBridge fires when the job completes; a Lambda picks it up. ~£0.015 per minute of video.

  • 720p cap
  • Poster frame
  • Pay-per-use

SQS + CodeBuild

Slack approvals enqueue a rebuild message with 30-second delivery delay (five approvals in ten seconds → one rebuild). A Lambda pulls and starts a CodeBuild that rebuilds the site and invalidates the CDN.

  • Debounce
  • CodeBuild
  • Cache invalidation

Secrets Manager + Route53

One Secrets Manager secret holds the Slack signing secret, bot token, and Google client ID. Route53 hosted zone for afc.dependabledigitalsolutions.com in the same account, ACM cert in us-east-1 for CloudFront.

  • JSON-secret
  • ACM
  • Route53 alias

Terraform — and a module that pays for itself

Infrastructure is split into three: a one-time bootstrap/ stack that creates the Terraform state bucket (with S3-native locking — no DynamoDB needed); a base/ module containing every actual resource organised by concern (s3.tf, cloudfront.tf, media-pipeline.tf, iam.tf); and a ten-line prod/ wrapper that consumes base. Adding a real dev/ environment later is a single file.

The interesting bit is the media-moderation-pipeline module. The "users submit media → owner approves in Slack → CDN-served output" pattern was extracted into its own Terraform module in dependabledigitalsolutions/terraform-aws-modules, tagged v0.2.0. Future DDS clients who need the same shape — community photo walls, branch-submitted compliance evidence, customer-submitted incident media — get the whole pipeline as a single module block. The work to design and harden it has already been paid for by Arsenal Hub.

Upstream Terraform modules come from Anton Babenko's terraform-aws-modules registry: s3-bucket, cloudfront, iam/modules/iam-github-oidc-role, lambda, apigateway-v2. DDS adds the moderation-pipeline module on top. No hand-rolled aws_lambda_function resources — production-grade modules from people who have already solved the hard parts.

CI/CD — three workflows, zero static AWS credentials

Three GitHub Actions workflows handle every change to the codebase:

Authentication to AWS is via GitHub OIDC — the workflows assume the arsenal-hub-prod-github-deployer IAM role, with trust restricted to repo:dependabledigitalsolutions/arsenal-hub:*. No long-lived access keys live anywhere — not in the repo, not in GitHub secrets, not in a developer laptop. The trust boundary is the GitHub repository itself.

Approval-triggered rebuilds use CodeBuild rather than GitHub Actions. The rebuild Lambda already has AWS credentials and CodeBuild's IAM trust is natural; reaching out to GitHub Actions for a rebuild would require shipping a personal-access token. Same buildspec semantics on both paths — only the trigger differs.

Slack moderation — no admin app to build or maintain

The owner moderates submissions from a Slack channel (#afc-moderation in the DDS workspace). Each new submission posts a Block Kit card with the media, the uploader's caption, an Approve button, and a Reject button. The caption can be edited by replying in the thread before approving.

When a button is clicked, Slack POSTs to /api/slack-interaction. The Lambda verifies the request's HMAC against the signing secret (with a 5-minute timestamp window to defeat replays), then either copies the file from the pending S3 bucket to the public one and marks the content row approved, or deletes the file and marks it rejected. Either way, the original Slack message updates in place so the channel stays a clean audit trail.

On approval, a message goes onto an SQS queue with a 30-second delivery delay — enough that batched approvals trigger a single CodeBuild rebuild, not one per click. About 60-90 seconds after a tick, the new content is live on the public grid.

The point: no admin web app to build, no admin auth to manage, no operator UI to keep in step with the rest of the product. Slack is the UI. That's worth a couple of thousand pounds of avoided work for the owner.

Google auth — stateless, no Cognito

Visitors sign in with Google's Identity Services on-page button. The button returns a Google ID token (a signed JWT). The frontend includes it as Authorization: Bearer … on the one endpoint that requires auth — POST /api/sign-upload. That Lambda verifies the JWT signature against Google's JWKS (cached per warm Lambda container for cost), checks the aud claim matches the Arsenal Hub client ID, and requires email_verified.

That's it. No Cognito user pool, no session cookie, no refresh token rotation, no password reset flow. The token Google already issued is the credential, verified statelessly on every call. Visitors get one less account to remember; the platform gets one less piece of infrastructure to operate.

For a B2C consumer product like this, stateless social sign-in is the right shape. For B2B products with role-based access control across enterprises and branches, Cognito starts paying for itself — that's the shape DDS reaches for on multi-tenant ERP work. Picking the lighter mechanism here, not by default, is the point.

Cost envelope — and the cuts that hold it there

Target: under £5/month at v1 traffic. CloudWatch alarm fires at £15/month forecast. Top contributors: Secrets Manager (£0.32 fixed), Route53 zone (£0.40 fixed), MediaConvert (£0.015 per minute of video). Everything else — CloudFront, Lambda, DynamoDB, S3, CodeBuild — runs in the pennies-a-month range while traffic is small.

Hitting that number depends as much on what's not there as on what is:

Each cut is documented in the design spec with a "what would have to be true to bring this back in scope" note. That's the discipline that keeps a hobbyist budget honest — and the same discipline that keeps small-business platforms from bloating into enterprise budgets the businesses can't afford.

Want this for your business?

The same architecture is for hire

If "users submit content, an operator reviews, the public site updates" looks like something your business needs — community walls, customer-submitted evidence, branch-uploaded photos for compliance — the moderation pipeline is already a Terraform module on the shelf, ready to drop into a project. Talk to us about scoping.