Products & Platforms · Padel club chain

Video infrastructure for a sports chain: from court booking to player highlight reel

Court cameras turned into a product channel: the match is captured, processed, and delivered to the player's Telegram without a single staff action.

Client
Padel club chain
Timeline
2026
Role
Architecture, product engineering of the…
Status
In development
5
Applications in the monorepo
web Mini App / api / Telegram bot / video-agent / desktop check-in — all against one shared API contract
2
Clubs in the multi-club model
clubs are text slugs in the data model and are fetched dynamically from GET /api/clubs — a new club needs no client release
10
REST endpoints in the API contract
phone auth, clubs, courts, cameras, video jobs and Telegram delivery — fixed in a contract document inside the repository
7
Additive SQL migrations
numbers 002–008 extend the live wristband schema without breaking it; the owner applies them via SQL editor after a backup

Context

In padel, match video has become part of the expected customer experience: players want to rewatch rallies, analyze technique, and share highlights. Clubs answer by installing cameras — but in a typical club there is no software at all between the camera and the player. Recordings pile up on a DVR where nobody ever finds them, and no front-desk manager can realistically cut and distribute reels after every slot by hand.

The chain's IT landscape was fragmented by definition: booking in an external industry CRM, access control in a desktop wristband application, cameras in the vendor's own video loop, the website on a closed site builder. None of it was designed to integrate, none of it could be stopped or rewritten, and the production database sits outside the contractor's administrative access. The only reliable fact to build automation around is a player physically standing on a court.

The task

The client stated the target scenario precisely: a player finishes a match and receives the reel in Telegram — no manual cutting by an administrator, no working out who was on which court, no pulling files off the DVR. The hard constraint: the service must run on top of the existing wristband check-in application without breaking it, because it is already embedded in the clubs' daily operations. In parallel, booking had to be connected to the video loop, and the builder-hosted website brought back under code-level control.

Approach

The work was split into three independently moving tracks — the video platform with a Telegram Mini App, the integration layer with the external booking CRM, and the website export — so a schedule slip in one never blocks the others.

The key architectural decision was not to replace the wristband application but to use it as the exchange buffer: the single source of truth about who is physically on the court. A wristband worn on a court is a live presence signal that survives any outage of webhooks, schedules, or external systems. Development discipline came from a contract: the API source of truth lives as a document in the repository, which let five applications be developed in parallel against one fixed interface.

Video platform architecture

The monorepo holds five applications: a Telegram Mini App on React / Vite / TypeScript / Tailwind; an API on Fastify / TypeScript exposing 10 REST endpoints; a Telegram bot handling onboarding and contact sharing; a Python video agent that captures RTSP streams from Dahua cameras, processes them with FFmpeg, and uploads finished reels to object storage; and the desktop wristband application refactored to .env configuration so it deploys at any club without code changes.

Multi-club support sits in the data model, not bolted on: clubs are text slugs — two real clubs run in the system — entities carry UUIDs, and the Mini App fetches the club list dynamically from GET /api/clubs. Recording is triggered by the wristband visit itself — the fact of physical presence — so the pipeline keeps working even if the integration layer is down. Because the production database is outside our administrative reach, reads go through PostgREST with the anon key, and all seven migrations (002–008) are additive: the client applies them via the SQL editor after a backup. Temporary TEMP_ customers can never authorize, and phone numbers are stored normalized.

Integration layer: two-way booking

Two-way booking — a slot becomes occupied in both systems, whichever one it was booked in — was designed as a bridge with an Anti-Corruption Layer and an idempotent sync engine: key-based deduplication, upsert on the (source, external_id) pair, and a transactional outbox for fault tolerance.

An adversarial design review produced a sober conclusion: strong slot exclusivity requires either write-through — confirming a booking only after a successful response from the external system — or split slot ownership. Write-behind was rejected outright: in the desync window the external CRM can sell the same court at the front desk, a double booking with real people on the court. The conflict resolver therefore ranks physical presence and status above record timestamps. The design, its guarantees, and the phase-0 blocker — whether the external CRM supports an atomic conflict-rejecting create — were all fixed before the first line of bridge code, so the client decides on the integration investment with open eyes.

Outcome

The video platform MVP is built and verified: API, web app, bot, video agent, and desktop application all compile and run against the fixed contract and data model, with the multi-club model live on two real clubs. The website left the closed builder — 22 pages and 227 assets exported into owned code, changed non-invasively via pluggable custom.css and custom.js, with booking buttons wired to the CRM's booking widget.

The responsibility boundary at go-live is explicit: the owner applies the migrations, creates the storage bucket and service role, and connects the real camera channels and bot after a backup. The external CRM integration stands as a fully specified concept with fixed risks, ready to implement once API access is granted. From booking to delivered reel, the path requires zero manual staff actions.

What we built

  • Monorepo of 5 applications

    web (React/Vite/TS/Tailwind), api (Fastify/TS, 10 REST endpoints), Telegram bot, video-agent (Python + FFmpeg), and a refactor of the desktop wristband application to .env — all through a single API contract.

  • Dahua video agent → storage

    RTSP capture from the cameras, FFmpeg processing, and upload of finished reels to object storage; recording starts on the real-time wristband trigger.

  • Multi-club data model

    Text slugs for clubs, UUIDs for customers, visits, wristbands, courts, and video jobs, bigint for the Telegram user; clubs are served dynamically from GET /api/clubs.

  • Additive migrations 002–008

    Seven migrations extend the existing wristband schema without breaking it; the client applies them via the SQL editor after a backup.

  • API contract as the source of truth

    A dedicated contract document in the repository fixes the interface between the five applications and enables parallel development.

  • Bridge concept for the external CRM

    A chain with an Anti-Corruption Layer, an idempotent sync engine, a transactional outbox, and a conflict resolver ranked by on-court presence.

  • Landing page reworked in code

    22 pages and 227 assets exported locally, changes via custom.css/custom.js, booking buttons on the CRM widget, court photos enhanced with originals preserved.

Engineering challenges

Production database outside our control

There is no direct access to the production database: reads go through PostgREST with the anon key, and the client applies migrations themselves. We designed the changes additively (002–008) to extend the wristband schema rather than rewrite it, and moved the final rollout to the owner's side, after a backup.

Two-way booking without races

A slot must become occupied in both systems at once. An adversarial design review showed: a local lock is not a cross-system lock, and write-behind leads to a double booking with a customer at the front desk. The solution is a mode switched on the external CRM's capability flag (write-through or split slot ownership) and a conflict resolver ranked by presence, not timestamps.

Starting recording without external events

Relying on webhooks from the external CRM to trigger recording is unreliable. The real-time trigger is the wristband on the court itself — a live presence signal that does not depend on booking-start events.

Editing a site on a closed builder

The builder's API is read-only. We exported the site locally with clean routing and made changes non-invasively via pluggable CSS/JS, preserving the option of a future move to self-hosted infrastructure.