A full-stack platform for running a college's sports council online: live scoreboards, match history, photo/video galleries, news, schedules, coordinators, and a CMS for the About pages — with a role-based Admin panel to manage all of it.
Verified working end-to-end — the backend was actually installed, seeded,
booted, and exercised through a full login → create match → live score →
end match → match history archive cycle, and the frontend was built with
vite build with zero errors, before this was handed to you.
| Layer | Technology |
|---|---|
| Backend | FastAPI (Python), SQLAlchemy ORM |
| Frontend | React 18 + Vite + Tailwind CSS |
| Database | PostgreSQL (SQLite supported for quick local dev) |
| Auth | JWT (access + refresh tokens), bcrypt password hashing |
| Real-time | WebSockets (live scoreboard) |
| File storage | Local disk, S3-compatible-ready (AWS/MinIO/Wasabi/R2) |
| Containerization | Docker + docker-compose |
college-sports-app/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app, router wiring, CORS, static mount
│ │ ├── config.py # Settings (env-driven)
│ │ ├── database.py # SQLAlchemy engine/session
│ │ ├── models/ # SQLAlchemy models (users, sports, matches, media…)
│ │ ├── schemas/ # Pydantic request/response schemas
│ │ ├── routers/ # REST endpoints, one file per resource
│ │ └── core/ # security (JWT/bcrypt), deps (auth/RBAC),
│ │ # storage (local/S3 abstraction), ws_manager
│ ├── seed.py # Creates default admin + 7 sports + About pages
│ ├── requirements.txt
│ ├── Dockerfile
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── api/ # Axios client + grouped endpoint functions
│ │ ├── context/ # Auth + Theme (dark/light) providers
│ │ ├── hooks/ # useMatchSocket (WebSocket live scores)
│ │ ├── components/ # Navbar, ScoreBoard, PDFViewer, modals…
│ │ └── pages/
│ │ ├── user/ # Dashboard, Sports, Gallery, News, Schedule…
│ │ └── admin/ # Sports/LiveScore/Media/News/Schedule/CMS mgmt
│ ├── package.json
│ ├── Dockerfile
│ └── nginx.conf
└── docker-compose.yml
cd college-sports-app
cp backend/.env.example backend/.env
docker compose up --build- Frontend: http://localhost
- Backend API docs (Swagger): http://localhost:8000/docs
- Postgres: localhost:5432 (user:
sports_user/ pass:sports_pass/ db:college_sports)
Then seed the default admin account and the 7 sports:
docker compose exec backend python seed.pyDefault admin login: admin@college.edu / Admin@12345 (change this in
backend/.env before deploying anywhere real).
cd backend
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# For a zero-setup smoke test, edit .env and set:
# DATABASE_URL=sqlite:///./dev.db
# For real use, point DATABASE_URL at a running Postgres instance instead.
python seed.py
uvicorn app.main:app --reloadBackend runs at http://localhost:8000 — interactive API docs at /docs.
cd frontend
npm install
npm run devFrontend runs at http://localhost:5173 and proxies /api + /media to
http://localhost:8000 (see vite.config.js) — no CORS setup needed in dev.
- Single
Usertable with aroleenum (admin/user) rather than separate tables — avoids duplicating auth logic while still satisfying the "Users" + "Admins" data model from the spec. Match(live, mutable) vsMatchHistory(permanent, immutable) — admins score a match live viaMatch; callingPOST /matches/{id}/endautomatically snapshots the final result intoMatchHistory, exactly as specified.LiveScoreEventis an append-only log of every score change, used to drive the WebSocket feed.- Storage abstraction (
app/core/storage.py) — every upload route callsstorage.save(file, folder)and gets back a URL. FlippingSTORAGE_BACKEND=local→s3in.envmoves every upload (rulebooks, photos, videos) to any S3-compatible bucket with zero route changes. - WebSocket per match (
/api/matches/ws/{match_id}) — the frontend'suseMatchSockethook subscribes only to the match currently on screen, with automatic reconnect + backoff. - JWT access + refresh tokens — access tokens are short-lived (60 min
default); the Axios client in
frontend/src/api/client.jstransparently refreshes on a 401 and retries the original request.
All routes are prefixed /api. Full interactive docs (with request/response
schemas) are auto-generated at /docs (Swagger) and /redoc.
| Resource | Base path | Notes |
|---|---|---|
| Auth | /api/auth |
register, login, refresh, me |
| Users | /api/users |
admin-only directory (for captain picks) |
| Sports | /api/sports |
CRUD, rulebook upload, captain assignment |
| Matches | /api/matches |
live score CRUD + ws/{match_id} socket |
| Media | /api/media |
albums + photo/video upload |
| News | /api/news |
CRUD, search + category filter + pagination |
| Schedule | /api/schedule |
CRUD, filter by type/sport |
| Coordinators | /api/coordinators |
CRUD |
| Pages (CMS) | /api/pages/{slug} |
About College / About Developers |
Admin-only endpoints are protected by a require_admin dependency that
validates the JWT and checks role == "admin" — enforced server-side, not
just hidden in the UI.
See backend/.env.example for the full list (database, JWT secret/expiry,
CORS origins, storage backend, S3 credentials, default admin seed values).
Change JWT_SECRET_KEY and the default admin password before any real
deployment.
- Swap
Base.metadata.create_all()(used for dev convenience on startup) for proper Alembic migrations before going to production. - Put the FastAPI app behind a real ASGI server setup (e.g. multiple
Uvicorn/Gunicorn workers) — the in-memory
ConnectionManagerfor WebSockets would need to move to Redis pub/sub if you scale to multiple backend processes. - Set
STORAGE_BACKEND=s3and fill in the S3 credentials in.envfor a horizontally-scalable, CDN-friendly media pipeline.