fullstack
Courtsee
Livestreaming and match-recording platform for padel and racket-sport clubs.
- Role
- Software Engineer
- Duration
- 2 months
- Date
- June 2026
Technologies
- go
- nuxt
- vue
- typescript
- postgresql
- hasura
- graphql
- ffmpeg
- sanity
- gcp
- terraform
- docker
Impact
Platform grew to 14,600 users across 3 countries, spanning 54 clubs and 215 courts.
Problem
Racket-sport clubs record matches on phones, if at all. Footage ends up scattered across devices, rarely shared, and tournaments get streamed only when someone happens to bring the right equipment. There is no single place a club can run a repeatable setup from.
Most of the work turned out to be on the other side of that: an admin portal where clubs, courts, devices, subscriptions and payments are managed, and where the platform’s rules are enforced. Its components had been built without anyone looking at them across resolutions, and the bill arrived as a long run of layout fixes, one breakpoint at a time; a viewport nobody had checked, a banner whose buttons stacked instead of sitting side by side, an organisation name too long for its column, a court card breaking on a tablet, a serial number cut off. Worse than the cosmetics was a class of bug that misleads people: analytics still counted clubs whose payment configuration had been switched off, device search showed a confident empty state when no organisation was even selected, selecting every sport hid the results instead of showing more, admin user search was case-sensitive, and the verification popup never said which address the email was going to.
Two more layers of the same problem. Validation was thin exactly where it matters: coupon codes accepted hyphens and spaces, so a duplicate could slip past the uniqueness rule; a club identifier was never checked as a UUID before it reached the Go handler; watermark images were uploaded with no size or format limit, and re-uploading one did not bust the CDN cache; event dates were not required. And the parts that cross domains were the parts nobody owned end to end; payment sessions that expired per gateway rather than by rule, a payment that opened in a new tab and left the user stranded there, no loading state on the way back from a successful payment, and a livestream session that could be created twice over. Meanwhile whole features were still missing: event management, the subscription page, the analytics portal, and the marketing site’s pricing, about and roadmap sections.
Solution
Courtsee turns that into one platform: matches are recorded and published from the same place they are streamed from, so a club can run a weekly event without a dedicated media team. The front end is Nuxt with TypeScript, the API is Go, and reads go through GraphQL so the client asks only for the fields a screen needs: the scoreboard and match lists stay responsive on the club’s own wifi.
The response moved from patching symptoms to removing the conditions that produced them. Breakpoints were agreed once in the styling config and theme instead of being decided per component, hardcoded rem values gave way to max-widths and colour tokens, and display logic moved out of components into composables (one of which was touched fifteen times as its feature grew), which is the point: it had a home to grow in. Validation moved to the boundary, where it can be trusted: an alphanumeric rule and whitespace sanitation for coupons in the Go handler, UUID checking before a club identifier is used, size and format checks in the uploader before anything is submitted, and date rules in the repository schema. Filters moved to the query layer rather than the view: unverified users out of the statistics, deleted courts out of the livestream list, the payment bypass restricted to system admins, and empty states were made informative, so “no data” and “no filter chosen” stopped looking identical.
The parts that span domains were dealt with by giving each one a single owner. Session expiry moved behind a payment provider abstraction, so gateway-specific rules stop leaking into the flow; payment now redirects inside the app instead of spawning a tab, and the return path got the loading state it was missing; a guard at the top of the livestream function refuses a second session. The features that did not exist were built as vertical slices rather than horizontal layers (schema, then query, then server, then composable, then page), which is how event management arrived complete with soft delete, image upload and date validation, and how the per-court watermark went from a database migration to an API endpoint to a component pulled out of a slot and made whole, cache busting included. Across seven months that came to 271 substantive commits and roughly 28,000 lines added against 5,000 removed, spread over the Nuxt app, the Go backend, the Nuxt server and the CMS schema. That ratio is the shape of building rather than patching, and the reason the later fixes were small adjustments instead of rewrites is that the shared components and the internationalisation groundwork were already in place.
Gallery
Screenshots

Homepage
Contact
Interested in working together?
Tell me what you are building and what you need. A short note is enough.