Achievements
TravStats hides a small game inside your flight log. Over 100 achievements unlock automatically as you fly, log, and explore. You don’t do anything to get them — they’re computed from your existing data — but they make it more interesting to come back and see what you’ve been up to.
The full catalogue lives at Achievements in the top nav.
How unlocking works
Section titled “How unlocking works”Every achievement has:
- A code like
FREQUENT_FLYER_50(internal ID, never changes) - A requirement type — what counts toward it (
flights_count,distance_km,countries,night_flights, …) - A threshold — the value at which it unlocks
- A tier — bronze → silver → gold → platinum → diamond
- Point value — added to your total when unlocked
Recomputation runs automatically after every flight change (add, edit, delete, import). If you’ve imported a large backlog and the counter doesn’t look fresh, hit Achievements → Refresh to recompute on demand.
The eight categories
Section titled “The eight categories”The catalogue is grouped into eight thematic categories. You can filter by category in the UI; each category covers a different slice of your travel behaviour.
| Category | What it tracks |
|---|---|
| Explorer | Total number of flights + special places (islands, micro-states, high-altitude airports) |
| Distance | Total kilometres, total hours, single-leg extremes |
| Collector | Variety — how many different countries / airlines / airports / continents / aircraft types |
| Elite | Premium experiences — First / Business class, single-airline loyalty, wide-body and long-haul |
| Special | Time-of-day, streaks, route repeats, polar / equator crossings |
| Planner | Planning behaviour — how far ahead you book, notes, regular logging |
| Survivor | When things went wrong — cancellations, delays, tight connections |
| Kurios | Easter eggs — birthdays, NYE in flight, Pi Day, etc. |
Each category has achievements at multiple tier levels. The thresholds grow exponentially, so the tier system stays meaningful even after years of flying.
How the individual achievements work
Section titled “How the individual achievements work”Behind every badge sits a small function that runs over your flights. It falls into one of about a dozen detection patterns — this is the full catalogue of mechanics TravStats uses:
Threshold counters
Section titled “Threshold counters”The simplest form: a counter that fires at a fixed value. This covers total flights, total kilometres, total flight hours, the longest single leg, or the number of distinct countries / airlines / airports / aircraft-types / continents. The catalogue ships multiple thresholds per counter — an early one, an ambitious one, an extreme one.
Set-match achievements
Section titled “Set-match achievements”“Have you visited all of these places?” — the list is hardcoded. Examples (without spoiling the badge names): a set of Scandinavian capital airports, a set of historical pilgrimage centres, a set of European micro-states, or the three big airline alliances. Match runs on IATA-code basis.
Aircraft-class achievements
Section titled “Aircraft-class achievements”Wide-body vs. turbo-prop vs. jumbo are detected from the aircraft
type strings on your flights. Classification runs as a substring
match against known ICAO codes (e.g. A38, B77, B78, A35
for wide-body), so different spellings (“Boeing 777-300ER”, “B777”,
“777”) all match.
Distance and geo predicates
Section titled “Distance and geo predicates”Some achievements check geometric properties of the flight path:
- Distance bucket — single-leg < 250 km (micro-flight),
5,000 km (long-haul), > 11,000 km (marathon), > 15,000 km (ultra)
- Equator crossing —
dep.lat × arr.lat < 0, i.e. a sign change in latitude - Polar flag — any coordinate above 66.5° N (the Arctic Circle)
- Ocean heuristic — ≥ 5,000 km counts as an ocean crossing (no actual coastline check)
- High-altitude airport — airport elevation ≥ 2,500 m, looked up in the cached airport database
- Island flight — both endpoints are flagged as island airports in the database
Time-of-day buckets
Section titled “Time-of-day buckets”Using local departure time (timezone-aware via the airport’s IANA zone), flights drop into buckets: 04:00–07:00 (morning), 06:00–12:00 (forenoon), 12:00–18:00 (afternoon), 18:00–24:00 (evening), 00:00–06:00 (night), 23:00–05:00 (red-eye). Achievements count how many flights fall into a given bucket.
Streak detectors
Section titled “Streak detectors”Achievements that measure consecutive events:
- Same-seat streak (window / middle / aisle) — chronological order, the counter resets on a non-matching seat
- Same-route streak — the same route on three consecutive days
- Longest travel chain — arrival airport equals next departure airport, within 24 h, chained as far as possible
- Consecutive months — at least one flight in every calendar month over a 6- or 12-month window
Per-window buckets
Section titled “Per-window buckets”Unlike streaks, these count how many flights fall into one time window — day, month, year. Examples: busiest single day on record, month with 10+ flights, year with 50+ flights.
Season coverage
Section titled “Season coverage”Four meteorological seasons (by departure date: spring = March–May, etc.). Unlocks when you have at least one flight in every season. Optional 12-month windowing.
Calendar-day match
Section titled “Calendar-day match”Very specific date triggers — birthday, 31 October, 14 March (Pi
Day), 4 May, 7 December (ICAO Day), 19 August (Wright Brothers),
29 February (leap day). Matches on departureTime.month and .day
in UTC.
Time-of-flight predicate
Section titled “Time-of-flight predicate”Achievements that check the flight as a span, not just departure: crossing midnight from 31 Dec into 1 Jan in flight is detected via departure < midnight UTC and arrival ≥ midnight UTC.
Time-traveller detector
Section titled “Time-traveller detector”Special case: local hour at arrival < local hour at departure. Needs both IANA timezones from the airport database. Triggers regularly on westward date-line crossings (Asia → US).
Geometric matches
Section titled “Geometric matches”Pi Precision is the most exotic: Pi Day (14 March) and the great-circle distance ≈ 3,141 km, ±5 % tolerance. Both must hold.
Status / flag counters
Section titled “Status / flag counters”Count flights with specific property values: cancelled (status =
cancelled), delayed (planned vs. actual time > 60 min), First /
Business class (seat class), low-cost airline (carrier tag in the
dataset), window / aisle / middle (seat letter).
Connection detector
Section titled “Connection detector”Tight Connector: two flights of the same user with
(B.departureTime − A.arrivalTime) < 45 min and
A.arrivalAirport == B.departureAirport. Both must be flown.
Notes counter
Section titled “Notes counter”Counts flights with non-empty notes field. Trivial, but baked in
to reward logging behaviour.
Alphabet coverage
Section titled “Alphabet coverage”Alphabet Soup is set-based on the first letter of every visited IATA
code. As soon as {first_letter ∀ visited_iata} reaches 26
elements, the achievement fires.
All these functions live in backend/src/utils/achievementChecks.ts
(plus achievementData.ts and achievementStats.ts for helpers).
The thresholds and category metadata come from the seed files under
backend/src/data/achievementSeeds/. A new achievement type needs
two places: an entry in the seeds + a detection function (or
reuse of an existing one).
Hidden achievements
Section titled “Hidden achievements”Some achievements are flagged isHidden: true in the catalogue.
They show up in your list only after you’ve unlocked them —
easter eggs are meant to stay easter eggs. The proportion of hidden
achievements isn’t visible up front; only the gap between
“visible-plus-unlocked” and “total unlocked” hints that there are
more out there you haven’t triggered yet.
Hidden achievements often turn on very specific constellations (particular dates, seat-letter streaks, route repetitions). Some are deliberately hard to trigger; others fall into your lap with normal travel over time.
Where you see them
Section titled “Where you see them”- Profile badge on the dashboard shows your latest unlock and total points
- Achievements page is the full overview — a grid of every visible badge, filterable by category and tier, with progress bars on the locked ones
- Toast notification when you save a flight or finish an import: a brief ”🎉 Achievement unlocked: X” pops up for each newly-fired badge
Tiers and points
Section titled “Tiers and points”Tier levels are primarily visual — they have no functional effect, they just sort the catalogue and hint at difficulty:
| Tier | Colour tone | Typical point range |
|---|---|---|
| Bronze | warm brown | 10–35 |
| Silver | grey | 30–75 |
| Gold | yellow-gold | 50–250 |
| Platinum | blue-silver | 200–500 |
| Diamond | cyan-violet | 500–1,500 |
Total points is the sum across your unlocked achievements. There’s no leaderboard — TravStats is single-tenant per user, your achievements are yours alone. The number is still satisfying to watch grow.
What happens behind the scenes
Section titled “What happens behind the scenes”The catalogue is maintained as code under
backend/src/data/achievementSeeds/. Every TravStats release can
add new achievements; they get synced into the database on boot
(idempotent — your existing user-unlocks aren’t lost).
If you spot a new badge after an update that looks “already unlocked”: that’s intentional — newly introduced achievements retroactively scan your existing flight history, so you don’t have to fly a special flight just to earn the new Halloween Flyer badge when you happen to have flown on 31 October ten years ago.
API access
Section titled “API access”Read your achievement state via PAT:
curl -fsS -H "Authorization: Bearer $TOKEN" \ https://travstats.example.com/api/v1/achievementsReturns the full catalogue with per-achievement progress and unlock
state for the authenticated user. Useful for building your own
“year in review” pages or syncing to other tools. The endpoint
needs a write-scoped PAT because the same route also handles the
recomputation trigger.
Limitations
Section titled “Limitations”- No achievement editor — the catalogue ships with the app. User-defined achievements aren’t supported (yet)
- No retroactive toast — when a release brings new achievements, they unlock retroactively but no toast fires per back-filled badge. You’ll just notice them on the next Achievements-page visit
- Some hidden achievements depend on fields you may not have logged (seat letter for window streaks, exact flight number for Re-Roll). Imports without those fields leave those achievements locked — but the next boarding-pass scan or manual edit will catch up