RTSport API & Data Contract (v0.2)
Status: Draft — Phase 1 MVP
Target Stack: FastAPI, PostgreSQL, Pydantic
Source of truth: docs/contract.md in the rtsport repo
Owner: Daedalus (frontend) + Socrates (backend) — contract-first collaboration
1. Core Data Models (Pydantic Schemas)
These represent the exact JSON shapes passing between the database layer and the API routing layer.
1.1 The Tenant (SchoolSchema)
The multi-tenant boundary. Every operational model below must include a school_id to ensure data isolation.
{
"id": "schl_001",
"name": "Green Bay Preble High School",
"domain": "preble.k12.wi.us",
"config": {
"primary_color": "#0f766e",
"logo_url": "/school/preble/logo.svg",
"sport_names": {
"Soccer": "Boys Soccer",
"Football": "Varsity Football"
}
}
}
1.2 The Athlete (AthleteSchema)
The central node. Roster queries pull this data fast without dragging the clinical payload.
Note: current_status is a cached property. The backend must use a DB trigger or service-layer transaction to update this whenever a new Event is written.
{
"id": "ath_102938",
"school_id": "schl_001",
"first_name": "Marcus",
"last_name": "Johnson",
"grade": 11,
"sports": ["Football", "Track"],
"team": "Varsity",
"parent_ids": ["usr_998877"],
"current_status": "restricted",
"active_case_ids": ["cas_554433"]
}
current_status ENUM: "cleared", "restricted", "out"
1.3 The Case (CaseSchema)
A "Case" represents a single injury lifecycle. An athlete can have multiple cases over their high school career, but usually only one active at a time.
{
"id": "cas_554433",
"school_id": "schl_001",
"athlete_id": "ath_102938",
"title": "Right Ankle Sprain",
"severity": "moderate",
"attention_level": "stable",
"status": "active",
"opened_at": "2026-08-15T14:30:00Z",
"resolved_at": null,
"primary_at_id": "usr_112233"
}
severity ENUM: "mild", "moderate", "severe" (clinical diagnosis, does not change)
attention_level ENUM: "urgent", "warning", "stable" (operational priority, updated by AT daily)
status ENUM: "active", "resolved" (resolved via clearance_granted event; a case can be reopened if re-injury occurs within 30 days)
1.4 The Milestone (MilestoneSchema)
Forward-looking targets. These define the "Phase Bar" UI and are strictly separated from historical events.
{
"id": "mil_001",
"school_id": "schl_001",
"case_id": "cas_554433",
"title": "Phase 2 — Active Rehab",
"target_date": "2026-08-22",
"status": "pending",
"achieved_at": null
}
status ENUM: "pending", "achieved", "skipped"
1.5 The Event (EventSchema)
The workhorse. Every sideline entry, clinical note, or clearance is an Event. Writing an event of type clearance_granted is the atomic action that updates the parent Case status to "resolved".
FERPA Rule: visibility must ALWAYS default to ["at"] on write unless explicitly expanded by the user.
{
"id": "evt_776655",
"school_id": "schl_001",
"case_id": "cas_554433",
"author_id": "usr_112233",
"event_type": "restriction_added",
"visibility": ["at", "coach"],
"content": {
"text": "No lateral cutting drills. Straight line jogging only.",
"clinical_notes": "Moderate swelling medial malleolus. Ice applied 15m."
},
"timestamp": "2026-08-16T09:15:00Z"
}
event_type ENUM: "note", "status_change", "restriction_added", "clearance_granted"
visibility: Array of role keys ("at", "coach", "parent"). The clinical_notes key in content is only ever included in responses when the requesting role is "at".
2. API Endpoints
The HTMX frontend will hit HTML-rendering endpoints (e.g., GET /at/dashboard), but the core application logic relies on these REST endpoints for data mutations and retrieval.
2.1 Read Operations (GET)
GET /api/v1/roster?school_id={school_id}&sport={sport}&team={team}
Returns an array of AthleteSchema.
- Coach role: Filters out non-directory info based on parent opt-outs.
- Team scope: If
teamis provided, only athletes assigned to that team are returned. - Parent role: Not applicable — parents don't access the roster.
GET /api/v1/cases/{athlete_id}
Returns an array of CaseSchema for the given athlete.
- Coach role: Returns active status only (no severity, no clinical details).
- Parent role: Returns cases for own child only.
GET /api/v1/cases/{case_id}/timeline
Returns an array of EventSchema for the given case.
- FERPA Filter: Dynamically strips the
clinical_noteskey and filters visibility array based on requesting role.
GET /api/v1/cases/{case_id}/milestones
Returns an array of MilestoneSchema for the given case.
2.2 Write Operations (POST)
POST /api/v1/events/sideline-entry
The 3-tap rapid entry form — the primary data entry path for ATs on the sideline.
Payload:
{
"school_id": "schl_001",
"athlete_id": "ath_102938",
"body_part": "Ankle - Right",
"severity": "moderate",
"attention_level": "warning",
"removed_from_play": true
}
Action: Automatically creates a new Case if no active case exists for this athlete + injury area. Creates the initial Event. Updates the athlete's current_status to "out" or "restricted" via backend hook.
3. Role-Based Access Matrix (The FERPA Gate)
Every API route must check this matrix via a FastAPI dependency (e.g., Depends(verify_role_access)).
| Entity | Athletic Trainer (AT) | Coach | Parent |
|---|---|---|---|
| Roster Directory | Full Access | Team Only | No Access |
| Athlete Cases | Full Access | Active Status Only | Own Child Only |
| Event Timeline | View All | View visibility: "coach" |
View visibility: "parent" |
| Clinical Notes | Read / Write | HARD BLOCK | HARD BLOCK |
| Restrictions | Write | Read-Only | Read-Only |
| Milestones | Read / Write | Read-Only | Read-Only |
4. Design Decisions
| Decision | Rationale |
|---|---|
Multi-tenant by school_id |
Single Postgres instance serves multiple schools. Data isolation at the query layer. |
current_status is a cached property |
Fast roster queries without joining to Events. Must be synced via trigger or transaction hook. |
| Milestones separate from Events | Milestones are forward-looking targets. Events are backward-looking records. Different lifecycle, different query patterns. |
severity ≠ attention_level |
Severity (mild/moderate/severe) is clinical and static. Attention level (urgent/warning/stable) is operational and changes daily. |
clearance_granted not cleared |
A case can be reopened if re-injury occurs within 30 days. The event name must not imply finality. |
Visibility defaults to ["at"] |
Safety-first. The AT must explicitly expand visibility to coach or parent. No accidental data leaks. |
clinical_notes stripped at the API layer |
The backend filters this key based on role. The frontend never decides who sees clinical data. |