Python React Native FastAPI Expo Supabase Complete

Goalboard

A full-stack soccer analytics mobile app: live standings, match detail with lineups and head-to-head history, team and player profiles, push notifications for favorited teams, and full user authentication. Built with React Native (Expo), a Python FastAPI backend deployed on Railway, and Supabase for auth and data storage.

Goalboard app screenshot

Overview

Goalboard is a cross-platform mobile app for soccer fans who want clean, real data without the noise of mainstream sports apps. It pulls live and historical soccer data from the API-Football v3 REST API through a Python FastAPI backend, presenting standings, fixtures, team analytics, match detail, and player statistics in a focused interface. The project was built as my Individual Software Engineering project at Trevecca Nazarene University (Spring 2026).

The app went through six sprints, starting from a basic standings screen and growing into a feature-complete product with user accounts (email/password, Google OAuth, Apple Sign-In), a Favorites system backed by Supabase Row Level Security, and push notifications for 11 live match event types. The backend is always-on, deployed to Railway with auto-deploy on every push to main.

Problem

Existing soccer apps are cluttered with ads, betting integrations, and social noise. There's no focused option for just standings, match data, and team trends.

Solution

A FastAPI backend that proxies API-Football v3, applies TTL caching, and exposes clean endpoints, consumed by a React Native app with auth, favorites, and live push notifications.

My Role

Sole developer. Responsible for the full stack: backend API design, database schema, mobile UI, auth integration, push notification scheduler, and cloud deployment.

Tech Stack

Python FastAPI API-Football v3 React Native Expo SDK 55 Supabase PostgreSQL APScheduler Expo Push Notifications EAS Railway pytest

What I Built

  • FastAPI REST backend (18 endpoints): proxies API-Football v3, applies per-endpoint TTL caching (30s for live matches, up to 1 hour for reference data), transforms response shapes for the mobile client, and verifies Supabase JWTs on protected routes via JWKS public key lookup.
  • League and match browsing: featured competitions grid with full-text search by name or country; standings with color-coded position zones; fixture list grouped by date with status filters and auto-scroll to the next upcoming match; live matches auto-poll every 60 seconds.
  • Match detail screen: score header, match statistics (possession, shots, corners, passes, fouls), mini league table for both teams, head-to-head history, recent form badges, and starting XI on a visual pitch diagram with player ratings.
  • Team and player profiles: season stats chips, next fixture card, recent results, full squad by position, transfers, and a season fixtures browser (all past results + all upcoming matches). Player profile shows bio, physical attributes, and per-competition season stats.
  • Team Analytics tab: goals scored/conceded sparkline charts, actual last-5-match W/D/L form badges, win-rate stats, and a global team search by name.
  • User authentication: email/password (8+ chars, letter, number, symbol required), Google OAuth, and Apple Sign-In via Supabase Auth. Sessions persist indefinitely until the user logs out.
  • Favorites system: authenticated users save leagues, teams, and players; stored in Supabase with Row Level Security. Tapping a favorite navigates directly to the full detail screen. Logged-out users see an AuthGate bottom sheet.
  • Push notifications: two APScheduler background jobs run every 60 seconds on the backend: one polls live match events (kick-off, goals, halftime, full time, red cards, VAR penalties, extra time, penalty shootout) and one polls upcoming fixtures (lineups confirmed ~1 hour before kickoff, 30-min pre-match reminder, postponed/cancelled alerts).
  • Profile screen: editable display name, email view, three-step OTP password change (send code → verify → set new password), and logout. Accessible via header icon on all main tabs.
  • Cloud deployment: backend deployed on Railway with auto-deploy on push to main; frontend distributed via EAS. Push token refreshed on every app open so delivery is always current.
  • 28 backend unit tests: pytest suite covering fixture parsing, standings transformation, league search deduplication, H2H parsing, lineup parsing, cache TTL behavior, and error handling for invalid inputs. All mocked with unittest.mock.patch.

Architecture

Four-tier architecture with clear separation of concerns:

API-Football v3
FastAPI on Railway
Supabase (Auth + DB)
React Native (Expo)
Backend proxy: All API-Football calls are routed through the FastAPI backend, protecting the API key, enabling server-side TTL caching, and providing a stable contract independent of upstream schema changes. Protected endpoints verify Supabase JWTs via JWKS (PyJWKClient), making verification algorithm-agnostic as Supabase migrated to ECC P-256 signing keys.
Navigation: A root NativeStackNavigator wraps the bottom tab navigator so any tab can push shared screens (MatchDetail, TeamDetail, PlayerDetail, Profile) without losing tab state or duplicating screen registrations. The Favorites tab registers its own LeagueDetail screen for in-context league navigation.
Push notification pipeline: Two APScheduler jobs (60s interval) handle separate concerns: poll_live_events for in-match events and poll_upcoming_events for pre-match alerts. A single job cannot cover both because postponed/cancelled statuses only appear in the full daily fixture list, not the live-events endpoint.

Sprint Breakdown

  • Sprint 0 (Week 1): Requirements analysis, system architecture design, Gantt chart, dev environment setup.
  • Sprint 1 (Week 2): API-Football v3 integration, standings screen, matches screen with date/status filters, backend TTL caching layer.
  • Sprint 2 (Week 3): Team analytics tab with sparkline charts, dark/light mode via system theme, per-section error states with retry, pull-to-refresh.
  • Sprint 3 (Week 4): League search by name and country, match detail screen (H2H, lineups on pitch, form badges, match stats), team profile, player profile, top scorers, root stack navigation refactor.
  • Sprint 4 (Week 5): Haptic feedback, blur UI (tab bar + header), live match auto-polling, team season fixtures browser, global team search, EAS build configuration, 28-test pytest suite, documentation.
  • Sprint 5 (Post-delivery): Supabase auth (email/password, Google OAuth, Apple Sign-In), profile screen, system theme, Favorites tab, AuthGate modal, push notifications for 8 live event types, backend JWT middleware, Railway cloud deployment.
  • Sprint 6 (Post-delivery): OTP email verification for password change, password strength enforcement, profile name editing, persistent sessions, push token refresh on every app open, pre-match lineups notification, 30-min kickoff reminder, extra time / penalty shootout / postponed / cancelled notifications, scrollable search results with X dismiss, actual last-5-match form badges.

Challenges & What I Learned

  • Navigation architecture matters early. Switching from a flat tab navigator to a root-stack-plus-tabs composition in Sprint 3 required touching many files. Designing navigation before building screens would have reduced this refactoring cost significantly.
  • Push notification reliability requires end-to-end thinking. The notification pipeline spans four independent components (frontend token registration, Supabase push_tokens table, backend scheduler, Expo Push API). Two silent failures surfaced late: the push token was only registered on SIGNED_IN events (missing all subsequent app opens with a persisted session), and lineups notifications only fired when a match went live rather than when lineups actually dropped. Both required tracing the full delivery path to diagnose.
  • Auth adds surface area across every layer. Integrating Supabase auth required coordinated changes across the frontend (AuthContext, OAuth flows), backend (JWT verification, protected endpoints), and three external systems (Supabase project, Google Cloud Console, Apple Developer Portal). Supabase had also migrated from HS256 shared-secret JWTs to ECC P-256 asymmetric keys, which required switching to JWKS-based verification.
  • Iterative polish compounds. Haptics, blur effects, retry buttons, live polling, and scrollable search results each took under an hour individually but together produced an app that feels significantly more polished than the Sprint 2 baseline.
  • Cloud deployment removes a critical dependency. The push notification scheduler requires the backend to be always-on. Deploying to Railway eliminated the dependency on my local machine and made real-device testing for notifications actually reliable.
  • Security is incremental. The initial password change flow allowed anyone with an unlocked device to silently update the account password. Adding OTP verification and password strength requirements in Sprint 6 closed that gap with minimal UX disruption, with three clear steps that match patterns users already recognize.