Scoring taekwondo competitions without stopping them

TKD Poomsae Scorer lets three to seven judges score from their phones while a local server applies the official consolidation rules, and puts the result on the venue's TV within seconds. It runs without internet.

Role
Sole engineer, from requirements to a working system
Built for
A regional taekwondo organization
Stack
FastAPI, SQLite, React Native (Expo), WebSockets
Source
github.com/darwinhc/tkd-poomsae-scorer
Live TV scoreboard of a five-judge match: each judge's technique and presentation per round, the consolidated values, each athlete's total and the winner
The scoreboard on the venue's TV: every judge's technique (TEC) and presentation (PRES), the consolidated value after dropping the highest and lowest, each poomsae score and the total.

The problem

In poomsae, each judge scores accuracy live, deducting 0.1 for a minor mistake and 0.3 for a serious one, and presentation after the form on three criteria. With five or seven judges the highest and lowest scores are dropped, separately for each part, and athletes perform two forms.

One head-to-head match produces up to 56 numbers to collect, trim and average. On paper, the competition stopped after every athlete, a miscopied number could change a winner, and the audience waited without seeing anything.

What it had to survive

No internet at the venue
Phones, admin panel and TV all run on a closed local network; every asset ships with the server.
Unreliable Wi-Fi
A phone that drops off must never lose a score or sign the other judges out.
Exact rules
Three, five or seven judges, trimming per part, two decimals and the official tie-break.
Operators who are not engineers
One executable to start, a QR code and a PIN per area, large buttons on low-end phones.

How it works

The phone never waits for the server

Every tap is written to SQLite on the phone before any network call. A sync engine sends events in the background, retries with exponential backoff and jitter, and tells final errors apart from temporary ones.

Every score arrives exactly once

Score events are idempotent, sequenced per judge and reconciled at the end with a SHA-256 checksum that the app and the server build from the same canonical payload, pinned by a shared test vector.

One scorecard per judge

The server keeps a scorecard for each judge and consolidates accuracy and presentation separately, as the rulebook says. A judge who is removed stops counting, and one who changes phones keeps a single scorecard.

Rules in a pure domain layer

The backend follows a hexagonal architecture: scoring rules have no framework code, so every rule, including the worked examples from the rulebook, is covered by fast unit tests.

Architecture

Ports and adapters on the server. The scoring rules sit in a domain with no framework code, use cases orchestrate them, and the database, the live channel and the security services plug in as adapters.

Clients

  • Judge app ×3–7Android · offline-first
  • Admin panelBrowser on the LAN
  • TV scoreboardBrowser on the venue TV

Entry point

FastAPIREST · WebSocket · admin and TV pages

Application core

Use cases

  • Connect station
  • Ingest score event
  • Finalize per judge
  • Start and close match

Domain

Judge scorecards · Consolidation for 3, 5 or 7 judges · Tie-break · Checksum

No framework code

Adapters

  • SQLiteThrough SQLAlchemy repositories
  • WebSocket hubPushes each change to the TV and panel
  • JWT tokensOne session per judge station
  • Encrypted credentialsStation PINs stored with Fernet

The judge's phone

Accuracy is tapped during the form with large −0.1 and −0.3 buttons; presentation is set afterwards with the three criteria. The interface follows the phone's language, English or Spanish.

Technical scoring screen with deduction buttons for the red and blue athletePresentation screen with speed and power, rhythm and tempo, and expression of energy for each athlete

Result

Judges submit from their phones, so nothing has to be collected between athletes, and the TV shows each judge's scores and the consolidated result as soon as they finish.

The complete flow was verified end to end against a running server with three- and five-judge matches, including a judge removed in the middle of a match. 160 backend tests and 83 app tests run in CI with linting and type checks.

Python · FastAPI · SQLAlchemy · SQLite · React Native · Expo · TypeScript · Zustand · WebSockets · PyInstaller

Need a system that keeps working when the network does not?

Tell me on LinkedIn what you are building. I answer every message.