Handicap System
What is a handicap?
Your handicap is your personal start time advantage. A longer handicap means you start earlier. The system targets everyone finishing at roughly the same time โ so a slow runner starts well ahead of a fast runner.
Target finish times
| Distance | Target |
|---|---|
| 1K | 26:00 |
| 3K | 38:00 |
| 6K | 44:00 |
Adjustment rules
| How you ran vs target | Handicap change |
|---|---|
| Finished early (faster than target) | +seconds early, rounded UP to nearest 10 seconds |
| 0โ60 seconds late | No change |
| 61โ120 seconds late | โ10 seconds |
| More than 120 seconds late | โ20 seconds |
DNF โ no adjustment
A runner who did not finish keeps their stored handicap unchanged. The values 0:00 for start and 99:00 for finish are placeholders used in DNF storage โ they are not real times and must not be used in calculations.
Wrong Handicap formula
When a runner starts at the wrong time, the system cannot use the normal formula (which assumes start = correct handicap). Instead it computes an effective finish time:
effective_finish = storedHandicap + (clockFinish โ actualStart)
This effective finish is then compared to the target using the standard adjustment rules. Both wrong-hcp-early and wrong-hcp-late runners have their handicap adjusted via this formula.
New runners
A new runner's opening handicap is set from the DOS master list at season start. If no DOS value exists, the coordinator sets it manually before Week 1.
Season reset
At the start of each season, 2 minutes are subtracted from every runner's handicap. This is applied once, after the final week of the previous season is processed.
Points System
Race points by position
| Position | Points |
|---|---|
| 1st place | 20 |
| 2nd place | 15 |
| 3rd place | 10 |
| 4th place | 8 |
| 5th and beyond | 5 |
Special entry types
| Type | Eligible for placing? | Points | Handicap adjusted? |
|---|---|---|---|
| Normal runner | Yes | Per position | Yes |
| Wrong hcp โ started late | Yes | Per position | Yes (DOS-parity formula) |
| Wrong hcp โ started early | No | 5 | Yes (DOS-parity formula) |
| Early runner | Yes | Per position | Yes |
| Early runner + volunteer | Yes | Higher of position points or 8 | Yes |
| Time Trial | No | 5 | Yes |
| DNF | No | 8 (if also volunteering) | No |
| Volunteer (did not run) | No | 8 | No |
max(position_points, 8). A runner who finishes 3rd (10 pts) and volunteers gets 10 pts. A runner who finishes 5th (5 pts) and volunteers gets 8 pts.Placing groups
Runners compete within their distance ร gender ร age group. Age is calculated as the age you turn in the current calendar year, not your age on race day. Placings are by clock finish time โ earliest finisher wins.
Cross-distance
A runner can run a different distance each week. Their placing is only within runners of the same distance that week.
Season Structure
The season runs for 24 weeks on Sundays from late January to late July.
Monthly blocks
The season is divided into 6 blocks of 4 weeks each. Monthly blocks are used for monthly prize computation.
Monthly prizes
| Rule | Detail |
|---|---|
| Prize scope | One prize winner per age group ร gender ร distance per block |
| Winner determination | Most points accumulated in the block within their category |
| Tie-break | Most weeks run in the block |
| One-win limit | Once a runner wins a monthly prize, they are excluded from winning again in future months that season |
Season standings
Accumulated points across all weeks. Updated live after each week is saved. Grouped by distance ร gender ร age group.
Age Groups
How determined
Age = the age the runner turns in the current calendar year (not their age on race day). This is computed from their date of birth stored at registration.
2026 age brackets (by distance)
Groups are set per distance ร gender. The exact brackets are configured in the Google Sheet's config tab. Typical groups: 8 & under, 9โ11, 12โ14, 15โ46, 47โ59, 60+.
Adjustment period
At the start of the season, age groups are re-evaluated based on the new calendar year. Runners who move into a new age group at year-change will compete in the new group from Week 1.
Locking
Age groups are locked per season once Week 1 is saved. Mid-season corrections require an admin recalculate.
Splits
If an age group has fewer than 3 runners across all weeks, it may be merged with an adjacent group for prize purposes. The coordinator decides.
Distances
Three race distances: 1K, 3K, 6K.
Distances alternate on a rotating weekly schedule (1K โ 3K โ 6K โ repeat). A runner may run any distance each week regardless of their "preferred" distance. Distance for a given week is per-entry, not per-runner.
Storage format
Always stored as 1K, 3K, 6K (with the K suffix). Never as bare integers (1, 3, 6). This applies in both Google Sheets and Supabase.
No preferred distance at registration
Runners do not nominate a preferred distance at registration time for racing purposes. Volunteer points are assigned to their preferred distance (set in their registration record).
Registration and Bib Numbers
Registration
Runners register via Google Form. Registration data flows into the Runners tab of the main app. New runners appear as PENDING until a bib is assigned.
Bib pool
Bibs are assigned from a pre-allocated pool. Numbers are permanent within a season โ a bib assigned to a runner stays with them for all 24 weeks. Do not reassign a bib that has been used in any week's results.
Duplicate detection
The system flags if the same bib appears more than once in a week's results. Fix before saving.
Permanent bibs
Once a bib number has results recorded against it, it should not be changed, even if the runner's name is corrected. Name corrections go through the name-aliases mechanism.
Name aliases
Long hyphenated names may truncate in source data. Unresolved name mismatches are added to data/name-aliases.csv โ not hand-edited in the results rows.
Data Storage
Dual storage architecture (2026)
The system uses Google Sheets as the primary write destination and Supabase as the read-first data source.
| Layer | Role |
|---|---|
| Google Sheets | Primary write: results, runners, volunteers, config |
Supabase (lrfhvesylfwbabzxbqhr) | Read-first: results, handicaps, result-weeks |
When results are saved, the system writes to Google Sheets first, then mirrors to Supabase via supabase-mirror.js.
Handicaps in Supabase
There is no separate handicaps table in Supabase. Handicaps are derived from the latest results.handicap per (name, distance) across all weeks.
Supabase results table columns
| Column | Type | Meaning |
|---|---|---|
started | TEXT | Actual start time used on race day. MM.SS format. |
handicap | TEXT | For backfill: correctHcp (pre-race). For live mirror: newHandicap (post-race). MM.SS format. |
notes | TEXT | Flag: 'DNF', 'WrongHcp', or 'TimeTrial'. No spaces, no dots. |
category | TEXT | Age group + gender, e.g. "15-46 Female" |
distance | TEXT | '1K', '3K', or '6K' (with K suffix) |
UNIQUE (season, week, bib, distance). NULL bibs are excluded (NULL โ NULL in SQL).Time format conventions
Two conventions exist in the system:
| Format | Where used | Example |
|---|---|---|
| Period format (MM.SS) | Supabase, DOS legacy data, internal storage | 25.29 = 25 min 29 sec |
| Colon format (MM:SS) | App display, Google Sheets | 25:29 = 25 min 29 sec |
The dotToColon() function in supabase-reads.js converts period format to colon format on read. Never write colon format into a Supabase column that originated from DOS data.
Per-week tabs (Google Sheets)
After each week is saved, the system creates backup tabs: Week N Results and Week N Handicaps. These are the audit trail. Do not delete them.
Test mode
Add ?test=1 to the main app URL to switch to the test Google Sheet. All writes in test mode go to the test sheet only. A TEST MODE banner appears.
Corrections
Individual result corrections go through the Admin panel. After any correction, run Admin โ Recalculate Standings.
Data Import Methods
| Method | When to use |
|---|---|
| Manual entry | Small corrections โ type bib and time directly |
| Single-file CSV | One file with bib, distance, finish time. Most common. |
| Two-file CSV | Separate bibs file and times file โ scribe and timer worked independently |
| Stopwatch import | Import the Week N Stopwatch tab data directly |
| OCR Scanner | Photo of handwritten race sheet โ AI extracts the values |
Security
Main app PIN
Required to access the main app, OCR scanner, and admin panel. Shared among coordinators. Change at the start of each season.
Admin password
Separate from the PIN. Required for Admin panel actions (corrections, recalculate, register, assign bibs). More restricted distribution.
API
All API routes that serve data (/api/data?type=...) are read-only. Write operations go through PIN-protected Vercel functions. The Supabase service key is server-side only โ never exposed to client JavaScript.
Vercel function limit
The project runs on the Vercel Hobby plan (12-function limit). Do not add new files under api/ โ extend existing handlers instead.
Key Formulas
Standard handicap adjustment
diff = clockFinish โ target
if diff < 0 (finished early):
adjustment = ceil(|diff| / 10) ร 10 โ rounds UP to nearest 10s
elif diff โค 60: adjustment = 0
elif diff โค 120: adjustment = โ10
else: adjustment = โ20
newHandicap = storedHandicap + adjustment
Wrong Hcp adjustment (DOS-parity formula)
effectiveFinish = storedHandicap + (clockFinish โ actualStart) diff = effectiveFinish โ target adjustment = standard_formula(diff) โ same rounding rules apply newHandicap = storedHandicap + adjustment
Early runner + volunteer points
points = max(positionPoints, 8)
Age calculation
age = currentYear โ birthYear
Uses the year the runner turns that age, not their age on race day.
Placing
Within distance ร gender ร age group, ranked by clock finish time ascending.
Season standings
Sum of all weekly points per runner per distance.