work/Sports · Platform

UKCA

The platform that runs an entire national sport - clubs, coaches, cheerleaders, competitions, judging, qualifications, certifications and payments - on one data model. From club registration to competition-day paperwork, with no spreadsheets and no paper forms.

Role

Product design · domain architecture · full-stack build · platform migration

Stack

NestJS · Prisma · PostgreSQL · BullMQ · Stripe

Year

2022 · in active development

Status

In production · second generation in build

0+Domain entities in one data model
0Ways to pay, one payment ledger
0Manual steps: entry → payment → competition-day paperwork
The challenge

What it needed to solve.

The UK Cheerleading Association runs an entire sport: community clubs and school programmes, coaches and cheerleaders, competitions and judging, coaching qualifications, exams and certifications. For years all of it lived in spreadsheets, email threads and paper forms that didn't talk to each other.

Clubs registered by hand. Competition entries were reconciled manually, and every roster change after the deadline meant another round of emails and another version of the spreadsheet. The running order was assembled in Excel, score sheets were printed from that same Excel, and judges were handed their paperwork in folders. Coaching certificates were issued and verified on trust. Payments arrived through a patchwork of methods and rarely matched who owed what.

And sensitive records for school cheerleaders - children under 18 - needed real protection, not a shared folder.

What was needed was a platform that knows the rules of the sport itself, that carries a club from entry to a clean invoice and from the running order to a signed score sheet - reliably, repeatably, and without anyone re-keying a thing.

Our approach

How we built it.

We built a domain-driven platform where the rules of the sport live in the code, not in the heads of a handful of people. One data model, one place of truth, and a flow that moves forward - from club, through competition, to certificate.

  • The rules of the sport in code, not in a footnote on a form. Age groups, divisions, masters and crossover rules are checked on the backend before an entry ever becomes an entry. An impossible roster simply cannot be submitted - there is no reconciling it by email afterwards.
  • Two worlds of clubs, one system. Community clubs and school programmes have different roles, different rosters and different ways of paying, but they share the same data model. Schools get branded invoices with a purchase order and Sage reference, ready for accounting; community clubs pay by card.
  • A payment ledger, not a list of transactions. Every entry carries its own balance through partial payments, refunds and later roster changes. A payment is allocated to what it covers, and changes to an entry leave an audit trail - who changed what, and when. Stripe Checkout online, Stripe Terminal for card payments taken live at the competition, invoicing for schools. All three land in the same ledger.
  • Competition day from a single source. The running order is the source of truth. All the paperwork that used to be made by hand generates from it: score sheets allocated per judge, packing lists and envelopes - through ExcelJS and conversion to PDF, ready to print.
  • A full education flow. Coaching courses and applications, online quizzes and exams with per-section questions and attempt records, qualifications by discipline, level and rank, and refresher courses with a review and approval flow. At the end stands a certificate and a public register of certified coaches - a club or a parent can check at any moment whether a real qualification stands behind a coach.
  • Tickets that get scanned. Spectator ticket sales with per-session QR passes, checked at the door - instead of a list on paper.
  • Encryption at rest for minors. Sensitive fields for school cheerleaders are AES-256 encrypted before they reach the database, with a key that never leaves the backend. The frontend never sees that data in the raw, and whoever does get it, gets it through a role entitled to it.
  • Resilient to slow SMTP. Email sending goes through a durable job queue with retries - the user gets an answer immediately, the message leaves when it leaves. Templates render through React Email, so the look of a message changes without touching the logic that sends it.
Selected screens
Manage your club
Your club overview
Club cheerleaders table
Team management
Assign team member, with age eligibility rules
Enter competition
Architecture

One data model, clear boundaries.

The backend is a NestJS application split by domain - each domain has its own module, controller and service, and the business rules live in the services. Controllers stay thin: validate, call, shape the response.

01

Data

PostgreSQL through Prisma. The model covers 50+ entities and holds the whole sport in one schema - club, team, cheerleader, coach, competition, entry, payment, qualification, certificate, ticket. Changes go through versioned migrations.

02

Job queue

BullMQ on Redis separates slow work from the HTTP request. Sending an email is a job, not a block - with retries and a record of failed jobs.

03

Storage

Google Cloud Storage with signed URLs. Files upload straight into storage and are read through a time-limited signed link, so private documents never become public.

04

Access

JWT authentication (local and Google), roles and policies at route level, and record ownership as an extra layer - a club sees its own club, not somebody else's.

05

Frontend

React and Vite, MUI with data grids, Redux Toolkit for state, react-hook-form for forms, i18next for languages, dnd-kit for assembling the running order by dragging, the Stripe Terminal SDK for the card reader.

Migration

Changing the engine mid-flight.

The first generation of the platform ran on the Strapi headless CMS, and it got the sport moving. When the scope outgrew what a CMS can sustain, rather than rewriting from scratch we built a second generation on NestJS and Prisma - and we are migrating it domain by domain, with the application running the whole time.

The migration is not run “by feel”. Every domain has its own script with three modes: a dry run that changes nothing, --save which writes a report of what would happen, and --apply which performs the move. Alongside it comes an audit script that compares the old and new state, a behaviour-parity report, and a cutover runbook - step by step, with a way back if it is needed.

What moved includes what nobody sees: media files from Cloudinary to GCS, certificates, historical payments, competition entries, users and roles.

Security

Least privilege, nothing in the clear.

  • Encryption at rest (AES-256) for sensitive school-cheerleader fields; the key is a backend secret only and never reaches the browser.
  • Passwords are stored as bcrypt hashes; password-reset tokens are single-use and time-limited.
  • Roles and ownership are checked on every route, not in the interface - the interface hides, the backend forbids.
  • Private documents in storage are reachable only through signed URLs with an expiry.
  • Secrets and keys come from the environment and a secret manager, never from the repository.
  • Stripe is never taken at its word: a payment becomes a payment only when a signature-checked webhook confirms it.
The stack

Chosen to last.

Runtime / languageNode.js 20 · TypeScript 5
FrameworkNestJS 11
Database / ORMPostgreSQL 16 · Prisma 6 (43 migrations, 1500 lines of schema)
Job queueBullMQ + Redis 7
PaymentsStripe - Checkout, webhooks, Terminal (card_present)
Documentspdfkit · ExcelJS · XLSX → PDF conversion · qrcode
EmailReact Email templates · Nodemailer / SMTP
StorageGoogle Cloud Storage · signed URLs
SecurityPassport JWT · bcrypt · AES-256 field encryption
FrontendReact · Vite · MUI · Redux Toolkit · i18next
Local developmentDocker Compose (Postgres + Redis)
Engineering quality

Built to be handed over.

  • TypeScript in strict mode, with input validation through class-validator and Zod before data touches a service.
  • Unit tests where the rules actually bite: age-group calculation, judge allocation across score sheets, running-order mapping, invoice generation.
  • Versioned database migrations; no schema change goes in by hand.
  • Migration scripts with a mandatory dry run, a report and an audit before anything is written to real data.
  • A documented cutover runbook and a behaviour-parity report between platform generations.
  • Docker Compose for local development - Postgres and Redis come up with one command.
Status

Where it stands.

The platform is in production and runs the sport day to day. Clubs register themselves and enter competitions without back-and-forth. Competition day assembles from the running order, and judges get paperwork that generates itself. Coaches earn qualifications and walk away with a certificate anyone can check in the register. Schools are invoiced cleanly, community clubs pay by card, and whatever is left is settled with a card reader at the competition itself - all in the same payment ledger.

In parallel, the second-generation backend on NestJS and Prisma is taking over domain by domain, with audit scripts and a runbook for every step.

in production · national association
  • club registration and management
  • competition entries (community + school)
  • age, division and crossover validation
  • Stripe online · Terminal live · school invoicing
  • payment ledger with balance and audit trail
  • running order and competition-day paperwork
  • online exams, qualifications and certificates
  • ticketing with QR passes
  • encryption at rest for minors' data
  • migration to the second-generation platform
“TherraCode built the system our sport never had. Clubs, competitions and coaching qualifications finally live in one place - and we trust it.”
UK Cheerleading Association
Next project

The Playbook Studio

OPEN FOR NEW PROJECTS

Have a project like this one?

Tell us what you're building. We'll come back within one business day with a clear plan, a timeline, and a real human to talk to.