📄 contract.md 7,265 bytes May 01, 2026 📋 Raw

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 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:

{
  "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.
severityattention_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.