Database schema
PostgreSQL 13+. Every table uses UUID primary keys, created_at/updated_at timestamps, and soft deletes (deleted_at) where the app needs "undo" (timeline_entries, results uses update-in-place with an override trail).
Core tables
users (id UUID PK, auth0_id UNIQUE, name, email UNIQUE, role)
athletes (id UUID PK, coach_id -> users, name, dob, gender, squad, notes,
archived_at, created_at, updated_at)
events (id UUID PK, created_by -> users, type, discipline, title, date, time,
location_name, latitude, longitude, status)
event_participants (event_id, athlete_id, rsvp_status) — PK (event_id, athlete_id) — Stage 2
timeline_entries (id UUID PK, event_id, athlete_id, discipline, entry_type, value, unit,
is_foul, incident_type, note_text, recorded_by, version, device_id, deleted_at)
results (event_id, athlete_id, discipline, outcome, final_result, unit, placing,
is_pb, is_sb, manual_override, override_reason, overridden_by, override_at)
— PK (event_id, athlete_id, discipline)
timeline_entries
The append-only live log — the heart of the app.
entry_type:attempt,split,penalty,note.value+unit: seconds for time, metres/cm for distance/height. For the 100m contract the unit isseconds.is_foul: field-event foul attempts.incident_type:false_start,dq,dnf,dns,lane_infringement.note_text: free-text body fornoteentries.version: bumped on every edit — used for merge-conflict detection (Stage 3).device_id: originating device for offline merge (Stage 3).deleted_at: "undo" is a soft delete, neverDELETE.
results
Derived/materialized from timeline_entries. Recalculated after every entry change.
outcome:no_result|valid|dq|dnf|dns— distinguishes no result, a valid finish, and voided outcomes.final_result: computed value (best valid field attempt, finishing time). Must beNULLfor voided outcomes.is_pb/is_sb: derived flags, not manually logged.manual_override,override_reason,overridden_by,override_at: coach corrections with an audit trail (who corrected, when, and why).
Ownership boundaries
Protected requests resolve the verified Auth0 subject to users.id before resource access. Direct ownership is athletes.coach_id for athletes and events.created_by for events. Timeline entries, participants and results are authorized through both parent relationships: the event must have been created by the current user and the athlete must belong to that user. recorded_by and overridden_by are audit actors, not ownership fields.
Ownership checks use owner-scoped queries and deliberately return the same generic NOT_FOUND response when an identifier is malformed, missing, attached to the wrong parent or belongs to another coach. This avoids revealing another coach's resource IDs. The policy is centralized in backend/src/services/ownership.ts so the Stage 2 sharing model can replace it consistently when assistants and cross-coach fixtures are introduced.
Constraints and indexes
Migration 0002_contract_100m.sql adds CHECK constraints and lookup indexes so invalid state cannot be written:
events:statusinscheduled/in_progress/completed/cancelled;typeincompetition/training; indexes on(created_by)and(status, date).event_participants:rsvp_statusinpending/yes/no; index on(athlete_id).timeline_entries:entry_type,incident_typeandunitdomain checks;value >= 0when present; index on(event_id, athlete_id, discipline).results:outcomedomain check;final_result >= 0,manual_override >= 0,placing > 0; and outcome/value shape rules — voided outcomes (dq/dnf/dns) must not carry afinal_result,validfinishes must, andno_resultmust not.
The MVP discipline is fixed to 100m only at the API/service boundary (see the API contract) — the database discipline column stays free-form TEXT so future disciplines can be added by new migrations.
Migration conventions
Migrations live in backend/src/db/migrations, one file per change, sequentially numbered. Never edit a migration after it has been merged — write a new one. From backend, run npm run db:migrate; applied names and checksums are recorded in schema_migrations.
Current migration
0001_init.sql — the full authoritative base schema, followed by 0002_contract_100m.sql which adds the MVP contract state (athlete archival, result outcomes, override audit timestamp, note storage, domain constraints and indexes). Table and column names are fixed by the build spec (Section 5) and shared with the frontend types and API contracts. Never rename them without flagging to the team and updating the spec first.
Status: applied to the Neon development database and tracked in schema_migrations. New databases and pending migrations are handled with:
cd backend
npm run db:migrate
ERD
An ERD will be drafted from the dev plan's data model and reviewed as a team before further migrations are written.
AI declaration
This document was generated with the assistance of opencode[deepseek-v4-flash-free] and opencode[gpt-5.6-sol].