Skip to content

Achievements

TravStats hides a small game inside your travel log. 270+ achievements (as of 2.6.0) unlock automatically as you fly, sail, sleep away, log, and explore — across flights, cruises, stays, places, and journeys that combine them. 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 domain — flight, cruise, lodging, place, or cross-domain (shared)
  • A requirement type — what counts toward it (flights_count, distance_km, countries, cruise_ports_unique, sea_days, nights slept, chains, cities, …)
  • 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 change in any domain (add, edit, delete, import). Runs for one account are chained, so a flight saved while a place tick is still being evaluated waits rather than colliding. If you’ve imported a large backlog and the counter doesn’t look fresh, hit Achievements → Refresh to recompute on demand.

Only what has happened counts: a booked cruise, a planned stay or a scheduled flight unlocks nothing. A badge’s unlock date is set only on a genuine unlock and cleared if the badge is revoked, so the catalogue never shows a badge “unlocked below its requirement”.

Domains: flights, cruises, lodging, places, and cross-domain

Section titled “Domains: flights, cruises, lodging, places, and cross-domain”

Every achievement is scoped to a domain:

  • Flight ✈ — earned from your flights (the original catalogue)
  • Cruise 🚢 — earned from your cruises (first cruise, ports visited, distance at sea, sea days, and more)
  • Lodging 🏨 — earned from your stays: nights slept, distinct houses, chains and loyalty programmes, countries slept in, the northernmost night, award nights, ratings, runs away from home
  • Place 📍 — earned from recorded places: cities, continents, streaks, latitudes, ratings, and the curated checklists (world heritage, natural wonders, national parks, museum warships). Visible only on an instance with the beta switch on
  • Cross-domain (shared) — spanning the domains. Some count things that apply to any travel — countries and continents, read through the same evidence rule the passport uses, so a country counted several times under different spellings is one country; others reward combining domains, like a flight and a cruise on the same trip

On the Achievements page you can filter by domain. A domain’s filter appears only when you have that domain enabled; cross-domain achievements are always shown. The country ladder reads your country strictness setting, so tightening it can lower progress on a badge not yet earned — a badge already earned stays earned.

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 travel. Cruise, lodging, place and cross-domain achievements reuse the Explorer, Distance, Collector, and Special categories; Planner and Survivor stay flight-only, and Kurios now reaches into every domain — Friday the 13th, a Christmas flight, a palindrome date, flight number 666, a 777 flown on a 777, an equator crossing at sea, and the like.

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

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.

The cruise catalogue adds its own mechanics, all computed the same way — a function over your cruises:

  • Cruise & port counts — total cruises, unique ports visited, ports in a single voyage, unique ships, cruise-line loyalty
  • Nautical distance — great-circle sum across cruise legs, plus a longest-single-leg check
  • Sea days — total days at sea and consecutive-sea-day streaks (e.g. a transatlantic run)
  • Regions & passages — Mediterranean, Caribbean, Baltic / fjords, canal transits (Panama / Suez), polar and cold-water waters
  • Cabin & deck — balcony, suite, and high-deck sailings
  • Dates at sea — birthday or New Year’s Eve spent aboard

Stays bring their own measures, all decided by the one shared counting rule — a stay counts once you have checked out:

  • Nights and houses — total nights, distinct houses, distinct chains, houses in one city
  • Loyalty — nights per programme, award nights and their value
  • Geography — countries and continents slept in, the northernmost and southernmost night
  • Rhythm — the longest unbroken run away from home, seasons and weekdays
  • Quality — rating ladders on each of the four rating columns

Recorded places count cities, countries and continents, visit streaks, latitude extremes, ratings, and progress on the curated checklists — the world heritage list, the natural wonders, the national parks of Germany and the United States, and fourteen museum warships with two collector badges of their own.

Cross-domain detectors tie the domains together — for example a flight and a cruise on the same trip, or a hotel night in a country no flight ever touched.


All these functions live under backend/src/utils/ and backend/src/services/achievements/; the thresholds and category metadata come from the seed files under backend/src/data/achievementSeeds/, in German and English. 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 header with your total points, unlocked count / total, and overall progress %, then a grid of every visible badge, filterable by domain, category, and tier (plus a “show unlocked only” toggle), 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. The exact shape is in the OpenAPI spec on your instance.

  • 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
  • Place badges need the beta switch. On an instance with the switch off, the places domain is hidden and its badges with it; the progress is kept and reappears when the switch comes on