Skip to content

Ostrobothnia Nepalese Super League

Status
live
Stack
Next.js 16 · TypeScript · PostgreSQL · Drizzle ORM · Auth.js v5 · Tailwind CSS 4 · React Three Fiber · Cloudflare R2
Host cities
5
Auth
Role-based, credentials + JWT
Telemetry
Self-hosted Umami
Destination
Homelab via Docker + Cloudflare Tunnel

A full-stack league platform built for Oulu Nepalese Sports RY — live scores, standings, team registration and media for the ONSL across Vaasa, Seinäjoki, Pietarsaari, Kokkola and Oulu. Mobile-first, and built to own its own data so the association is never locked into a vendor it cannot afford.

The problem

A volunteer sports association had a league to run and no software to run it with. Fixtures lived in group chats, standings were recalculated by hand, and registration was a form someone had to read and retype. The association has no budget, so whatever replaced that had to keep working after the free tiers stopped being generous.

Constraints

  • No budget, ever. Launch had to fit inside free tiers, and the migration path off them had to exist on day one rather than be discovered later.
  • Phones only. Players and organisers check standings on a phone at a pitch-side, on Finnish mobile data. Desktop is the rare case.
  • Non-technical operators. Everything an organiser touches has to survive being used once a week by someone who did not read a manual.
  • Five cities. Tournaments, teams and fixtures are scoped per city, so the data model had to be multi-tenant from the first migration.

Decisions

Postgres with Drizzle, not a hosted app platform. The whole point is that the association owns its data. Drizzle keeps the schema in the repository as plain TypeScript with real migration files, so moving from Neon to a container in a homelab is a connection-string change and not a rewrite.

Auth.js v5 with credentials and role-based access. Organisers, team captains and the public see different things. Rolling this myself would have been the most likely place to ship a real security bug, so it went to a library with a well-understood threat model.

Cloudflare R2 for media. S3-compatible, so the storage layer is portable. Match photos are the fastest-growing thing on the platform and the most expensive to be locked into.

Self-hosted Umami over a hosted analytics product. The site is for a community organisation; shipping third-party tracking to their members to answer questions I could answer myself was not a trade worth making.

Tradeoffs

Launching on Vercel and Neon contradicts the self-hosting instinct, and it was still correct: the league needed to exist before the infrastructure was interesting. Every choice underneath — Postgres, Drizzle, S3-compatible storage, Docker in the repo — was made so that the eventual move to a homelab is dull. Being deliberately temporary is different from being accidentally stuck.

React Three Fiber is in the stack for phase three. It is the one piece that exists because it would be good, rather than because it was needed, and it is gated behind everything that was.