Case Study

SCBorbense Website

2026 · Club Website & Admin PanelVisit live site ↗

The official website of Sport Clube Borbense — the football club of Borba, est. 1945 — and the admin panel the club runs it from: fixtures and standings across nine age groups, match reports, squad profiles, news, a native app and push notifications, on a stack that costs the club nothing.

SCBorbense Website

The problem

Sport Clube Borbense had no real home on the web. Fixtures, results and club news lived scattered across social media posts — fine for reaching followers, but invisible to anyone searching for the club, and impossible to browse as a record of the season.

The harder constraint was maintenance. A local club has no developer on staff and no budget, so whatever I built had to be editable by volunteers, from a phone, minutes after the final whistle — and it had to still look right when nobody got around to filling something in.

Key decisions

01

From a hosted CMS to a panel of the club's own

The first version used Notion as the content backend. It got the club online fast, but the ceiling arrived quickly: no roles, no audit trail, no way to validate what someone typed into a table. The site now runs on Supabase Postgres with an admin panel I built at admin.scborbense.com — accounts with roles, an activity log on every write, and forms that actually understand what a fixture is.

02

The database trusts nobody

The app is the only client. Every table has row-level security enabled and forced with zero policies, and the publishable key grants access to nothing at all; the service key is server-only, in a module that fails the build if a client component ever imports it. Authorization is re-checked on every request, which is what makes deactivating an account take effect on the next click rather than the next login.

03

Everything optional, and nothing prints a dash

Youth players get registered before anyone decides where they play, and a match report gets filled in on a phone with half the minutes missing. So every field is optional and empty ones are omitted rather than rendered as an em dash — a half-filled match report is the expected state, not a bug. A player's age is derived from the birth date and never stored, and the birth date itself stays admin-only, because the squad includes minors.

04

Standings imported weekly, never typed by hand

The official federation source bans automated access on the first request, so the league table is imported from zerozero.pt by a Monday cron and validated before it is saved: team count in range, Borbense present in the table, and matches played equal to wins plus draws plus losses. A failed import keeps last week's table on the site with its date visible instead of publishing a wrong one — a table that contradicts the club's own results is worse than no table at all.

05

One club, several front doors

Web Push for the site, an installable PWA, and a native Expo app with per-type notification preferences — news, matches, goals. Club posts are imported from Facebook by cron and deduplicated on the post id, so the news page keeps itself current. A Shopify storefront themed to match the site — same typefaces, same blue and gold — is built and waiting for the club's kit and merchandise.

06

The site can be closed without going dark

A site-status switch (Online / Maintenance / Countdown) is read from Vercel Edge Config on every request rather than the database, so the toggle costs no query on a normal visit. While the site is closed the homepage still answers 200 with real, indexable content so the domain can enter Google's index; every other path is 503 and noindex. The HTML is identical for people and for crawlers.

Stack

Next.jsTypeScriptSupabaseVercel

Next.js 16 App Router and TypeScript on Vercel, Supabase Postgres and Auth, Upstash Redis for push subscriptions and rate limiting, Vercel Edge Config and Blob, Cloudflare Turnstile, Web Push over VAPID, Sentry for error tracking, and a Shopify (Dawn) storefront for the club store.

The result

The club runs its own site. Volunteers publish news, results, match reports and squad changes from their phones; the league table updates itself every Monday morning; supporters get a push notification when the score changes. Nine age groups with fixtures, squads and standings, full player profiles, the club's history since 1945 — and a monthly bill of zero.

09
Age groups with fixtures, squads & standings
0€
Monthly running cost for the club
1945
Club history now online

In the wild

SCBorbense fixtures page
Fixtures and results, organized per age group.
SCBorbense player profile
A player profile prints what the club filled in, and nothing else.
SCBorbense squad page
The senior squad, with position filters.
Next projectScanBite →

Contact

Let's build something together.

Have a project in mind, or just want to say hi? My inbox is always open — I usually reply within a day.

Open to internships & freelance