Język / Language
PLPolskiENEnglish

Case study · Sport & Technology

Polish Basketball League.

A new PLK.PL - three applications, the complete competition archive, live scores and sporting data engineered for match-day traffic peaks.

3applications in one repository
71public views
146API endpoints
60 slive score refresh
84production releases
~68klines of application code
01

The starting point

The Polish Basketball League ran a site that had been live for well over a decade: news, fixtures, standings, statistics, team and player profiles, galleries and video. It was the central source of competition information, but technologically belonged to another era.

A database built for a legacy CMS

Polish table and column names, undocumented numeric statuses, tag relations that had drifted and raw FCKeditor HTML - with no schema documentation or types.

No friendly URLs or SEO foundation

News, videos and galleries had no slugs and existed only under numeric identifiers.

Sports data exclusively from ESOR

Fixtures, rosters, statistics, box scores and play-by-play came from an unusual external API with no intermediate layer.

Traffic spikes

Match-day traffic grows sharply while scores must stay fresh. Querying the source on every visit could never scale.

An outdated editorial panel

News, galleries, video, banners, partners and static pages were managed in ageing tooling.

02

Project scope

We rebuilt the frontend, API and editorial panel on the same production database. New entities map onto historical tables, avoiding a one-off, high-risk migration of the entire archive.

Three applications deployed through one process:

ApplicationScope
Frontend (Next.js)The public PLK.PL site - 71 views and 159 components.
Backend (NestJS)146 REST endpoints, 68 modules, 187 DTOs and OpenAPI documentation.
Admin panel (Angular)59 components for editorial work, media, banners, partners and configuration.

Site modules:

ModuleScope
Live scoresA global bar for games in progress with quarter, clock and score.
CompetitionFixtures, league and round filters, standings, U23 and .ics export.
GameBox score, statistics, play-by-play, shot chart, records, media and PDF.
Teams and playersProfiles, rosters, fixtures, careers, statistics, records and news.
Statistics and recordsIndividual and team rankings, multi-level sorting and CSV export.
Season archiveThe complete view tree in the context of every historical season.
News and mediaCategories, tags, galleries, lightbox and YouTube video imports.
SearchFull-text editorial search plus teams and players from the league system.
Play-off bracketFan voting, prediction rankings and server-generated share images.
CommercialRotating banners, click tracking, promo boxes, partners and newsletter.
Static contentEditorial pages protected against collisions with application routes.
03

The ESOR league-system integration

Every result, table and statistic comes from ESOR, the Polish Basketball Federation's system. The source could not be changed, while the site could not be slower or less available than it.

What we found

One API entry point, form-encoded POST requests, operations passed in the body, and no versioning, types or contract. Responses reflected an internal record-keeping model rather than a public site's needs.

What we did

A domain layer of our own

13 modules for leagues, seasons, rounds, games, teams, players, coaches, standings, statistics, venues and search. The frontend never sees raw ESOR responses.

Caching as architecture

Every request has a deterministic memcached key and lifetime, with controlled refresh and bypass mechanisms.

Pre-emptive refresh

Heavy queries rebuild every four minutes and live games every minute - before a visitor asks for them.

Cluster awareness

Only instance zero refreshes data, so scaling the process count never multiplies traffic to ESOR.

A second site-side buffer

Incremental regeneration every 60 or 300 seconds adds an independent layer between the visitor and backend.

The site remains fast when its source is under load, and new statistics views use ready, typed modules instead of direct calls to a foreign API.

04

Live scores

A league site is judged on match day. A live score must be visible immediately on every page, regardless of how many fans are watching in the same minute.

Fetched server-side

The API asks ESOR for games in progress every 60 seconds while browsers perform no polling.

One instance polls

Only cluster instance zero fetches fresh state, while every process can serve it.

One game model

Live state merges into fixtures: status, quarter, clock, score and broadcast URL share one model.

A global score bar

Live games and standings follow fans through articles, player profiles, fixtures and archives.

An interface leading to the game

The carousel finds the live game, marks it with a pulse and expands into the complete round and standings.

The cost of live scores does not grow with traffic. Tens of thousands of sessions still produce exactly one request per minute at the league system.

05

Design system

PLK.PL uses a dedicated design system developed and versioned with the application. The design was a system from which views are assembled, not a collection of isolated mockups.

Colour - named roles instead of values

The accent scale covers the full interaction cycle, surfaces define depth levels, and signal colours communicate status without reading.

accent red 1#FDE1CBicons on dark backgrounds
accent red 2#F88D65focus ring
accent red 3#E81E04primary accent, live
accent red 4#A70209hover
accent red 5#6F0017pressed
drk 0#090718deepest background
drk 1#151324dark sections
drk 2#1E1C2Bdefault button
drk 3#292733hover state
lg 0#FFFFFFcards, form fields
lg 1#FDFDFDpressed state
lg 2#F8F8F8section background, card shadow
text primary#0E0C1Dheadings
text tertiary#292733labels, hints
support#4A4661inactive text
border darker#F0F0F0border on light surfaces
border darkest#ABA6C1switches
disabled#DDDCE1disabled element
stats#3A3843statistics table grid
accent yellow 1#E8B604game clock, countdown
accent green 1#4ECF00positive status

Typography - separate faces for reading and data

Three type families: one for content and two monospaced faces for scores, the game clock and statistics tables.

Content - Inter

Another set of points at the Kolejowa arena

Headings, game previews, articles and navigation. The face remains readable in small labels and dense table layouts.

400 · 500 · 700 · 800

Data - Roboto Mono / Geist Mono

89:76
Quarter 402:14
PlayerPKTZBASEVAL
A. Kowalski247528
M. Nowak1811225
P. Zieliński93817
monospaced figures - numbers align in columns

Components - one implementation across the site

Variants, sizes, focus and disabled states come from the system rather than local overrides.

Average per game
LiveHome - Away89 : 76Quarter 4 · 02:14

System details

Radii, shadows, focus rings and motion are tokens too.

Corner radius0.125rem
Card shadow0 2px 0 #F8F8F8
Focus ringring 2px #F88D65
Media tile gradient180° → #0E0C1D
Loading indicatorloaderLine 1s linear

159 components behave like one product across 71 views - including post-launch additions such as the play-off bracket and U23 table.

06

Working on a live production database

The new site was never handed a clean schema. We mapped 30 entities onto legacy CMS tables and translated Polish columns into a readable domain model. Versioned migrations introduced every change at deployment.

  • Slugs for news, video, galleries and categories - the foundation of friendly URLs and SEO.
  • Repair of historical tag relations that had drifted over years of legacy operation.
  • Cleanup of editorial content left behind by the old editor.
  • Full-text indexes for news, video and galleries.
  • New areas without disturbing the archive: play-offs, voting, promo boxes and partner sections.

Media is converted to WebP and resized on upload while preserving the old CDN structure, so archival photos and logos remain at unchanged addresses.

07

Technologies used

Frontend
Next.js 14, React 18, TypeScript 5, Tailwind CSS, Radix UI, Swiper, PhotoSwipe and React Hook Form
Backend
NestJS 10, TypeORM, MySQL 8, Swagger/OpenAPI and JWT access/refresh sessions
Admin panel
Angular 17, NGXS, Angular Material, Bootstrap and TinyMCE
Performance
memcached, scheduled refresh, Next.js ISR, sharp and WebP conversion
Integrations
ESOR, YouTube Data API and GetResponse
Server graphics
node-canvas for shareable play-off bracket prediction images
Infrastructure
GitLab CI, clustered pm2, Docker Compose and Adminer
Quality
139 API test suites, ESLint, Prettier and frontend types generated from OpenAPI

OpenAPI is the contract between applications. A backend contract change becomes a frontend compilation error instead of a production defect.

08

Deployment

A merge into the development branch publishes staging; a merge into main publishes production. The pipeline compares the commit with the last build and rebuilds only the applications that actually changed.

Database migrations run before the new API starts. Every frontend build lands in a separate release directory with a shared image cache, and clustered processes reload without downtime.

84 production releases

A steady rhythm of small changes across 1,230 commits instead of rare, high-risk deployments.

Incremental build

One process deploys frontend, backend and admin panel while rebuilding only changed applications.

09

Results achieved

The site shipped

71 public views, including the complete season archive tree, run in production.

The archive preserved

The new site uses the same database with no risky, one-off content migration.

Friendly URLs and SEO

Slugs, metadata and Open Graph also cover years of content that previously existed only under IDs.

Scalable live scores

Quarter, clock and score refresh every 60 seconds with one source request per minute.

A coherent design system

Tokens and shared components cover all 71 views and evolve with the product.

Resilience to ESOR

Two cache layers and pre-emptive refresh isolate fans from external API load.

Complete statistics

Rankings, records, shot charts, play-by-play, CSV and PDF work across current and historical seasons.

A new editorial panel

59 components manage content, media, advertising, partners, play-offs and configuration with permissions.

Fan engagement

The bracket, voting, rankings and individual share images turn predictions into participation.

Post-launch growth

New seasons, Cup, Super Cup, U23 and calendar export arrive without downtime.