DEV Community

Jaehoon Lee
Jaehoon Lee

Posted on

How I prevent unsupported places from looking 'live' in a Capacitor travel app

I ran into a product bug that was easy to miss because the UI looked polished: places outside the supported live feed could still carry a numeric crowd value from a catalog or fallback path.

The number was valid JavaScript. It was not valid live data.

That distinction matters in a travel app. A user may change plans based on a badge that says "live", so a plausible-looking fallback is worse than an honest empty state.

This is how I tightened the boundary in Gap-trip, an Android app built with JavaScript, Capacitor, Firebase, and public tourism data.

1. Make provenance part of the type check

Checking only whether a value is numeric was not enough. I added one predicate that requires both a number and evidence that it came from the supported live pipeline.

function isRealtimeCrowdPlace(place = {}) {
  return Number.isFinite(place.crowd) && (
    place.crowdDataAvailable === true
    || place.source === "서울 실시간 도시데이터"
    || place.forecastSource === "seoul-citydata"
    || String(place.id || "").startsWith("seoul-citydata-")
  );
}
Enter fullscreen mode Exit fullscreen mode

The important part is not the exact source names. It is that "has a number" and "is live" are different facts.

2. Normalize unsupported values once

After the predicate runs, unsupported crowd fields are removed before the object reaches the rest of the UI. Weather and air-quality data can still remain available.

const hasRealtimeCrowd = isRealtimeCrowdPlace(place);

return {
  ...place,
  crowd: hasRealtimeCrowd ? place.crowd : null,
  usualCrowd: hasRealtimeCrowd ? place.usualCrowd : null,
  forecast: hasRealtimeCrowd ? place.forecast : [],
  forecastSource: hasRealtimeCrowd ? "seoul-citydata" : "none",
  crowdDataAvailable: hasRealtimeCrowd,
  crowdDataSource: hasRealtimeCrowd ? "realtime" : "none",
};
Enter fullscreen mode Exit fullscreen mode

Doing this at one boundary is safer than asking every card, map marker, detail panel, and share image to remember the same rule.

3. Reuse the same predicate in filters

A "show only places with live crowd data" toggle must use the same definition as the badge renderer. Otherwise the filter and the card can disagree.

.filter((place) => !state.crowdOnly || isRealtimeCrowdPlace(place))
Enter fullscreen mode Exit fullscreen mode

This sounds small, but duplicate definitions of "live" were exactly the kind of drift I wanted to avoid.

4. Degrade the score, not the truth

When crowd data is unavailable, the place does not disappear. The recommendation can still use available conditions such as weather, air quality, travel time, transit access, and nearby facilities. The UI says that the score used the other available conditions instead of fabricating a crowd signal.

This gives up some visual consistency: fewer cards have colorful crowd badges. I think that is the right trade. An empty field is recoverable. Misplaced trust is not.

5. Reject a partially broken batch

A successful HTTP response can still contain too little usable coverage. The app therefore checks whether the live set is healthy before treating the batch as live. The current minimum is 30 supported places.

const MIN_HEALTHY_REALTIME_CROWD_PLACES = 30;

function hasHealthyRealtimeCrowdSet(places = []) {
  return Array.isArray(places)
    && places.filter(isRealtimeCrowdPlace).length >= MIN_HEALTHY_REALTIME_CROWD_PLACES;
}
Enter fullscreen mode Exit fullscreen mode

The backend also times out upstream calls, caches valid results briefly, and returns an explicit unavailable response when the live batch fails. A 200-shaped payload is not automatically a healthy product state.

What I test now

The release scripts include crowd resilience, nearby coverage, soak, and failover checks. The cases I care about are:

  • a valid live value with supported provenance;
  • a numeric fallback with no live provenance;
  • a partial batch below the coverage threshold;
  • an upstream timeout;
  • a place with weather data but no crowd data;
  • the live-only filter after a failed refresh.

The broader lesson for me was simple: availability is a product claim, not just a data shape. If the UI says "live", the code should be able to prove why.

Gap-trip currently applies this rule only where supported public live data is available; it does not claim live crowd coverage for every place. If you want to inspect the behavior:

I would be interested in how other teams represent provenance when multiple public-data sources feed the same UI.

Top comments (0)