Public Logger Links
Public logger links allow meet officials or external contributors to record finish times and incidents for an event without needing an Athlora account. A coach creates and manages the shareable link from the active Live Logger; the official opens it, identifies themselves, and submits timeline entries through a token-authenticated session.
All paths are relative to /api/v1.
Owner Endpoints (Authenticated)
Mounted at /events/:eventId/public-loggers.
Owner access is scoped to event participation: coaches in the event-owning workspace and coaches in fixture-guest workspaces whose fixture acceptance is still accepted at the current revision may create, list, and revoke links. Any other workspace (including stale or retracted fixture guests) receives the generic 404 NOT_FOUND at the ownership middleware before the service runs. Host-only event lifecycle controls (start/complete/cancel, finalization) remain restricted to the event-owning workspace.
| Method | Path | Purpose |
|---|---|---|
POST | /events/:eventId/public-loggers | Create a new public logger link |
GET | /events/:eventId/public-loggers | List all links for the event |
DELETE | /events/:eventId/public-loggers/:linkId | Revoke a link |
Create link
POST /events/:eventId/public-loggers
Returns { data: { link: PublicLoggerLink, token: string } }. The token is shown once and must be shared with the official. Links can only be created for scheduled or in_progress events.
Response:
{
"data": {
"link": {
"id": "uuid",
"eventId": "uuid",
"status": "active",
"createdAt": "2026-01-01T00:00:00Z",
"revokedAt": null
},
"token": "base64url-token"
}
}
List links
GET /events/:eventId/public-loggers
Returns all links (active and revoked) for the event, ordered by creation date.
Revoke link
DELETE /events/:eventId/public-loggers/:linkId
Sets the link status to revoked. Existing sessions lose access immediately. The next online client response also removes that session's token-scoped offline queue and cached snapshots. A device that remains offline retains local data until it reconnects, but cannot submit it after revocation.
Public Endpoints (Unauthenticated)
Mounted at /public/logger. These routes do not require a JWT — they use session tokens instead.
| Method | Path | Purpose |
|---|---|---|
POST | /public/logger/sessions | Start a session using a link token |
GET | /public/logger/events/:eventId | Get event snapshot (participants + timeline) |
POST | /public/logger/events/:eventId/entries | Submit a timeline entry |
PATCH | /public/logger/events/:eventId/entries/:entryId | Correct the caller's own public entry |
DELETE | /public/logger/events/:eventId/entries/:entryId | Undo the caller's own public entry |
GET | /public/logger/events/:eventId/discipline-sessions | Get multi-discipline meet sessions, entrants, entries, and results |
POST | /public/logger/events/:eventId/discipline-sessions/:disciplineSessionId/entrants/:entrantId/entries | Submit a selected-session observation |
PUT | /public/logger/events/:eventId/discipline-sessions/:disciplineSessionId/entrants/:entrantId/entries/:entryId | Replace the caller's own session entry |
DELETE | /public/logger/events/:eventId/discipline-sessions/:disciplineSessionId/entrants/:entrantId/entries/:entryId | Undo the caller's own session entry |
POST | /public/logger/sync/batch | Submit a batch of offline actions |
Start session
POST /public/logger/sessions
Body: { linkToken: string, name: string, club: string }
The official provides the shareable link token, their name, and their club. Returns a session token and an event snapshot with participants and current timeline.
Response:
{
"data": {
"sessionToken": "base64url-session-token",
"snapshot": {
"event": { "id": "uuid", "title": "Spring Invitational", "status": "in_progress" },
"participants": [
{ "athleteId": "uuid", "name": "Usain Bolt" }
],
"timeline": [...]
}
}
}
The session token is passed as X-Public-Logger-Session header on subsequent requests. Sessions expire after a configurable TTL (default 2 hours, min 15 minutes, max 240 minutes via PUBLIC_LOGGER_SESSION_TTL_MINUTES).
The session snapshot identifies legacy single-discipline links with event.discipline. A null discipline denotes a generic meet: the public UI loads the session snapshot below and lets the official choose an active discipline session.
Get snapshot
GET /public/logger/events/:eventId
Header: X-Public-Logger-Session: <session-token>
Returns the current event snapshot: event metadata, the complete event participant list (including fixture guest-club athletes), and active timeline entries. Entries omit recordedBy, publicLoggerSessionId, deviceId, updatedAt, deletedAt, and coach notes. Each entry includes canEdit and canUndo only when it was created by the current public session.
Submit entry
POST /public/logger/events/:eventId/entries
Header: X-Public-Logger-Session: <session-token>
Body: { athleteId, entryType, value?, unit?, incidentType? }
Creates a timeline entry attributed to the public logger session. The event must be in_progress. The athlete must be a participant of the event. After insertion, event results are automatically recomputed.
Correct or undo an entry
PATCH /public/logger/events/:eventId/entries/:entryId
DELETE /public/logger/events/:eventId/entries/:entryId
Header: X-Public-Logger-Session: <session-token>
Body: { expectedVersion, value? | incidentType? }
Public officials can correct or undo only entries attributed to their current public session. Both operations use optimistic versions and recompute event results. They cannot alter coach, assistant, or other public officials' entries. The existing coach role is the head-coach authority and can override any timeline entry or result through the authenticated console.
Multi-discipline meet sessions
GET /public/logger/events/:eventId/discipline-sessions
Header: X-Public-Logger-Session: <session-token>
Returns the public-safe discipline catalogue, entrants (relay teams include ordered members[] with relayMemberId, leg, name, and guest flag), relay leg names, session status, result state, session entries, and computed results for the linked meet; relay results include relayLegs[] with each official split value or null while a leg awaits selection. The UI presents only in_progress sessions for writing. It can record timed observations, field attempts including fouls, vertical-event state, and approved incident codes against the selected (disciplineSessionId, entrantId) target. The recorder's own attempts and DQ/DNF/DNS incident entries expose the same versioned correction/undo capability; undoing an incident recomputes the affected result.
The public meet logger UI renders discipline tabs, the relay/athlete logging rows, each entrant's official split values and logged entries, and each entrant's current result — it deliberately has no standings table and no CSV export, so results are read from the snapshot rather than exported. The three nested entry routes use the same header. Create accepts a session-entry payload; replacement includes expectedVersion; undo requires expectedVersion. For a relay session every value attempt carries relayMemberId so the split belongs to one athlete — a team-level value attempt is rejected with 400 ('Relay results must be recorded as a split for each athlete'), a split with an incident, foul, or note is rejected with 400, a member of another team returns 404, and a replacement may omit relayMemberId (the previous member is kept) but may not move a split to a different athlete. Public responses omit workspace attribution, logger identity, device identifiers, and note text; relay legs carry the same isPb/isSb markers as the coach console so an official split that is an athlete's relay PB is labelled in place. Public session entries cannot be note entries and cannot contain noteText; coach-only notes, roster/lifecycle controls, and official leg selection remain authenticated-only.
Batch sync (offline)
POST /public/logger/sync/batch
Header: Authorization: Bearer <session-token>
Body: {
eventId: string,
deviceId: string,
actions: PublicSyncActionInput[]
}
Queues multiple offline actions for batch processing. The public logger frontend enqueues actions in IndexedDB when offline and drains them via this endpoint on reconnect.
For a multi-discipline meet, every action carries a target with disciplineSessionId and entrantId. A batch is either entirely legacy event actions or entirely targeted session actions; mixed batches are rejected. This prevents an offline action from being applied to the wrong discipline or entrant.
Action input:
| Field | Type | Required | Description |
|---|---|---|---|
actionId | UUID | Yes | Client-generated UUID (idempotency key) |
actionType | create_entry | edit_entry | undo_entry | Yes | Action type |
payload | object | Yes | Action-specific data |
expectedVersion | number | No | For edit/undo — used for audit logging |
clientTimestamp | ISO 8601 | Yes | When the action was created on the client |
Conflict resolution: Last-write-wins. If an edit targets a stale version, the conflict is logged in public_sync_conflict_log and the winning value is applied. This differs from the authenticated endpoint which rejects stale edits with VERSION_CONFLICT.
Response:
{
"receipts": [
{
"actionId": "uuid",
"status": "accepted",
"entryId": "uuid",
"serverVersion": 2
},
{
"actionId": "uuid",
"status": "rejected",
"code": "ENTRY_NOT_FOUND"
}
],
"recomputedResults": true
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
athleteId | UUID | Yes | The athlete this entry is for |
entryType | attempt | split | penalty | note | Yes | Entry type |
value | number | No | Time in seconds (for attempt) |
unit | seconds | No | Unit (normalized to seconds) |
incidentType | false_start | dq | dnf | dns | lane_infringement | No | Incident type |
Response: the created timeline entry (without attribution fields).
Security
- Link tokens are stored as SHA-256 hashes — the raw token is shown only once
- Session tokens are also stored as SHA-256 hashes
- Sessions are scoped to a single event and expire after the TTL
- The event must be
in_progressfor entry submission - All participants of the linked event, including fixture guest-club athletes, can receive entries
- Public officials can edit and undo only their own entries; the authenticated
coachrole can override any entry - The public logger cannot view coach notes, athlete dates of birth, or other private data
- Public session writes are scoped to a linked event, its selected session, and its selected registered entrant; direct-route device IDs are discarded and note content is rejected
Database Tables
public_logger_links— shareable link records (token hash, status, event association)public_logger_sessions— active sessions (token hash, logger identity, expiry, device ID)timeline_entries.public_logger_session_id— links entries back to the session that created thempublic_sync_action_receipts— idempotent receipts for batch sync actionspublic_sync_conflict_log— audit log for last-write-wins conflict resolution
AI declaration
This document was created or updated with the assistance of OpenCode[openai/gpt-5.6-terra].