The backend is a Python/FastAPI service layer that supports the Football Coach platform's transactional workflows, asynchronous video analysis, tactical alerting, and AI-assisted querying. The repository currently contains roughly 85 Python files across src/, tests/, and scripts/. Its architecture is unusual in a productive way: instead of a single API surface, it runs two cooperating FastAPI applications backed by the same MySQL schema and the same authentication model.
Those two applications are:
- the classic backend API on port
8000, centered on CRUD, RBAC, assistant endpoints, and tactical feedback - the analysis-management API on port
8001, centered on uploads, gRPC orchestration, streaming assets, analysis history, retry, and cancellation
Together, they form the application middle tier between the Flutter frontend and the tracking_engine gRPC service.
Flutter Frontend
-> FastAPI app.py (:8000) team, player, match, notes, assistant, alerts, auth
-> FastAPI analysis_app.py (:8001) upload, history, status, files, retry, cancel
analysis_app.py / AnalysisJobService
-> TrackingEngineClient
-> gRPC AnalysisService (:50051) in tracking_engine/server.py
MySQL (soccer_analytics)
<- users, teams, matches, stats, analysis reports/runs, alerts, insights, segments
Remote LLM
<- assistant RAG answers
<- tactical advisory generation
| Area | Implementation |
|---|---|
| Web framework | FastAPI |
| App servers | Uvicorn |
| Database access | PyMySQL with manual SQL and DictCursor |
| Authentication | OAuth2 password flow + RS256 JWT |
| Authorization | Role and path-based RBAC in dependency layer |
| Analysis transport | gRPC client to tracking engine |
| Assistant architecture | Rule-based intent routing + retrieval + deterministic SQL analytics + remote LLM |
| Realtime transport | WebSocket for tactical alerts, SSE for segment streams |
| Media/file handling | python-multipart, static file serving, byte-range streaming |
backend/
├── .env.example # runtime env template
├── data/
│ ├── full_creation.sql # schema bootstrap
│ └── full_insert.sql # demo/sample seed data
├── proto/
│ └── analysis.proto # gRPC contract shared with tracking engine
├── scripts/
│ ├── rotate_jwt_keys.sh
│ ├── add_uuid_ids_to_inserts.py
│ └── smoke_parse_and_persist.py
├── src/
│ ├── app.py # classic API (:8000)
│ ├── analysis_app.py # analysis API (:8001)
│ ├── controllers/
│ ├── services/
│ ├── models/
│ ├── security/
│ ├── dependencies.py
│ ├── database.py
│ ├── rbac.py
│ └── config.py
└── tests/
└── test_analysis_status.py
This is the general product API. It:
- mounts the static directory for uploaded assets
- configures CORS from
CORS_ALLOW_ORIGINS - exposes
/api/tokenand/api/register - includes the full CRUD controller set
- exposes
/api/assistant/* - exposes tactical alert feedback and metrics routes
- exposes the
/ws/alerts/{match_id}WebSocket
This app is the primary source of truth for organization, football entities, and human interactions.
This is the operational analysis API used by the Flutter analysis screens. It:
- receives video uploads
- records analysis runs in
analysis_runs - submits jobs to the tracking engine over gRPC
- updates status/progress in the database
- exposes analysis history
- serves generated videos and JSON files
- supports retry, cancellation, stale-run cleanup, and segment streams
This app is specialized enough that it should be read as a workflow service rather than a CRUD service.
The repository also still contains a controller-based analysis path inside the classic API (controllers/analysis_controller.py + AnalysisJobService). That means two analysis orchestration styles currently coexist:
- controller-driven job records stored in
analysis_reports - dedicated-analysis-app runs stored in
analysis_runs
The frontend code points primarily to the dedicated analysis app, so that path appears to be the current production-facing flow.
Authentication is based on OAuth2 password login:
POST /api/tokenvalidates email + password- credentials are signed into an RS256 JWT
- the JWT is persisted by the frontend and sent on subsequent requests
JWT key material is loaded by security/jwt_keys.py from either:
- PEM contents in environment variables
- PEM file paths in environment variables
- fallback files in
backend/certs/
The backend distinguishes:
ownerstaff
For staff users, the token may also include:
staff_idteam_idpermission_levelroleapp_roleapp_permissions
rbac.py maps user and staff roles to application roles:
| Input role | App role |
|---|---|
| owner | account_manager |
| head_coach | coach |
| assistant_coach | assistant_coach |
| analyst | analyst |
| anything else | player |
The permission vocabulary is explicit:
accounts.read/create/update/deletefootball.read/writeanalysis.runnotes.read/write
Authorization is centralized in dependencies.get_current_active_user(). The logic is path-aware:
- account managers can view all
/api/*resources - only account managers can modify many team/account resources
- assistant routes are intentionally open to any authenticated active user
- tactical decision feedback routes are explicitly allowed for owners
This makes security highly visible in code, but also means route naming conventions matter.
The schema is defined in data/full_creation.sql and seeded by data/full_insert.sql. It uses UUID-like string identifiers for primary entities and a mix of JSON columns for analytics-heavy payloads.
| Family | Tables | Purpose |
|---|---|---|
| Identity | users, staff |
accounts, roles, permission context |
| Club and squad | teams, players, formations |
roster and tactical structure |
| Competition | events, matches, match_events, match_lineups |
fixtures and match context |
| Performance | player_match_statistics, match_team_statistics |
per-player and team analytics |
| Notes and planning | match_notes, reunions, training_sessions |
collaboration and planning |
| Analysis state | analysis_reports, analysis_runs, video_segments, analysis_segments |
async processing records and outputs |
| Tactical intelligence | tactical_alerts, decision_feedback, tactical_insights |
alert history, coach feedback, long-term insight memory |
analysis_reports: older job/result container, JSON-based report payloadsanalysis_runs: newer dedicated analysis-run table used byanalysis_app.pyanalysis_segments: per-window tactical slices persisted bySegmentServicetactical_alerts: normalized alert packets emitted by the tracking enginedecision_feedback: human acceptance or dismissal decisions plus later effectiveness evaluationtactical_insights: durable analytical summaries used as tactical memory by the assistant stack
The backend deliberately avoids an ORM. Services execute raw SQL through cursor objects and usually commit immediately after writes. This keeps query logic explicit, but it also means:
- schema drift must be managed carefully
- service methods own most query semantics
- consistency rules live in Python rather than in a data-access layer abstraction
| Capability | Representative routes | Backing files |
|---|---|---|
| Auth | /api/token, /api/register, /api/users/me |
app.py, dependencies.py, services/user_service.py |
| Teams and identity | /api/teams, /api/users, /api/staff |
team_controller.py, user_controller.py, staff_controller.py |
| Squad data | /api/players, /api/formations, /api/match_lineups |
player/formation/lineup controllers and services |
| Match domain | /api/matches, /api/match_events, /api/events |
match_controller.py, match_event_controller.py, event_controller.py |
| Statistics | /api/player_match_statistics, /api/matches/{id}/team_statistics |
statistics controllers and services |
| Notes and planning | /api/matches/{id}/notes, /api/reunions, /api/training_sessions |
note, reunion, and training-session services |
| Assistant | /api/assistant/query, /api/assistant/mode |
assistant_controller.py, assistant services |
| Tactical alerts | /api/matches/{match_id}/alerts, /api/decision/feedback, /api/decision/metrics, /ws/alerts/{match_id} |
tactical_alert_controller.py, tactical_alert_service.py |
| Match-scoped analysis | /api/matches/{match_id}/analyze, /api/analysis/{job_id}/status, /api/matches/{match_id}/analysis |
analysis_controller.py, AnalysisJobService |
| Analysis app endpoints | /api/analyze_match, /api/analysis_status/{id}, /api/analysis_history, /api/analysis/files, /api/analysis/{id}/retry |
analysis_app.py |
| Segment APIs | /api/matches/{id}/segments, /api/analysis/{id}/segments, /stream variants |
segment_controller.py, segment_service.py |
The dedicated path in analysis_app.py is the most complete workflow in the repository.
High-level lifecycle:
- A video upload reaches
POST /api/analyze_match. - The upload is stored on disk and an
analysis_runsrecord is inserted. - The app opens a gRPC stream through
TrackingEngineClient. - Progress packets update run status and message fields.
SEGMENT_DONEpackets are persisted toanalysis_segmentsand fanned out to SSE listeners.ALERTpackets are normalized intotactical_alertsand optionally pushed to WebSocket clients.- Final artifacts are exposed through file-serving endpoints.
- The frontend can later:
- read history
- retry
- cancel
- preview videos
- fetch JSON payloads
- replay segment timelines
analysis_app.py contains substantial delivery infrastructure:
- file-path sanitization and output-path resolution
- byte-range video streaming
- JSON preview endpoints
- MP4 "faststart" optimization
- derived preview-video creation
This means the analysis backend is not only a job coordinator but also an asset-serving gateway.
AnalysisJobService in services/analysis_job_service.py handles the classic controller-based analysis flow. It:
- inserts a job shell into
analysis_reports - clears prior alert history for the match
- submits the gRPC request
- updates report progress JSON
- persists segments on streamed
SEGMENT_DONE - broadcasts alerts on streamed
ALERT
This path uses analysis_reports as its storage substrate, in contrast to the analysis_runs table used by analysis_app.py.
controllers/segment_controller.py provides both query and SSE interfaces for analysis segments.
Two scopes are supported:
- match-scoped segment retrieval
- analysis-run-scoped segment retrieval
Internally, SSE fan-out is implemented with in-memory asyncio.Queue listeners keyed by:
match_idanalysis_id
This makes live segment delivery simple and fast, but it also means fan-out state is process-local rather than shared across multiple app instances.
TacticalAlertService is one of the most operationally interesting parts of the backend. It acts as:
- an in-memory broadcaster
- a persistence layer bootstrapper
- a feedback collector
- a suppression manager
- a decision-effectiveness coordinator
- Ensure
tactical_alertsanddecision_feedbacktables exist. - Broadcast incoming alert packets to live WebSocket connections.
- Persist alert history for later retrieval.
- Accept coach feedback (
ACCEPTorDISMISS). - Join alert history with user-specific decision feedback.
- Trigger asynchronous post-feedback evaluation through
DecisionEffectivenessService.
DecisionEffectivenessService checks whether the severity profile improves after an intervention:
- accepted recommendations are considered effective when downstream severity drops enough
- dismissed recommendations are considered risky when later severity escalates
This turns the alert pipeline into a measurable feedback system rather than a one-way notification mechanism.
The assistant subsystem is implemented in services/assistant_service.py and is more structured than a generic chat endpoint.
Pipeline:
IntentRouterclassifies the question using rule-based fuzzy matching.RetrievalServicepulls relevant data from the database under a tight timeout budget.ContextBuilderformats the data into bounded structured blocks.AnalyticalServicecomputes deterministic SQL analytics when the query asks for trends or rankings.TacticalMemoryServiceinjects prior stored insights when relevant.llm_client.call_llm()sends the final prompt to a remote model endpoint.
Assistant intents include:
- player statistics
- match statistics
- team summary
- tactical knowledge questions
- analytical queries
- general fallback questions
The assistant also has a global mode switch:
assistantanalysis
When the system mode is analysis, the assistant returns a structured "unavailable during live analysis" response instead of performing retrieval and generation.
AnalyticalService is important because it keeps high-value football calculations out of the LLM. Current deterministic outputs include:
- top improving players by sprint-intensity trend
- highest average player ratings over recent windows
- best pressing formations by average pressures
When these insights are computed, they are also stored in tactical_insights, which later powers tactical memory in assistant responses.
The code reads the following settings directly:
| Variable | Purpose |
|---|---|
DB_HOST |
MySQL host |
DB_USER |
MySQL user |
DB_PASSWORD |
MySQL password |
DB_NAME |
MySQL database name |
CORS_ALLOW_ORIGINS |
comma-separated allowed origins |
JWT_PRIVATE_KEY / JWT_PUBLIC_KEY |
inline PEM contents |
JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH |
filesystem PEM paths |
LLM_MODE |
remote or local |
REMOTE_LLM_URL |
assistant and tactical-advisory upstream model endpoint |
LLM_TIMEOUT |
remote-LLM timeout seconds |
TRACKING_ENGINE_HOST |
gRPC host for the tracking engine |
TRACKING_ENGINE_PORT |
gRPC port for the tracking engine |
ANALYSIS_OUTPUT_ROOT |
output directory override for analysis assets |
ANALYSIS_ENGINE_HOST / ANALYSIS_ENGINE_PORT |
legacy analysis gRPC client settings |
.env.example already captures the most important backend values for local setup.
cd backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRun:
data/full_creation.sqldata/full_insert.sql
against a MySQL-compatible server. The default code path expects a database named soccer_analytics.
Either:
- provide PEM content/path variables, or
- generate local keys:
cd backend
./scripts/rotate_jwt_keys.shClassic backend:
cd backend/src
source ../.venv/bin/activate
uvicorn app:app --host 0.0.0.0 --port 8000 --reloadAnalysis backend:
cd backend/src
source ../.venv/bin/activate
uvicorn analysis_app:app --host 0.0.0.0 --port 8001 --reloadOr from the repository root:
./start_services.shThat script also starts the tracking engine and applies performance-oriented tracking defaults.
Visible test coverage is currently light:
tests/test_analysis_status.py
Run with:
cd backend
source .venv/bin/activate
pytest| Script | Purpose |
|---|---|
scripts/rotate_jwt_keys.sh |
generate and rotate RS256 JWT keys |
scripts/add_uuid_ids_to_inserts.py |
patch SQL insert scripts to include UUID ids |
scripts/smoke_parse_and_persist.py |
smoke-test parsing/persistence workflow |
- Clear domain-oriented controller and service breakdown
- Explicit SQL makes data flows transparent
- RS256 JWT support is production-friendly compared with simple shared-secret setups
- Realtime tactical feedback is deeply integrated rather than an afterthought
- The assistant stack uses deterministic retrieval and SQL analytics before invoking an LLM
- The analysis app includes practical operational features like stale-run detection, retry, preview generation, and byte-range video streaming
- Two analysis orchestration paths coexist, which increases conceptual load.
- Some stateful components, especially segment SSE listeners and active WebSocket maps, are process-local and would need redesign for horizontal scaling.
- Raw SQL offers clarity but comes with higher maintenance cost as the schema evolves.
- Automated test coverage is sparse relative to the complexity of the system.
- A legacy
AnalysisGrpcServiceremains in the codebase and appears older than the currentTrackingEngineClientflow. - The assistant and tactical advisory pipelines depend on a remote LLM endpoint, which is an operational dependency outside this repository.
For a contributor trying to understand the backend quickly, the highest-yield sequence is:
src/app.pysrc/analysis_app.pysrc/dependencies.pysrc/rbac.pysrc/database.pysrc/controllers/analysis_controller.pysrc/controllers/segment_controller.pysrc/services/tracking_engine_client.pysrc/services/tactical_alert_service.pysrc/services/assistant_service.py
That order follows the real platform path from request entry, through security, into analysis orchestration, then into tactical intelligence and assistant reasoning.