# 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](https://github.com/NightKnight64/rtsport) **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. ```json { "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. ```json { "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. ```json { "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. ```json { "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. ```json { "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 `team` is 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_notes` key 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:** ```json { "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. |