Skip to content

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.

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

CategoryWhat it tracks
ExplorerTotal number of flights + special places (islands, micro-states, high-altitude airports)
DistanceTotal kilometres, total hours, single-leg extremes
CollectorVariety — how many different countries / airlines / airports / continents / aircraft types
ElitePremium experiences — First / Business class, single-airline loyalty, wide-body and long-haul
SpecialTime-of-day, streaks, route repeats, polar / equator crossings
PlannerPlanning behaviour — how far ahead you book, notes, regular logging
SurvivorWhen things went wrong — cancellations, delays, tight connections
KuriosEaster 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.

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:

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.

“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.

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.

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 crossingdep.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

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.

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

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.

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.

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.

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.

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).

Pi Precision is the most exotic: Pi Day (14 March) and the great-circle distance ≈ 3,141 km, ±5 % tolerance. Both must hold.

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).

Tight Connector: two flights of the same user with (B.departureTime − A.arrivalTime) < 45 min and A.arrivalAirport == B.departureAirport. Both must be flown.

Counts flights with non-empty notes field. Trivial, but baked in to reward logging behaviour.

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).

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.

  • 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

Tier levels are primarily visual — they have no functional effect, they just sort the catalogue and hint at difficulty:

TierColour toneTypical point range
Bronzewarm brown10–35
Silvergrey30–75
Goldyellow-gold50–250
Platinumblue-silver200–500
Diamondcyan-violet500–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.

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.

Read your achievement state via PAT:

Terminal window
curl -fsS -H "Authorization: Bearer $TOKEN" \
https://travstats.example.com/api/v1/achievements

Returns 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.

  • 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